Fixed IMAP encoding on non-UTF-8 messages + improved Received headers in SMTP

This commit is contained in:
mdecimus 2023-09-05 12:16:10 +02:00
parent 4e021b228c
commit 56b1fb893d
5 changed files with 174 additions and 72 deletions

View file

@ -112,7 +112,7 @@ pub enum DataItem<'x> {
BodySection { BodySection {
sections: Vec<Section>, sections: Vec<Section>,
origin_octet: Option<u32>, origin_octet: Option<u32>,
contents: Cow<'x, str>, contents: Cow<'x, [u8]>,
}, },
Envelope { Envelope {
envelope: Envelope<'x>, envelope: Envelope<'x>,
@ -127,19 +127,19 @@ pub enum DataItem<'x> {
uid: u32, uid: u32,
}, },
Rfc822 { Rfc822 {
contents: Cow<'x, str>, contents: Cow<'x, [u8]>,
}, },
Rfc822Header { Rfc822Header {
contents: Cow<'x, str>, contents: Cow<'x, [u8]>,
}, },
Rfc822Size { Rfc822Size {
size: usize, size: usize,
}, },
Rfc822Text { Rfc822Text {
contents: Cow<'x, str>, contents: Cow<'x, [u8]>,
}, },
Preview { Preview {
contents: Option<Cow<'x, str>>, contents: Option<Cow<'x, [u8]>>,
}, },
ModSeq { ModSeq {
modseq: u64, modseq: u64,
@ -733,7 +733,7 @@ impl<'x> DataItem<'x> {
} }
match contents { match contents {
BodyContents::Text(text) => { BodyContents::Text(text) => {
literal_string(buf, text); literal_string(buf, text.as_bytes());
} }
BodyContents::Bytes(bytes) => { BodyContents::Bytes(bytes) => {
buf.extend_from_slice(b"~{"); buf.extend_from_slice(b"~{");
@ -1322,7 +1322,7 @@ mod tests {
Section::Mime, Section::Mime,
], ],
origin_octet: 11.into(), origin_octet: 11.into(),
contents: "howdy".into(), contents: b"howdy"[..].into(),
}, },
"BODY[1.2.MIME]<11> {5}\r\nhowdy", "BODY[1.2.MIME]<11> {5}\r\nhowdy",
), ),
@ -1333,7 +1333,7 @@ mod tests {
fields: vec!["Subject".into(), "x-special".into()], fields: vec!["Subject".into(), "x-special".into()],
}], }],
origin_octet: None, origin_octet: None,
contents: "howdy".into(), contents: b"howdy"[..].into(),
}, },
"BODY[HEADER.FIELDS.NOT (SUBJECT X-SPECIAL)] {5}\r\nhowdy", "BODY[HEADER.FIELDS.NOT (SUBJECT X-SPECIAL)] {5}\r\nhowdy",
), ),
@ -1344,7 +1344,7 @@ mod tests {
fields: vec!["From".into(), "List-Archive".into()], fields: vec!["From".into(), "List-Archive".into()],
}], }],
origin_octet: None, origin_octet: None,
contents: "howdy".into(), contents: b"howdy"[..].into(),
}, },
"BODY[HEADER.FIELDS (FROM LIST-ARCHIVE)] {5}\r\nhowdy", "BODY[HEADER.FIELDS (FROM LIST-ARCHIVE)] {5}\r\nhowdy",
), ),
@ -1382,10 +1382,10 @@ mod tests {
super::DataItem::Uid { uid: 983 }, super::DataItem::Uid { uid: 983 },
super::DataItem::Rfc822Size { size: 443 }, super::DataItem::Rfc822Size { size: 443 },
super::DataItem::Rfc822Text { super::DataItem::Rfc822Text {
contents: "hi".into() contents: b"hi"[..].into()
}, },
super::DataItem::Rfc822Header { super::DataItem::Rfc822Header {
contents: "header".into() contents: b"header"[..].into()
}, },
], ],
}], }],

View file

