mirror of
https://github.com/stalwartlabs/mail-server.git
synced 2025-09-09 21:44:24 +08:00
Support for UTF8 APPEND + ignore empty lines in IMAP
This commit is contained in:
parent
cb41c91fb4
commit
8bceb49fb7
5 changed files with 150 additions and 59 deletions
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue