Support for UTF8 APPEND + ignore empty lines in IMAP

This commit is contained in:
mdecimus
2023-09-07 14:29:16 +02:00
parent cb41c91fb4
commit 8bceb49fb7
5 changed files with 150 additions and 59 deletions

View File

@@ -32,11 +32,19 @@ use crate::{
use super::parse_datetime; use super::parse_datetime;
enum State {
None,
Flags,
UTF8,
UTF8Data,
}
impl Request<Command> { impl Request<Command> {
pub fn parse_append(self) -> crate::Result<append::Arguments> { pub fn parse_append(self) -> crate::Result<append::Arguments> {
match self.tokens.len() { match self.tokens.len() {
0 | 1 => Err(self.into_error("Missing arguments.")), 0 | 1 => Err(self.into_error("Missing arguments.")),
_ => { _ => {
// Obtain mailbox name
let mut tokens = self.tokens.into_iter().peekable(); let mut tokens = self.tokens.into_iter().peekable();
let mailbox_name = tokens let mailbox_name = tokens
.next() .next()
@@ -45,49 +53,101 @@ impl Request<Command> {
.map_err(|v| (self.tag.as_str(), v))?; .map_err(|v| (self.tag.as_str(), v))?;
let mut messages = Vec::new(); let mut messages = Vec::new();
while let Some(token) = tokens.next() { while tokens.peek().is_some() {
let mut flags = Vec::new(); // Parse flags
let token = match token { let mut message = Message {
Token::ParenthesisOpen => { message: vec![],
#[allow(clippy::while_let_on_iterator)] flags: vec![],
while let Some(token) = tokens.next() { received_at: None,
match token {
Token::ParenthesisClose => break,
Token::Argument(value) => {
flags.push(
Flag::parse_imap(value)
.map_err(|v| (self.tag.as_str(), v))?,
);
}
_ => return Err((self.tag.as_str(), "Invalid flag.").into()),
}
}
tokens
.next()
.ok_or((self.tag.as_str(), "Missing paramaters after flags."))?
}
token => token,
};
let (message, received_at) = if tokens.peek().is_some() {
let token_bytes = token.unwrap_bytes();
if token_bytes.len() <= 28 {
if let Ok(date_time) = parse_datetime(&token_bytes) {
(tokens.next().unwrap().unwrap_bytes(), Some(date_time))
} else {
(token_bytes, None)
}
} else {
(token_bytes, None)
}
} else {
(token.unwrap_bytes(), None)
}; };
let mut state = State::None;
let mut seen_flags = false;
messages.push(Message { while let Some(token) = tokens.next() {
message, match token {
flags, Token::ParenthesisOpen => {
received_at, state = match state {
}); State::None if !seen_flags => {
seen_flags = true;
State::Flags
}
State::UTF8 => State::UTF8Data,
_ => {
return Err((
self.tag.as_str(),
"Invalid opening parenthesis found.",
)
.into())
}
};
}
Token::ParenthesisClose => match state {
State::None | State::UTF8 => {
return Err((
self.tag.as_str(),
"Invalid closing parenthesis found.",
)
.into())
}
State::Flags => {
state = State::None;
}
State::UTF8Data => {
break;
}
},
Token::Argument(value) => match state {
State::None => {
if value.eq_ignore_ascii_case(b"utf8") {
state = State::UTF8;
} else if matches!(tokens.peek(), Some(Token::Argument(_)))
&& value.len() <= 28
&& !value.contains(&b'\n')
{
if let Ok(date_time) = parse_datetime(&value) {
message.received_at = Some(date_time);
} else {
return Err((
self.tag.as_str(),
"Failed to parse received time.",
)
.into());
}
} else {
message.message = value;
break;
}
}
State::Flags => {
message.flags.push(
Flag::parse_imap(value)
.map_err(|v| (self.tag.as_str(), v))?,
);
}
State::UTF8 => {
return Err((
self.tag.as_str(),
"Expected parenthesis after UTF8.",
)
.into());
}
State::UTF8Data => {
if message.message.is_empty() {
message.message = value;
} else {
return Err((
self.tag.as_str(),
"Invalid parameter after message literal.",
)
.into());
}
}
},
_ => return Err((self.tag.as_str(), "Invalid arguments.").into()),
}
}
messages.push(message);
} }
Ok(append::Arguments { Ok(append::Arguments {
@@ -176,13 +236,37 @@ mod tests {
}], }],
}, },
), ),
(
"42 APPEND \"Drafts\" (\\Draft) UTF8 (~{5+}\r\nhello)\r\n",
append::Arguments {
tag: "42".to_string(),
mailbox_name: "Drafts".to_string(),
messages: vec![Message {
message: vec![b'h', b'e', b'l', b'l', b'o'],
flags: vec![Flag::Draft],
received_at: None,
}],
},
),
(
"42 APPEND \"Drafts\" (\\Draft) \"20-Nov-2022 23:59:59 +0300\" UTF8 (~{5+}\r\nhello)\r\n",
append::Arguments {
tag: "42".to_string(),
mailbox_name: "Drafts".to_string(),
messages: vec![Message {
message: vec![b'h', b'e', b'l', b'l', b'o'],
flags: vec![Flag::Draft],
received_at: Some(1668977999),
}],
},
),
] { ] {
assert_eq!( assert_eq!(
receiver receiver
.parse(&mut command.as_bytes().iter()) .parse(&mut command.as_bytes().iter())
.unwrap() .expect(command)
.parse_append() .parse_append()
.unwrap(), .expect(command),
arguments, arguments,
"{:?}", "{:?}",
command command
@@ -191,7 +275,7 @@ mod tests {
// Multiappend // Multiappend
for line in [ for line in [
"A003 APPEND saved-messages (\\Seen) {329}\r\n", "A003 APPEND saved-messages (\\Seen) UTF8 ({329}\r\n",
"Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)\r\n", "Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)\r\n",
"From: Fred Foobar <foobar@Blurdybloop.example.COM>\r\n", "From: Fred Foobar <foobar@Blurdybloop.example.COM>\r\n",
"Subject: afternoon meeting\r\n", "Subject: afternoon meeting\r\n",
@@ -200,7 +284,7 @@ mod tests {
"MIME-Version: 1.0\r\n", "MIME-Version: 1.0\r\n",
"Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n", "Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n",
"\r\n", "\r\n",
"Hello Joe, do you think we can meet at 3:30 tomorrow?\r\n", "Hello Joe, do you think we can meet at 3:30 tomorrow?\r\n)",
" (\\Seen) \"7-Feb-1994 22:43:04 -0800\" {295}\r\n", " (\\Seen) \"7-Feb-1994 22:43:04 -0800\" {295}\r\n",
"Date: Mon, 7 Feb 1994 22:43:04 -0800 (PST)\r\n", "Date: Mon, 7 Feb 1994 22:43:04 -0800 (PST)\r\n",
"From: Joe Mooch <mooch@OWaTaGu.example.net>\r\n", "From: Joe Mooch <mooch@OWaTaGu.example.net>\r\n",

View File

@@ -162,8 +162,6 @@ impl<T: CommandParser> Receiver<T> {
if !ch.is_ascii_whitespace() { if !ch.is_ascii_whitespace() {
self.buf.push(ch); self.buf.push(ch);
self.state = State::Tag; self.state = State::Tag;
} else if ch == b'\n' {
return Err(self.error_reset("Expected a tag."));
} }
} }
State::Tag => match ch { State::Tag => match ch {
@@ -177,18 +175,20 @@ impl<T: CommandParser> Receiver<T> {
self.state = State::Command { is_uid: false }; self.state = State::Command { is_uid: false };
} }
} }
_ if !ch.is_ascii_whitespace() => { b'\t' | b'\r' => {}
b'\n' => {
return Err(self.error_reset(format!(
"Missing command after tag {:?}, found CRLF instead.",
std::str::from_utf8(&self.buf).unwrap_or_default()
)));
}
_ => {
if self.buf.len() < 128 { if self.buf.len() < 128 {
self.buf.push(ch); self.buf.push(ch);
} else { } else {
return Err(self.error_reset("Tag too long.")); return Err(self.error_reset("Tag too long."));
} }
} }
_ => {
return Err(
self.error_reset(format!("Invalid character {:?} in tag.", ch as char))
);
}
}, },
State::Command { is_uid } => { State::Command { is_uid } => {
if ch.is_ascii_alphanumeric() { if ch.is_ascii_alphanumeric() {
@@ -1072,8 +1072,8 @@ mod tests {
fn receiver_parse_invalid() { fn receiver_parse_invalid() {
let mut receiver = Receiver::<Command>::new(); let mut receiver = Receiver::<Command>::new();
for invalid in [ for invalid in [
"\r\n", //"\r\n",
" \r \n", //" \r \n",
"a001\r\n", "a001\r\n",
"a001 unknown\r\n", "a001 unknown\r\n",
"a001 login {abc}\r\n", "a001 login {abc}\r\n",

View File

@@ -192,7 +192,7 @@ impl SessionData {
.into_iter() .into_iter()
.filter_map(|id| mailbox.id_to_imap.get(&id)) .filter_map(|id| mailbox.id_to_imap.get(&id))
.map(|id| id.uid) .map(|id| id.uid)
.collect(), .collect::<Vec<_>>(),
mailbox.uid_validity, mailbox.uid_validity,
) )
} }
@@ -212,7 +212,9 @@ impl SessionData {
) )
} }
}; };
response = response.with_code(ResponseCode::AppendUid { uid_validity, uids }); if !uids.is_empty() {
response = response.with_code(ResponseCode::AppendUid { uid_validity, uids });
}
} }
Ok(response.with_tag(arguments.tag)) Ok(response.with_tag(arguments.tag))

View File

@@ -16,8 +16,12 @@ implicit = false
timeout = "1m" timeout = "1m"
certificate = "default" certificate = "default"
#sni = [{subject = "", certificate = ""}] #sni = [{subject = "", certificate = ""}]
#protocols = ["TLSv1.2", TLSv1.3"] #protocols = ["TLSv1.2", "TLSv1.3"]
#ciphers = [] #ciphers = [ "TLS13_AES_256_GCM_SHA384", "TLS13_AES_128_GCM_SHA256",
# "TLS13_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
# "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
# "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
# "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"]
ignore-client-order = true ignore-client-order = true
[server.socket] [server.socket]

View File

@@ -59,6 +59,7 @@ directory = [ { if = "listener", ne = "smtp", then = "__SMTP_DIRECTORY__" },
{ else = false } ] { else = false } ]
require = [ { if = "listener", ne = "smtp", then = true}, require = [ { if = "listener", ne = "smtp", then = true},
{ else = false } ] { else = false } ]
allow-plain-text = false
[session.auth.errors] [session.auth.errors]
total = 3 total = 3