@ -187,11 +187,11 @@ pub fn quoted_string_or_nil(buf: &mut Vec<u8>, text: Option<&str>) {
} }
} }
pub fn literal_string(buf: &mut Vec<u8>, text: &str) { pub fn literal_string(buf: &mut Vec<u8>, text: &[u8]) {
buf.push(b'{'); buf.push(b'{');
buf.extend_from_slice(text.len().to_string().as_bytes()); buf.extend_from_slice(text.len().to_string().as_bytes());
buf.extend_from_slice(b"}\r\n"); buf.extend_from_slice(b"}\r\n");
buf.extend_from_slice(text.as_bytes()); buf.extend_from_slice(text);
} }
pub fn quoted_timestamp(buf: &mut Vec<u8>, timestamp: i64) { pub fn quoted_timestamp(buf: &mut Vec<u8>, timestamp: i64) {

View file

@ -379,7 +379,10 @@ impl SessionData {
} }
Attribute::Preview { .. } => { Attribute::Preview { .. } => {
items.push(DataItem::Preview { items.push(DataItem::Preview {
contents: email.get(&Property::Preview).as_string().map(|p| p.into()), contents: email
.get(&Property::Preview)
.as_string()
.map(|p| p.as_bytes().into()),
}); });
} }
Attribute::Rfc822Size => { Attribute::Rfc822Size => {
@ -392,7 +395,7 @@ impl SessionData {
} }
Attribute::Rfc822 => { Attribute::Rfc822 => {
items.push(DataItem::Rfc822 { items.push(DataItem::Rfc822 {
contents: String::from_utf8_lossy(raw_message.as_ref().unwrap()), contents: raw_message.as_ref().unwrap().into(),
}); });
} }
Attribute::Rfc822Header => { Attribute::Rfc822Header => {
@ -403,7 +406,7 @@ impl SessionData {
.get(message.offset_header..message.offset_body) .get(message.offset_header..message.offset_body)
{ {
items.push(DataItem::Rfc822Header { items.push(DataItem::Rfc822Header {
contents: String::from_utf8_lossy(header), contents: header.into(),
}); });
} }
} }
@ -415,7 +418,7 @@ impl SessionData {
.get(message.offset_body..message.offset_end) .get(message.offset_body..message.offset_end)
{ {
items.push(DataItem::Rfc822Text { items.push(DataItem::Rfc822Text {
contents: String::from_utf8_lossy(text), contents: text.into(),
}); });
} }
} }
@ -593,7 +596,7 @@ pub trait AsImapDataItem<'x> {
&'z self, &'z self,
sections: &[Section], sections: &[Section],
partial: Option<(u32, u32)>, partial: Option<(u32, u32)>,
) -> Option<Cow<'x, str>>; ) -> Option<Cow<'x, [u8]>>;
fn binary( fn binary(
&self, &self,
sections: &[u32], sections: &[u32],
@ -805,14 +808,16 @@ impl<'x> AsImapDataItem<'x> for Message<'x> {
&'z self, &'z self,
sections: &[Section], sections: &[Section],
partial: Option<(u32, u32)>, partial: Option<(u32, u32)>,
) -> Option<Cow<'x, str>> { ) -> Option<Cow<'x, [u8]>> {
let mut part = self.root_part(); let mut part = self.root_part();
if sections.is_empty() { if sections.is_empty() {
return String::from_utf8_lossy(get_partial_bytes( return Some(
self.raw_message.get(part.offset_header..part.offset_end)?, get_partial_bytes(
partial, self.raw_message.get(part.offset_header..part.offset_end)?,
)) partial,
.into(); )
.into(),
);
} }
let mut message = self; let mut message = self;
@ -848,13 +853,15 @@ impl<'x> AsImapDataItem<'x> for Message<'x> {
} }
} }
Section::Header => { Section::Header => {
return String::from_utf8_lossy(get_partial_bytes( return Some(
message get_partial_bytes(
.raw_message message
.get(part.offset_header..part.offset_body)?, .raw_message
partial, .get(part.offset_header..part.offset_body)?,
)) partial,
.into(); )
.into(),
);
} }
Section::HeaderFields { not, fields } => { Section::HeaderFields { not, fields } => {
let mut headers = let mut headers =
@ -876,22 +883,19 @@ impl<'x> AsImapDataItem<'x> for Message<'x> {
headers.extend_from_slice(b"\r\n"); headers.extend_from_slice(b"\r\n");
return Some(if partial.is_none() { return Some(if partial.is_none() {
String::from_utf8(headers).map_or_else( headers.into()
|err| String::from_utf8_lossy(err.as_bytes()).into_owned().into(),
|s| s.into(),
)
} else { } else {
String::from_utf8_lossy(get_partial_bytes(&headers, partial)) get_partial_bytes(&headers, partial).to_vec().into()
.into_owned()
.into()
}); });
} }
Section::Text => { Section::Text => {
return String::from_utf8_lossy(get_partial_bytes( return Some(
message.raw_message.get(part.offset_body..part.offset_end)?, get_partial_bytes(
partial, message.raw_message.get(part.offset_body..part.offset_end)?,
)) partial,
.into(); )
.into(),
);
} }
Section::Mime => { Section::Mime => {
let mut headers = let mut headers =
@ -912,14 +916,9 @@ impl<'x> AsImapDataItem<'x> for Message<'x> {
} }
headers.extend_from_slice(b"\r\n"); headers.extend_from_slice(b"\r\n");
return Some(if partial.is_none() { return Some(if partial.is_none() {
String::from_utf8(headers).map_or_else( headers.into()
|err| String::from_utf8_lossy(err.as_bytes()).into_owned().into(),
|s| s.into(),
)
} else { } else {
String::from_utf8_lossy(get_partial_bytes(&headers, partial)) get_partial_bytes(&headers, partial).to_vec().into()
.into_owned()
.into()
}); });
} }
} }
@ -928,17 +927,13 @@ impl<'x> AsImapDataItem<'x> for Message<'x> {
// BODY[x] should return both headers and body, but most clients // BODY[x] should return both headers and body, but most clients
// expect BODY[x] to return only the body, just like BOXY[x.TEXT] does. // expect BODY[x] to return only the body, just like BOXY[x.TEXT] does.
String::from_utf8_lossy(get_partial_bytes( Some(
message.raw_message.get(part.offset_body..part.offset_end)?, get_partial_bytes(
partial, message.raw_message.get(part.offset_body..part.offset_end)?,
)) partial,
.into() )
.into(),
/*String::from_utf8_lossy(get_partial_bytes( )
raw_message.get(part.offset_header..part.offset_end)?,
partial,
))
.into()*/
} }
fn binary( fn binary(

View file

@ -415,7 +415,7 @@ impl<T: AsyncWrite + AsyncRead + IsTls + Unpin> Session<T> {
let params = self let params = self
.build_script_parameters() .build_script_parameters()
.with_message(edited_message.as_ref().unwrap_or(&raw_message).clone()) .with_message(edited_message.as_ref().unwrap_or(&raw_message).clone())
.set_variable("from", auth_message.from().to_string()) .set_variable("dmarc_from", auth_message.from().to_string())
.set_variable( .set_variable(
"arc_result", "arc_result",
arc_output arc_output
@ -735,7 +735,7 @@ impl<T: AsyncWrite + AsyncRead + IsTls + Unpin> Session<T> {
.iprev .iprev
.as_ref() .as_ref()
.and_then(|ir| ir.ptr.as_ref()) .and_then(|ir| ir.ptr.as_ref())
.and_then(|ptr| ptr.first().map(|s| s.as_str())) .and_then(|ptr| ptr.first().map(|s| s.strip_suffix('.').unwrap_or(s)))
.unwrap_or("unknown") .unwrap_or("unknown")
.as_bytes(), .as_bytes(),
); );
@ -747,12 +747,12 @@ impl<T: AsyncWrite + AsyncRead + IsTls + Unpin> Session<T> {
headers.extend_from_slice(self.instance.hostname.as_bytes()); headers.extend_from_slice(self.instance.hostname.as_bytes());
headers.extend_from_slice(b" (Stalwart SMTP) with "); headers.extend_from_slice(b" (Stalwart SMTP) with ");
headers.extend_from_slice( headers.extend_from_slice(
if self.stream.is_tls() { match (self.stream.is_tls(), self.data.authenticated_as.is_empty()) {
"ESMTPS" (true, true) => b"ESMTPS",
} else { (true, false) => b"ESMTPSA",
"ESMTP" (false, true) => b"ESMTP",
} (false, false) => b"ESMTPA",
.as_bytes(), },
); );
headers.extend_from_slice(b" id "); headers.extend_from_slice(b" id ");
headers.extend_from_slice(format!("{id:X}").as_bytes()); headers.extend_from_slice(format!("{id:X}").as_bytes());

View file

@ -28,6 +28,7 @@ pub fn register_functions() -> FunctionMap {
FunctionMap::new() FunctionMap::new()
.with_function("trim", |v| v.to_cow().trim().to_string().into()) .with_function("trim", |v| v.to_cow().trim().to_string().into())
.with_function("len", |v| v.to_cow().len().into()) .with_function("len", |v| v.to_cow().len().into())
.with_function("is_empty", |v| v.to_cow().as_ref().is_empty().into())
.with_function("to_lowercase", |v| { .with_function("to_lowercase", |v| {
v.to_cow().to_lowercase().to_string().into() v.to_cow().to_lowercase().to_string().into()
}) })
@ -40,12 +41,24 @@ pub fn register_functions() -> FunctionMap {
.unwrap_or("unknown") .unwrap_or("unknown")
.into() .into()
}) })
.with_function("domain", |v| { .with_function("is_email", |v| is_email_valid(v.to_cow().as_ref()).into())
.with_function("domain_part", |v| {
v.to_cow() v.to_cow()
.rsplit_once('@') .rsplit_once('@')
.map_or(Variable::default(), |(_, d)| d.trim().to_string().into()) .map_or(Variable::default(), |(_, d)| d.trim().to_string().into())
}) })
.with_function("base_domain", |v| { .with_function("local_part", |v| {
v.to_cow()
.rsplit_once('@')
.map_or(Variable::default(), |(u, _)| u.trim().to_string().into())
})
.with_function("domain_name_part", |v| {
v.to_cow()
.rsplit_once('@')
.and_then(|(_, d)| d.trim().split('.').rev().nth(1).map(|s| s.to_string()))
.map_or(Variable::default(), Variable::from)
})
.with_function("subdomain_part", |v| {
v.to_cow() v.to_cow()
.rsplit_once('@') .rsplit_once('@')
.map_or(Variable::default(), |(_, d)| { .map_or(Variable::default(), |(_, d)| {
@ -81,7 +94,101 @@ pub fn register_functions() -> FunctionMap {
.all(|c| c.is_lowercase()) .all(|c| c.is_lowercase())
.into() .into()
}) })
.with_function("word_count", |v| { .with_function("count_words", |v| {
v.to_cow().as_ref().split_whitespace().count().into() v.to_cow().as_ref().split_whitespace().count().into()
}) })
.with_function("count_chars", |v| {
v.to_cow().as_ref().chars().count().into()
})
.with_function("count_control_chars", |v| {
v.to_cow()
.as_ref()
.chars()
.filter(|c| {
matches!(c, '\u{0000}'..='\u{0008}'
| '\u{000B}'
| '\u{000C}'
| '\u{000E}'..='\u{001F}'
| '\u{007F}')
})
.count()
.into()
})
.with_function("count", |v| {
if let Variable::Array(l) = v {
l.len().into()
} else {
1.into()
}
})
}
fn is_email_valid(email: &str) -> bool {
let mut last_ch = 0;
let mut in_quote = false;
let mut at_count = 0;
let mut dot_count = 0;
let mut lp_len = 0;
let mut value = 0;
for ch in email.bytes() {
match ch {
b'0'..=b'9'
| b'a'..=b'z'
| b'A'..=b'Z'
| b'!'
| b'#'
| b'$'
| b'%'
| b'&'
| b'\''
| b'*'
| b'+'
| b'-'
| b'/'
| b'='
| b'?'
| b'^'
| b'_'
| b'`'
| b'{'
| b'|'
| b'}'
| b'~'
| 0x7f..=u8::MAX => {
value += 1;
}
b'.' if !in_quote => {
if last_ch != b'.' && last_ch != b'@' && value != 0 {
value += 1;
if at_count == 1 {
dot_count += 1;
}
} else {
return false;
}
}
b'@' if !in_quote => {
at_count += 1;
lp_len = value;
value = 0;
}
b'>' | b':' | b',' | b' ' if in_quote => {
value += 1;
}
b'\"' if !in_quote || last_ch != b'\\' => {
in_quote = !in_quote;
}
b'\\' if in_quote && last_ch != b'\\' => (),
_ => {
if !in_quote {
return false;
}
}
}
last_ch = ch;
}
at_count == 1 && dot_count > 0 && lp_len > 0 && value > 0
} }