Support for UTF8 APPEND + ignore empty lines in IMAP
This commit is contained in:
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user