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;
enum State {
None,
Flags,
UTF8,
UTF8Data,
}
impl Request<Command> {
pub fn parse_append(self) -> crate::Result<append::Arguments> {
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<Command> {
.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 <foobar@Blurdybloop.example.COM>\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 <mooch@OWaTaGu.example.net>\r\n",

View file

@ -162,8 +162,6 @@ impl<T: CommandParser> Receiver<T> {
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<T: CommandParser> Receiver<T> {
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::<Command>::new();
for invalid in [
"\r\n",
" \r \n",
//"\r\n",
//" \r \n",
"a001\r\n",
"a001 unknown\r\n",
"a001 login {abc}\r\n",

View file

@ -192,7 +192,7 @@ impl SessionData {
.into_iter()
.filter_map(|id| mailbox.id_to_imap.get(&id))
.map(|id| id.uid)
.collect(),
.collect::<Vec<_>>(),
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))

View file

@ -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]

View file

@ -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