diff --git a/crates/imap-proto/src/parser/append.rs b/crates/imap-proto/src/parser/append.rs index f44c2da7..a48f293c 100644 --- a/crates/imap-proto/src/parser/append.rs +++ b/crates/imap-proto/src/parser/append.rs @@ -32,11 +32,19 @@ use crate::{ use super::parse_datetime; +enum State { + None, + Flags, + UTF8, + UTF8Data, +} + impl Request { pub fn parse_append(self) -> crate::Result { match self.tokens.len() { 0 | 1 => Err(self.into_error("Missing arguments.")), _ => { + // Obtain mailbox name let mut tokens = self.tokens.into_iter().peekable(); let mailbox_name = tokens .next() @@ -45,49 +53,101 @@ impl Request { .map_err(|v| (self.tag.as_str(), v))?; let mut messages = Vec::new(); - while let Some(token) = tokens.next() { - let mut flags = Vec::new(); - let token = match token { - Token::ParenthesisOpen => { - #[allow(clippy::while_let_on_iterator)] - while let Some(token) = tokens.next() { - 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) + while tokens.peek().is_some() { + // Parse flags + let mut message = Message { + message: vec![], + flags: vec![], + received_at: None, }; + let mut state = State::None; + let mut seen_flags = false; - messages.push(Message { - message, - flags, - received_at, - }); + while let Some(token) = tokens.next() { + match token { + Token::ParenthesisOpen => { + 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 { @@ -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!( receiver .parse(&mut command.as_bytes().iter()) - .unwrap() + .expect(command) .parse_append() - .unwrap(), + .expect(command), arguments, "{:?}", command @@ -191,7 +275,7 @@ mod tests { // Multiappend 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", "From: Fred Foobar \r\n", "Subject: afternoon meeting\r\n", @@ -200,7 +284,7 @@ mod tests { "MIME-Version: 1.0\r\n", "Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\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", "Date: Mon, 7 Feb 1994 22:43:04 -0800 (PST)\r\n", "From: Joe Mooch \r\n", diff --git a/crates/imap-proto/src/receiver.rs b/crates/imap-proto/src/receiver.rs index 3a07fadc..434c9d57 100644 --- a/crates/imap-proto/src/receiver.rs +++ b/crates/imap-proto/src/receiver.rs @@ -162,8 +162,6 @@ impl Receiver { if !ch.is_ascii_whitespace() { self.buf.push(ch); self.state = State::Tag; - } else if ch == b'\n' { - return Err(self.error_reset("Expected a tag.")); } } State::Tag => match ch { @@ -177,18 +175,20 @@ impl Receiver { 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 { self.buf.push(ch); } else { 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 } => { if ch.is_ascii_alphanumeric() { @@ -1072,8 +1072,8 @@ mod tests { fn receiver_parse_invalid() { let mut receiver = Receiver::::new(); for invalid in [ - "\r\n", - " \r \n", + //"\r\n", + //" \r \n", "a001\r\n", "a001 unknown\r\n", "a001 login {abc}\r\n", diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index 91398096..90c33417 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -192,7 +192,7 @@ impl SessionData { .into_iter() .filter_map(|id| mailbox.id_to_imap.get(&id)) .map(|id| id.uid) - .collect(), + .collect::>(), 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)) diff --git a/resources/config/common.toml b/resources/config/common.toml index 0abe10ae..148aa24b 100644 --- a/resources/config/common.toml +++ b/resources/config/common.toml @@ -16,8 +16,12 @@ implicit = false timeout = "1m" certificate = "default" #sni = [{subject = "", certificate = ""}] -#protocols = ["TLSv1.2", TLSv1.3"] -#ciphers = [] +#protocols = ["TLSv1.2", "TLSv1.3"] +#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 [server.socket] diff --git a/resources/config/smtp.toml b/resources/config/smtp.toml index 9e0faf8e..56980d1b 100644 --- a/resources/config/smtp.toml +++ b/resources/config/smtp.toml @@ -59,6 +59,7 @@ directory = [ { if = "listener", ne = "smtp", then = "__SMTP_DIRECTORY__" }, { else = false } ] require = [ { if = "listener", ne = "smtp", then = true}, { else = false } ] +allow-plain-text = false [session.auth.errors] total = 3