Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+222
View File
@@ -0,0 +1,222 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::acl::{self, ModRights, ModRightsOp, Rights},
receiver::{Request, bad},
utf7::utf7_maybe_decode,
};
use super::PushUnique;
/*
setacl = "SETACL" SP mailbox SP identifier
SP mod-rights
deleteacl = "DELETEACL" SP mailbox SP identifier
getacl = "GETACL" SP mailbox
listrights = "LISTRIGHTS" SP mailbox SP identifier
myrights = "MYRIGHTS" SP mailbox
*/
impl Request<Command> {
pub fn parse_acl(self, is_utf8: bool) -> trc::Result<acl::Arguments> {
let (has_identifier, has_mod_rights) = match self.command {
Command::SetAcl => (true, true),
Command::DeleteAcl | Command::ListRights => (true, false),
Command::GetAcl | Command::MyRights => (false, false),
_ => unreachable!(),
};
let mut tokens = self.tokens.into_iter();
let mailbox_name = utf7_maybe_decode(
tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing mailbox name."))?
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
);
let identifier = if has_identifier {
tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing identifier."))?
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?
.into()
} else {
None
};
let mod_rights = if has_mod_rights {
ModRights::parse(
&tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing rights."))?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?
.into()
} else {
None
};
Ok(acl::Arguments {
tag: self.tag,
mailbox_name,
identifier,
mod_rights,
})
}
}
impl ModRights {
pub fn parse(value: &[u8]) -> super::Result<Self> {
let mut op = ModRightsOp::Replace;
let mut rights = Vec::with_capacity(value.len());
for (pos, ch) in value.iter().enumerate() {
rights.push_unique(match ch {
b'l' => Rights::Lookup,
b'r' => Rights::Read,
b's' => Rights::Seen,
b'w' => Rights::Write,
b'i' => Rights::Insert,
b'p' => Rights::Post,
b'k' => Rights::CreateMailbox,
b'x' => Rights::DeleteMailbox,
b't' => Rights::DeleteMessages,
b'e' => Rights::Expunge,
b'a' => Rights::Administer,
// RFC2086
b'd' => Rights::DeleteMessages,
b'c' => Rights::CreateMailbox,
b'+' if pos == 0 => {
op = ModRightsOp::Add;
continue;
}
b'-' if pos == 0 => {
op = ModRightsOp::Remove;
continue;
}
_ => {
return Err(
format!("Invalid character {:?} in rights.", char::from(*ch)).into(),
);
}
})
}
if !rights.is_empty() {
Ok(ModRights { op, rights })
} else {
Err("At least one right has to be specified.".into())
}
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::acl::{self, ModRights, ModRightsOp, Rights},
receiver::Receiver,
};
#[test]
fn parse_acl() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A003 Setacl INBOX/Drafts Byron lrswikda\r\n",
acl::Arguments {
tag: "A003".into(),
mailbox_name: "INBOX/Drafts".into(),
identifier: Some("Byron".into()),
mod_rights: ModRights {
op: ModRightsOp::Replace,
rights: vec![
Rights::Lookup,
Rights::Read,
Rights::Seen,
Rights::Write,
Rights::Insert,
Rights::CreateMailbox,
Rights::DeleteMessages,
Rights::Administer,
],
}
.into(),
},
),
(
"A002 SETACL INBOX/Drafts Chris +cda\r\n",
acl::Arguments {
tag: "A002".into(),
mailbox_name: "INBOX/Drafts".into(),
identifier: Some("Chris".into()),
mod_rights: ModRights {
op: ModRightsOp::Add,
rights: vec![
Rights::CreateMailbox,
Rights::DeleteMessages,
Rights::Administer,
],
}
.into(),
},
),
(
"A036 SETACL INBOX/Drafts John -lrswicda\r\n",
acl::Arguments {
tag: "A036".into(),
mailbox_name: "INBOX/Drafts".into(),
identifier: Some("John".into()),
mod_rights: ModRights {
op: ModRightsOp::Remove,
rights: vec![
Rights::Lookup,
Rights::Read,
Rights::Seen,
Rights::Write,
Rights::Insert,
Rights::CreateMailbox,
Rights::DeleteMessages,
Rights::Administer,
],
}
.into(),
},
),
(
"A001 GETACL INBOX/Drafts\r\n",
acl::Arguments {
tag: "A001".into(),
mailbox_name: "INBOX/Drafts".into(),
identifier: None,
mod_rights: None,
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_acl(false)
.unwrap(),
arguments,
"{:?}",
command
);
}
}
}
+351
View File
@@ -0,0 +1,351 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::{
Flag,
append::{self, Message},
},
receiver::{Request, Token, bad},
utf7::utf7_maybe_decode,
};
use super::parse_datetime;
enum State {
None,
Flags,
UTF8,
UTF8Data,
}
impl Request<Command> {
pub fn parse_append(self, is_utf8: bool) -> trc::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 = utf7_maybe_decode(
tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
);
let mut messages = Vec::new();
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;
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(bad(
self.tag.to_compact_string(),
"Invalid opening parenthesis found.",
));
}
};
}
Token::ParenthesisClose => match state {
State::None | State::UTF8 => {
return Err(bad(
self.tag.to_compact_string(),
"Invalid closing parenthesis found.",
));
}
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(bad(
self.tag.to_compact_string(),
"Failed to parse received time.",
));
}
} else {
message.message = value;
break;
}
}
State::Flags => {
message.flags.push(
Flag::parse_imap(value)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
);
}
State::UTF8 => {
return Err(bad(
self.tag.to_compact_string(),
"Expected parenthesis after UTF8.",
));
}
State::UTF8Data => {
if message.message.is_empty() {
message.message = value;
} else {
return Err(bad(
self.tag.to_compact_string(),
"Invalid parameter after message literal.",
));
}
}
},
_ => {
return Err(bad(
self.tag.to_compact_string(),
"Invalid arguments.",
));
}
}
}
messages.push(message);
}
Ok(append::Arguments {
tag: self.tag,
mailbox_name,
messages,
})
}
}
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{
Flag,
append::{self, Message},
},
receiver::{Error, Receiver},
};
#[test]
fn parse_append() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A003 APPEND saved-messages (\\Seen) {1+}\r\na\r\n",
append::Arguments {
tag: "A003".into(),
mailbox_name: "saved-messages".into(),
messages: vec![Message {
message: vec![b'a'],
flags: vec![Flag::Seen],
received_at: None,
}],
},
),
(
"A003 APPEND \"hello world\" (\\Seen \\Draft $MDNSent) {1+}\r\na\r\n",
append::Arguments {
tag: "A003".into(),
mailbox_name: "hello world".into(),
messages: vec![Message {
message: vec![b'a'],
flags: vec![Flag::Seen, Flag::Draft, Flag::MDNSent],
received_at: None,
}],
},
),
(
"A003 APPEND \"hi\" ($Junk) \"7-Feb-1994 22:43:04 -0800\" {1+}\r\na\r\n",
append::Arguments {
tag: "A003".into(),
mailbox_name: "hi".into(),
messages: vec![Message {
message: vec![b'a'],
flags: vec![Flag::Junk],
received_at: Some(760689784),
}],
},
),
(
"A003 APPEND \"hi\" \"20-Nov-2022 23:59:59 +0300\" {1+}\r\na\r\n",
append::Arguments {
tag: "A003".into(),
mailbox_name: "hi".into(),
messages: vec![Message {
message: vec![b'a'],
flags: vec![],
received_at: Some(1668977999),
}],
},
),
(
"A003 APPEND \"hi\" \"20-Nov-2022 23:59:59 +0300\" ~{1+}\r\na\r\n",
append::Arguments {
tag: "A003".into(),
mailbox_name: "hi".into(),
messages: vec![Message {
message: vec![b'a'],
flags: vec![],
received_at: Some(1668977999),
}],
},
),
(
"42 APPEND \"Drafts\" (\\Draft) UTF8 (~{5+}\r\nhello)\r\n",
append::Arguments {
tag: "42".into(),
mailbox_name: "Drafts".into(),
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".into(),
mailbox_name: "Drafts".into(),
messages: vec![Message {
message: vec![b'h', b'e', b'l', b'l', b'o'],
flags: vec![Flag::Draft],
received_at: Some(1668977999),
}],
},
),
(
"A003 APPEND \"&A8g- \\\"&A9QD1APUA9gD3APcA-+\\\"\" (\\Seen) \"7-Feb-1994 22:43:04 -0800\" {1+}\r\na\r\n",
append::Arguments {
tag: "A003".into(),
mailbox_name: "ψ \"ϔϔϔϘϜϜ+\"".into(),
messages: vec![Message {
message: vec![b'a'],
flags: vec![Flag::Seen],
received_at: Some(760689784),
}],
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.expect(command)
.parse_append(false)
.expect(command),
arguments,
"{:?}",
command
);
}
// Multiappend
for line in [
"A003 APPEND saved-messages (\\Seen) UTF8 ({329}\r\n",
"Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)\r\n",
"From: Fred Foobar <[email protected]>\r\n",
"Subject: afternoon meeting\r\n",
"To: [email protected]\r\n",
"Message-Id: <[email protected]>\r\n",
"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)",
" (\\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 <[email protected]>\r\n",
"Subject: Re: afternoon meeting\r\n",
"To: [email protected]\r\n",
"Message-Id: <[email protected]>\r\n",
"MIME-Version: 1.0\r\n",
"Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n\r\n",
"3:30 is fine with me.\r\n\r\n",
] {
match receiver.parse(&mut line.as_bytes().iter()) {
Ok(request) => {
assert_eq!(
request.parse_append(false).unwrap(),
append::Arguments {
tag: "A003".into(),
mailbox_name: "saved-messages".into(),
messages: vec![
Message {
message: concat!(
"Date: Mon, 7 Feb 1994 21:52:25 -0800 (PST)\r\n",
"From: Fred Foobar <[email protected]>\r\n",
"Subject: afternoon meeting\r\n",
"To: [email protected]\r\n",
"Message-Id: <[email protected]>\r\n",
"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",
)
.as_bytes()
.to_vec(),
flags: vec![Flag::Seen],
received_at: None,
},
Message {
message: concat!(
"Date: Mon, 7 Feb 1994 22:43:04 -0800 (PST)\r\n",
"From: Joe Mooch <[email protected]>\r\n",
"Subject: Re: afternoon meeting\r\n",
"To: [email protected]\r\n",
"Message-Id: <[email protected]>\r\n",
"MIME-Version: 1.0\r\n",
"Content-Type: TEXT/PLAIN; CHARSET=US-ASCII\r\n\r\n",
"3:30 is fine with me.\r\n",
)
.as_bytes()
.to_vec(),
flags: vec![Flag::Seen],
received_at: Some(760689784),
}
],
},
);
}
Err(err) => match err {
Error::NeedsMoreData | Error::NeedsLiteral { .. } => (),
Error::Error { response } => panic!("{:?}", response),
},
}
}
}
}
@@ -0,0 +1,98 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::authenticate::{self, Mechanism},
receiver::{Request, bad},
};
impl Request<Command> {
pub fn parse_authenticate(self) -> trc::Result<authenticate::Arguments> {
if !self.tokens.is_empty() {
let mut tokens = self.tokens.into_iter();
Ok(authenticate::Arguments {
mechanism: Mechanism::parse(&tokens.next().unwrap().unwrap_bytes())
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
params: tokens
.filter_map(|token| token.unwrap_string().ok())
.collect(),
tag: self.tag,
})
} else {
Err(self.into_error("Authentication mechanism missing."))
}
}
}
impl Mechanism {
pub fn parse(value: &[u8]) -> super::Result<Self> {
hashify::tiny_map_ignore_case!(value,
"PLAIN" => Self::Plain,
"CRAM-MD5" => Self::CramMd5,
"DIGEST-MD5" => Self::DigestMd5,
"SCRAM-SHA-1" => Self::ScramSha1,
"SCRAM-SHA-256" => Self::ScramSha256,
"APOP" => Self::Apop,
"NTLM" => Self::Ntlm,
"GSSAPI" => Self::Gssapi,
"ANONYMOUS" => Self::Anonymous,
"EXTERNAL" => Self::External,
"OAUTHBEARER" => Self::OAuthBearer,
"XOAUTH2" => Self::XOauth2,
)
.ok_or_else(|| {
format!(
"Unsupported mechanism '{}'.",
String::from_utf8_lossy(value)
)
.into()
})
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::authenticate::{self, Mechanism},
receiver::Receiver,
};
#[test]
fn parse_authenticate() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"a002 AUTHENTICATE \"EXTERNAL\" {16+}\r\n[email protected]\r\n",
authenticate::Arguments {
tag: "a002".into(),
mechanism: Mechanism::External,
params: vec!["[email protected]".into()],
},
),
(
"A01 AUTHENTICATE PLAIN\r\n",
authenticate::Arguments {
tag: "A01".into(),
mechanism: Mechanism::Plain,
params: vec![],
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_authenticate()
.unwrap(),
arguments
);
}
}
}
+89
View File
@@ -0,0 +1,89 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::copy_move,
receiver::{Request, bad},
utf7::utf7_maybe_decode,
};
use super::parse_sequence_set;
impl Request<Command> {
pub fn parse_copy_move(self, is_utf8: bool) -> trc::Result<copy_move::Arguments> {
if self.tokens.len() > 1 {
let mut tokens = self.tokens.into_iter();
Ok(copy_move::Arguments {
sequence_set: parse_sequence_set(
&tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing sequence set."))?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
mailbox_name: utf7_maybe_decode(
tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing mailbox name."))?
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
),
tag: self.tag,
})
} else {
Err(self.into_error("Missing arguments."))
}
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{Sequence, copy_move},
receiver::Receiver,
};
#[test]
fn parse_copy() {
let mut receiver = Receiver::new();
assert_eq!(
receiver
.parse(&mut "A003 COPY 2:4 MEETING\r\n".as_bytes().iter())
.unwrap()
.parse_copy_move(false)
.unwrap(),
copy_move::Arguments {
sequence_set: Sequence::Range {
start: 2.into(),
end: 4.into(),
},
mailbox_name: "MEETING".into(),
tag: "A003".into(),
}
);
assert_eq!(
receiver
.parse(&mut "A003 COPY 2:4 \"You &- Me\"\r\n".as_bytes().iter())
.unwrap()
.parse_copy_move(false)
.unwrap(),
copy_move::Arguments {
sequence_set: Sequence::Range {
start: 2.into(),
end: 4.into(),
},
mailbox_name: "You & Me".into(),
tag: "A003".into(),
}
);
}
}
+159
View File
@@ -0,0 +1,159 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::{CompactString, ToCompactString, format_compact};
use crate::{
Command,
protocol::{create, list::Attribute},
receiver::{Request, Token, bad},
utf7::utf7_maybe_decode,
};
impl Request<Command> {
pub fn parse_create(self, is_utf8: bool) -> trc::Result<create::Arguments> {
if !self.tokens.is_empty() {
let mut tokens = self.tokens.into_iter();
let mailbox_name = utf7_maybe_decode(
tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
);
let mailbox_role = if let Some(Token::ParenthesisOpen) = tokens.next() {
match tokens.next() {
Some(Token::Argument(param)) if param.eq_ignore_ascii_case(b"USE") => (),
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Failed to parse, expected 'USE'.",
));
}
}
if tokens
.next()
.is_none_or(|token| !token.is_parenthesis_open())
{
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Expected '(' after 'USE'.",
));
}
match tokens.next() {
Some(Token::Argument(value)) => {
let r = hashify::tiny_map_ignore_case!(value.as_slice(),
"\\Archive" => Some(Attribute::Archive),
"\\Drafts" => Some(Attribute::Drafts),
"\\Junk" => Some(Attribute::Junk),
"\\Sent" => Some(Attribute::Sent),
"\\Trash" => Some(Attribute::Trash),
"\\Important" => Some(Attribute::Important),
"\\Memos" => Some(Attribute::Memos),
"\\Scheduled" => Some(Attribute::Scheduled),
"\\Snoozed" => Some(Attribute::Snoozed),
"\\All" => None,
);
match r {
Some(Some(tag)) => Some(tag),
Some(None) => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
"A mailbox with the \"\\All\" attribute already exists.",
));
}
None => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
format_compact!(
"Special use attribute {:?} is not supported.",
String::from_utf8_lossy(&value)
),
));
}
}
}
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Invalid SPECIAL-USE attribute.",
));
}
}
} else {
None
};
Ok(create::Arguments {
mailbox_name,
mailbox_role,
tag: self.tag,
})
} else {
Err(self.into_error("Missing arguments."))
}
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{create, list::Attribute},
receiver::Receiver,
};
#[test]
fn parse_create() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A142 CREATE 12345\r\n",
create::Arguments {
tag: "A142".into(),
mailbox_name: "12345".into(),
mailbox_role: None,
},
),
(
"A142 CREATE \"my funky mailbox\"\r\n",
create::Arguments {
tag: "A142".into(),
mailbox_name: "my funky mailbox".into(),
mailbox_role: None,
},
),
(
"t1 CREATE \"Important Messages\" (USE (\\Important))\r\n",
create::Arguments {
tag: "t1".into(),
mailbox_name: "Important Messages".into(),
mailbox_role: Some(Attribute::Important),
},
),
(
"A142 CREATE \"Test-ąęć-Test\"\r\n",
create::Arguments {
tag: "A142".into(),
mailbox_name: "Test-ąęć-Test".into(),
mailbox_role: None,
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_create(true)
.unwrap(),
arguments
);
}
}
}
+71
View File
@@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::delete,
receiver::{Request, bad},
utf7::utf7_maybe_decode,
};
impl Request<Command> {
pub fn parse_delete(self, is_utf8: bool) -> trc::Result<delete::Arguments> {
match self.tokens.len() {
1 => Ok(delete::Arguments {
mailbox_name: utf7_maybe_decode(
self.tokens
.into_iter()
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
),
tag: self.tag,
}),
0 => Err(self.into_error("Missing mailbox name.")),
_ => Err(self.into_error("Too many arguments.")),
}
}
}
#[cfg(test)]
mod tests {
use crate::{protocol::delete, receiver::Receiver};
#[test]
fn parse_delete() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A142 DELETE INBOX\r\n",
delete::Arguments {
mailbox_name: "INBOX".into(),
tag: "A142".into(),
},
),
(
"A142 DELETE \"my funky mailbox\"\r\n",
delete::Arguments {
mailbox_name: "my funky mailbox".into(),
tag: "A142".into(),
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_delete(true)
.unwrap(),
arguments
);
}
}
}
+106
View File
@@ -0,0 +1,106 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Command,
protocol::{capability::Capability, enable},
receiver::{Request, bad},
};
use compact_str::ToCompactString;
impl Request<Command> {
pub fn parse_enable(self) -> trc::Result<enable::Arguments> {
let len = self.tokens.len();
if len > 0 {
let mut capabilities = Vec::with_capacity(len);
for capability in self.tokens {
capabilities.push(
Capability::parse(&capability.unwrap_bytes())
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
);
}
Ok(enable::Arguments {
tag: self.tag,
capabilities,
})
} else {
Err(self.into_error("Missing arguments."))
}
}
}
impl Capability {
pub fn parse(value: &[u8]) -> super::Result<Self> {
hashify::tiny_map_ignore_case!(value,
"IMAP4rev2" => Self::IMAP4rev2,
"STARTTLS" => Self::StartTLS,
"LOGINDISABLED" => Self::LoginDisabled,
"CONDSTORE" => Self::CondStore,
"QRESYNC" => Self::QResync,
"UTF8=ACCEPT" => Self::Utf8Accept,
"OBJECTID+" => Self::ObjectIdPlus,
"UIDONLY" => Self::UidOnly,
)
.ok_or_else(|| {
format!(
"Unsupported capability '{}'.",
String::from_utf8_lossy(value)
)
.into()
})
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{capability::Capability, enable},
receiver::Receiver,
};
#[test]
fn parse_enable() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"t2 ENABLE IMAP4rev2 CONDSTORE\r\n",
enable::Arguments {
tag: "t2".into(),
capabilities: vec![Capability::IMAP4rev2, Capability::CondStore],
},
),
(
"t3 ENABLE OBJECTID+\r\n",
enable::Arguments {
tag: "t3".into(),
capabilities: vec![Capability::ObjectIdPlus],
},
),
(
"t4 ENABLE CONDSTORE OBJECTID+ UTF8=ACCEPT\r\n",
enable::Arguments {
tag: "t4".into(),
capabilities: vec![
Capability::CondStore,
Capability::ObjectIdPlus,
Capability::Utf8Accept,
],
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_enable()
.unwrap(),
arguments,
"Failed to parse {command}"
);
}
}
}
+828
View File
@@ -0,0 +1,828 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{PushUnique, parse_number, parse_sequence_set};
use crate::{
Command,
protocol::fetch::{self, Attribute, Section},
receiver::{Request, Token, bad},
};
use compact_str::{CompactString, ToCompactString, format_compact};
use std::borrow::Cow;
use std::iter::Peekable;
use std::vec::IntoIter;
impl Request<Command> {
#[allow(clippy::while_let_on_iterator)]
pub fn parse_fetch(self) -> trc::Result<fetch::Arguments> {
if self.tokens.len() < 2 {
return Err(self.into_error("Missing parameters."));
}
let mut tokens = self.tokens.into_iter().peekable();
let mut attributes = Vec::new();
let sequence_set = parse_sequence_set(
&tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing sequence set."))?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
let mut in_parentheses = false;
while let Some(token) = tokens.next() {
match token {
Token::Argument(value) => {
hashify::fnc_map_ignore_case!(value.as_slice(),
"ALL" => {
attributes = vec![
Attribute::Flags,
Attribute::InternalDate,
Attribute::Rfc822Size,
Attribute::Envelope,
];
break;
},
"FULL" => {
attributes = vec![
Attribute::Flags,
Attribute::InternalDate,
Attribute::Rfc822Size,
Attribute::Envelope,
Attribute::Body,
];
break;
},
"FAST" => {
attributes = vec![
Attribute::Flags,
Attribute::InternalDate,
Attribute::Rfc822Size,
];
break;
},
"ENVELOPE" => {
attributes.push_unique(Attribute::Envelope);
},
"FLAGS" => {
attributes.push_unique(Attribute::Flags);
},
"INTERNALDATE" => {
attributes.push_unique(Attribute::InternalDate);
},
"BODYSTRUCTURE" => {
attributes.push_unique(Attribute::BodyStructure);
},
"UID" => {
attributes.push_unique(Attribute::Uid);
},
"RFC822" => {
attributes.push_unique(
if tokens.peek().is_some_and(|token| token.is_dot()) {
tokens.next();
let rfc822 = tokens
.next()
.ok_or_else(|| {
bad(self.tag.to_compact_string(), "Missing RFC822 parameter.")
})?
.unwrap_bytes();
if rfc822.eq_ignore_ascii_case(b"HEADER") {
Attribute::Rfc822Header
} else if rfc822.eq_ignore_ascii_case(b"SIZE") {
Attribute::Rfc822Size
} else if rfc822.eq_ignore_ascii_case(b"TEXT") {
Attribute::Rfc822Text
} else {
return Err(bad(
CompactString::from_string_buffer(self.tag),
format_compact!(
"Invalid RFC822 parameter {:?}.",
String::from_utf8_lossy(&rfc822)
),
));
}
} else {
Attribute::Rfc822
},
);
},
"BODY" => {
let is_peek = match tokens.peek() {
Some(Token::BracketOpen) => {
tokens.next();
false
}
Some(Token::Dot) => {
tokens.next();
if tokens
.next()
.is_none_or( |token| !token.eq_ignore_ascii_case(b"PEEK"))
{
return Err(bad(
self.tag.to_compact_string(),
"Expected 'PEEK' after '.'.",
));
}
if tokens.next().is_none_or( |token| !token.is_bracket_open()) {
return Err(bad(
self.tag.to_compact_string(),
"Expected '[' after 'BODY.PEEK'",
));
}
true
}
_ => {
attributes.push_unique(Attribute::Body);
if !in_parentheses {
break;
} else {
continue;
}
}
};
// Parse section-spect
let mut sections = Vec::new();
while let Some(token) = tokens.next() {
match token {
Token::BracketClose => break,
Token::Argument(value) => {
let section = if value.eq_ignore_ascii_case(b"HEADER") {
if let Some(Token::Dot) = tokens.peek() {
tokens.next();
if tokens.next().is_none_or( |token| {
!token.eq_ignore_ascii_case(b"FIELDS")
}) {
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Expected 'FIELDS' after 'HEADER.'.",
));
}
let is_not = if let Some(Token::Dot) = tokens.peek() {
tokens.next();
if tokens.next().is_none_or( |token| {
!token.eq_ignore_ascii_case(b"NOT")
}) {
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Expected 'NOT' after 'HEADER.FIELDS.'.",
));
}
true
} else {
false
};
if tokens
.next()
.is_none_or( |token| !token.is_parenthesis_open())
{
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Expected '(' after 'HEADER.FIELDS'.",
));
}
let mut fields = Vec::new();
while let Some(token) = tokens.next() {
match token {
Token::ParenthesisClose => break,
Token::Argument(value) => {
fields.push(String::from_utf8(value).map_err(
|_| bad(self.tag.to_compact_string(),"Invalid UTF-8 in header field name."),
)?);
}
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Expected field name.",
))
}
}
}
Section::HeaderFields {
not: is_not,
fields,
}
} else {
Section::Header
}
} else if value.eq_ignore_ascii_case(b"TEXT") {
Section::Text
} else if value.eq_ignore_ascii_case(b"MIME") {
Section::Mime
} else {
Section::Part {
num: parse_number::<u32>(&value)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
}
};
sections.push(section);
}
Token::Dot => (),
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
format_compact!(
"Invalid token {:?} found in section-spect.",
token
),
))
}
}
}
attributes.push_unique(Attribute::BodySection {
peek: is_peek,
sections,
partial: parse_partial(&mut tokens)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
});
},
"BINARY" => {
let (is_peek, is_size) = if let Some(Token::Dot) = tokens.peek() {
tokens.next();
let param = tokens
.next()
.ok_or({
bad(self.tag.to_compact_string(),"Missing parameter after 'BINARY.'.")
})?
.unwrap_bytes();
if param.eq_ignore_ascii_case(b"PEEK") {
(true, false)
} else if param.eq_ignore_ascii_case(b"SIZE") {
(false, true)
} else {
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Expected 'PEEK' or 'SIZE' after 'BINARY.'.",
));
}
} else {
(false, false)
};
// Parse section-part
if tokens.next().is_none_or( |token| !token.is_bracket_open()) {
return Err(bad(self.tag.to_compact_string(), "Expected '[' after 'BINARY'."));
}
let mut sections = Vec::new();
while let Some(token) = tokens.next() {
match token {
Token::Argument(value) => {
sections.push(
parse_number::<u32>(&value)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
);
}
Token::Dot => (),
Token::BracketClose => break,
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
format_compact!(
"Expected part section integer, got {:?}.",
token.to_string()
),
))
}
}
}
attributes.push_unique(if !is_size {
Attribute::Binary {
peek: is_peek,
sections,
partial: parse_partial(&mut tokens)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
}
} else {
Attribute::BinarySize { sections }
});
},
"PREVIEW" => {
attributes.push_unique(Attribute::Preview {
lazy: if let Some(Token::ParenthesisOpen) = tokens.peek() {
tokens.next();
let mut is_lazy = false;
while let Some(token) = tokens.next() {
match token {
Token::ParenthesisClose => break,
Token::Argument(value) if value.eq_ignore_ascii_case(b"LAZY") => {
is_lazy = true;
}
_ => (),
}
}
is_lazy
} else {
false
},
});
},
"MODSEQ" => {
attributes.push_unique(Attribute::ModSeq);
},
"OBJECTID" => {
attributes.push_unique(Attribute::ObjectId);
},
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
format_compact!("Invalid attribute {:?}", String::from_utf8_lossy(&value)),
));
}
);
if !in_parentheses {
break;
}
}
Token::ParenthesisOpen => {
if !in_parentheses {
in_parentheses = true;
} else {
return Err(bad(
self.tag.to_compact_string(),
"Unexpected parenthesis open.",
));
}
}
Token::ParenthesisClose => {
if in_parentheses {
break;
} else {
return Err(bad(
self.tag.to_compact_string(),
"Unexpected parenthesis close.",
));
}
}
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
format_compact!("Invalid fetch argument {:?}.", token.to_string()),
));
}
}
}
// CONDSTORE parameters
let mut changed_since = None;
let mut include_vanished = false;
if let Some(Token::ParenthesisOpen) = tokens.peek() {
tokens.next();
while let Some(token) = tokens.next() {
match token {
Token::Argument(param) if param.eq_ignore_ascii_case(b"CHANGEDSINCE") => {
changed_since = parse_number::<u64>(
&tokens
.next()
.ok_or_else(|| {
bad(
self.tag.to_compact_string(),
"Missing CHANGEDSINCE parameter.",
)
})?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?
.into();
}
Token::Argument(param) if param.eq_ignore_ascii_case(b"VANISHED") => {
include_vanished = true;
}
Token::ParenthesisClose => {
break;
}
_ => {
return Err(bad(
self.tag.to_compact_string(),
format_compact!("Unsupported parameter '{}'.", token),
));
}
}
}
}
if !attributes.is_empty() {
Ok(fetch::Arguments {
tag: self.tag,
sequence_set,
attributes,
changed_since,
include_vanished,
})
} else {
Err(bad(
CompactString::from_string_buffer(self.tag),
"No data items to fetch specified.",
))
}
}
}
pub fn parse_partial(tokens: &mut Peekable<IntoIter<Token>>) -> super::Result<Option<(u32, u32)>> {
if tokens.peek().is_none_or(|token| !token.is_lt()) {
return Ok(None);
}
tokens.next();
let start = parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| Cow::from("Missing partial start."))?
.unwrap_bytes(),
)?;
if tokens.next().is_none_or(|token| !token.is_dot()) {
return Err("Expected '.' after partial start.".into());
}
let end = parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| Cow::from("Missing partial end."))?
.unwrap_bytes(),
)?;
if end == 0 {
return Err("Invalid partial range.".into());
}
if tokens.next().is_none_or(|token| !token.is_gt()) {
return Err("Expected '>' after range.".into());
}
Ok(Some((start, end)))
}
/*
fetch = "FETCH" SP sequence-set SP (
"ALL" / "FULL" / "FAST" /
fetch-att / "(" fetch-att *(SP fetch-att) ")")
fetch-att = "ENVELOPE" / "FLAGS" / "INTERNALDATE" /
"RFC822" [".HEADER" / ".SIZE" / ".TEXT"] /
"BODY" ["STRUCTURE"] / "UID" /
"BODY" section [partial] /
"BODY.PEEK" section [partial] /
"BINARY" [".PEEK"] section-binary [partial] /
"BINARY.SIZE" section-binary
partial = "<" number64 "." nz-number64 ">"
; Partial FETCH request. 0-based offset of
; the first octet, followed by the number of
; octets in the fragment.
section = "[" [section-spec] "]"
section-binary = "[" [section-part] "]"
section-msgtext = "HEADER" /
"HEADER.FIELDS" [".NOT"] SP header-list /
"TEXT"
; top-level or MESSAGE/RFC822 or
; MESSAGE/GLOBAL part
section-part = nz-number *("." nz-number)
; body part reference.
; Allows for accessing nested body parts.
section-spec = section-msgtext / (section-part ["." section-text])
section-text = section-msgtext / "MIME"
; text other than actual body part (headers,
; etc.)
*/
#[cfg(test)]
mod tests {
use crate::{
protocol::{
Sequence,
fetch::{self, Attribute, Section},
},
receiver::Receiver,
};
#[test]
fn parse_fetch() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A654 FETCH 2:4 (FLAGS BODY[HEADER.FIELDS (DATE FROM)])\r\n",
fetch::Arguments {
tag: "A654".into(),
sequence_set: Sequence::range(2.into(), 4.into()),
attributes: vec![
Attribute::Flags,
Attribute::BodySection {
peek: false,
sections: vec![Section::HeaderFields {
not: false,
fields: vec!["DATE".into(), "FROM".into()],
}],
partial: None,
},
],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 BODY[]\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![Attribute::BodySection {
peek: false,
sections: vec![],
partial: None,
}],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 (BODY[HEADER])\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![Attribute::BodySection {
peek: false,
sections: vec![Section::Header],
partial: None,
}],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 (BODY.PEEK[HEADER.FIELDS (X-MAILER)] PREVIEW(LAZY))\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![
Attribute::BodySection {
peek: true,
sections: vec![Section::HeaderFields {
not: false,
fields: vec!["X-MAILER".into()],
}],
partial: None,
},
Attribute::Preview { lazy: true },
],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 (BODY[HEADER.FIELDS.NOT (FROM TO SUBJECT)])\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![Attribute::BodySection {
peek: false,
sections: vec![Section::HeaderFields {
not: true,
fields: vec!["FROM".into(), "TO".into(), "SUBJECT".into()],
}],
partial: None,
}],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 (BODY[MIME] BODY[TEXT] PREVIEW)\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![
Attribute::BodySection {
peek: false,
sections: vec![Section::Mime],
partial: None,
},
Attribute::BodySection {
peek: false,
sections: vec![Section::Text],
partial: None,
},
Attribute::Preview { lazy: false },
],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 (BODYSTRUCTURE ENVELOPE FLAGS INTERNALDATE UID)\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![
Attribute::BodyStructure,
Attribute::Envelope,
Attribute::Flags,
Attribute::InternalDate,
Attribute::Uid,
],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 (RFC822 RFC822.HEADER RFC822.SIZE RFC822.TEXT)\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![
Attribute::Rfc822,
Attribute::Rfc822Header,
Attribute::Rfc822Size,
Attribute::Rfc822Text,
],
changed_since: None,
include_vanished: false,
},
),
(
concat!(
"A001 FETCH 1 (",
"BODY[4.2.HEADER]<0.20> ",
"BODY.PEEK[3.2.2.2] ",
"BODY[4.2.TEXT]<4.100> ",
"BINARY[1.2.3] ",
"BINARY.PEEK[4] ",
"BINARY[6.5.4]<100.200> ",
"BINARY.PEEK[7]<9.88> ",
"BINARY.SIZE[9.1]",
")\r\n"
),
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![
Attribute::BodySection {
peek: false,
sections: vec![
Section::Part { num: 4 },
Section::Part { num: 2 },
Section::Header,
],
partial: Some((0, 20)),
},
Attribute::BodySection {
peek: true,
sections: vec![
Section::Part { num: 3 },
Section::Part { num: 2 },
Section::Part { num: 2 },
Section::Part { num: 2 },
],
partial: None,
},
Attribute::BodySection {
peek: false,
sections: vec![
Section::Part { num: 4 },
Section::Part { num: 2 },
Section::Text,
],
partial: Some((4, 100)),
},
Attribute::Binary {
peek: false,
sections: vec![1, 2, 3],
partial: None,
},
Attribute::Binary {
peek: true,
sections: vec![4],
partial: None,
},
Attribute::Binary {
peek: false,
sections: vec![6, 5, 4],
partial: Some((100, 200)),
},
Attribute::Binary {
peek: true,
sections: vec![7],
partial: Some((9, 88)),
},
Attribute::BinarySize {
sections: vec![9, 1],
},
],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 ALL\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![
Attribute::Flags,
Attribute::InternalDate,
Attribute::Rfc822Size,
Attribute::Envelope,
],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 FULL\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![
Attribute::Flags,
Attribute::InternalDate,
Attribute::Rfc822Size,
Attribute::Envelope,
Attribute::Body,
],
changed_since: None,
include_vanished: false,
},
),
(
"A001 FETCH 1 FAST\r\n",
fetch::Arguments {
tag: "A001".into(),
sequence_set: Sequence::number(1),
attributes: vec![
Attribute::Flags,
Attribute::InternalDate,
Attribute::Rfc822Size,
],
changed_since: None,
include_vanished: false,
},
),
(
"s100 UID FETCH 1:* (FLAGS MODSEQ) (CHANGEDSINCE 12345 VANISHED)\r\n",
fetch::Arguments {
tag: "s100".into(),
sequence_set: Sequence::range(1.into(), None),
attributes: vec![Attribute::Flags, Attribute::ModSeq],
changed_since: 12345.into(),
include_vanished: true,
},
),
(
"9 UID FETCH 1:* UID (VANISHED CHANGEDSINCE 1)\r\n",
fetch::Arguments {
tag: "9".into(),
sequence_set: Sequence::range(1.into(), None),
attributes: vec![Attribute::Uid],
changed_since: 1.into(),
include_vanished: true,
},
),
(
"A010 FETCH 1:* (OBJECTID)\r\n",
fetch::Arguments {
tag: "A010".into(),
sequence_set: Sequence::range(1.into(), None),
attributes: vec![Attribute::ObjectId],
changed_since: None,
include_vanished: false,
},
),
(
"A011 FETCH 1 (UID OBJECTID FLAGS)\r\n",
fetch::Arguments {
tag: "A011".into(),
sequence_set: Sequence::number(1),
attributes: vec![Attribute::Uid, Attribute::ObjectId, Attribute::Flags],
changed_since: None,
include_vanished: false,
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_fetch()
.expect(command),
arguments,
"{}",
command
);
}
}
}
+382
View File
@@ -0,0 +1,382 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::{CompactString, ToCompactString};
use crate::{
Command,
protocol::{
list::{self, ReturnOption, SelectionOption},
status::Status,
},
receiver::{Request, Token, bad},
utf7::utf7_maybe_decode,
};
impl Request<Command> {
#[allow(clippy::while_let_on_iterator)]
pub fn parse_list(self, is_utf8: bool) -> trc::Result<list::Arguments> {
match self.tokens.len() {
0 | 1 => Err(self.into_error("Missing arguments.")),
2 => {
let mut tokens = self.tokens.into_iter();
Ok(list::Arguments::Basic {
reference_name: tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
mailbox_name: utf7_maybe_decode(
tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
),
tag: self.tag,
})
}
_ => {
let mut tokens = self.tokens.into_iter();
let mut selection_options = Vec::new();
let mut return_options = Vec::new();
let mut mailbox_name = Vec::new();
let reference_name = match tokens.next().unwrap() {
Token::ParenthesisOpen => {
while let Some(token) = tokens.next() {
match token {
Token::ParenthesisClose => break,
Token::Argument(value) => {
selection_options.push(
SelectionOption::parse(&value)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
);
}
_ => {
return Err(bad(
self.tag.to_compact_string(),
"Invalid selection option argument.",
));
}
}
}
tokens
.next()
.ok_or_else(|| {
bad(self.tag.to_compact_string(), "Missing reference name.")
})?
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?
}
token => token
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
};
match tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing mailbox name."))?
{
Token::ParenthesisOpen => {
while let Some(token) = tokens.next() {
match token {
Token::ParenthesisClose => break,
token => {
mailbox_name.push(
token
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
);
}
}
}
}
token => {
mailbox_name.push(utf7_maybe_decode(
token
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
));
}
}
if tokens
.next()
.is_some_and(|token| token.eq_ignore_ascii_case(b"return"))
{
if tokens
.next()
.is_none_or(|token| !token.is_parenthesis_open())
{
return Err(bad(
self.tag.to_compact_string(),
"Invalid return option, expected parenthesis.",
));
}
while let Some(token) = tokens.next() {
match token {
Token::ParenthesisClose => break,
Token::Argument(value) => {
let mut return_option = ReturnOption::parse(&value)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
if let ReturnOption::Status(status) = &mut return_option {
if tokens
.next()
.is_none_or(|token| !token.is_parenthesis_open())
{
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Invalid return option, expected parenthesis after STATUS.",
));
}
while let Some(token) = tokens.next() {
match token {
Token::ParenthesisClose => break,
Token::Argument(value) => {
status.push(Status::parse(&value).map_err(
|v| bad(self.tag.to_compact_string(), v),
)?);
}
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Invalid status return option argument.",
));
}
}
}
}
return_options.push(return_option);
}
_ => {
return Err(bad(
self.tag.to_compact_string(),
"Invalid return option argument.",
));
}
}
}
}
Ok(list::Arguments::Extended {
tag: self.tag,
reference_name,
mailbox_name,
selection_options,
return_options,
})
}
}
}
}
impl SelectionOption {
pub fn parse(value: &[u8]) -> super::Result<Self> {
hashify::tiny_map_ignore_case!(value,
"SUBSCRIBED" => Self::Subscribed,
"REMOTE" => Self::Remote,
"RECURSIVEMATCH" => Self::RecursiveMatch,
"SPECIAL-USE" => Self::SpecialUse,
)
.ok_or_else(|| {
format!(
"Unsupported selection option '{}'.",
String::from_utf8_lossy(value)
)
.into()
})
}
}
impl ReturnOption {
pub fn parse(value: &[u8]) -> super::Result<Self> {
hashify::tiny_map_ignore_case!(value,
"SUBSCRIBED" => Self::Subscribed,
"CHILDREN" => Self::Children,
"STATUS" => Self::Status(Vec::with_capacity(2)),
"SPECIAL-USE" => Self::SpecialUse,
)
.ok_or_else(|| format!("Invalid return option {:?}", String::from_utf8_lossy(value)).into())
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{
list::{self, ReturnOption, SelectionOption},
status::Status,
},
receiver::Receiver,
};
#[test]
fn parse_list() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A682 LIST \"\" *\r\n",
list::Arguments::Basic {
tag: "A682".into(),
reference_name: "".into(),
mailbox_name: "*".into(),
},
),
(
"A02 LIST (SUBSCRIBED) \"\" \"*\"\r\n",
list::Arguments::Extended {
tag: "A02".into(),
reference_name: "".into(),
mailbox_name: vec!["*".into()],
selection_options: vec![SelectionOption::Subscribed],
return_options: vec![],
},
),
(
"A03 LIST () \"\" \"%\" RETURN (CHILDREN)\r\n",
list::Arguments::Extended {
tag: "A03".into(),
reference_name: "".into(),
mailbox_name: vec!["%".into()],
selection_options: vec![],
return_options: vec![ReturnOption::Children],
},
),
(
"A04 LIST (REMOTE) \"\" \"%\" RETURN (CHILDREN)\r\n",
list::Arguments::Extended {
tag: "A04".into(),
reference_name: "".into(),
mailbox_name: vec!["%".into()],
selection_options: vec![SelectionOption::Remote],
return_options: vec![ReturnOption::Children],
},
),
(
"A05 LIST (REMOTE SUBSCRIBED) \"\" \"*\"\r\n",
list::Arguments::Extended {
tag: "A05".into(),
reference_name: "".into(),
mailbox_name: vec!["*".into()],
selection_options: vec![SelectionOption::Remote, SelectionOption::Subscribed],
return_options: vec![],
},
),
(
"A06 LIST (REMOTE) \"\" \"*\" RETURN (SUBSCRIBED)\r\n",
list::Arguments::Extended {
tag: "A06".into(),
reference_name: "".into(),
mailbox_name: vec!["*".into()],
selection_options: vec![SelectionOption::Remote],
return_options: vec![ReturnOption::Subscribed],
},
),
(
"C04 LIST (SUBSCRIBED RECURSIVEMATCH) \"\" \"%\"\r\n",
list::Arguments::Extended {
tag: "C04".into(),
reference_name: "".into(),
mailbox_name: vec!["%".into()],
selection_options: vec![
SelectionOption::Subscribed,
SelectionOption::RecursiveMatch,
],
return_options: vec![],
},
),
(
"C04 LIST (SUBSCRIBED RECURSIVEMATCH) \"\" \"%\" RETURN (CHILDREN)\r\n",
list::Arguments::Extended {
tag: "C04".into(),
reference_name: "".into(),
mailbox_name: vec!["%".into()],
selection_options: vec![
SelectionOption::Subscribed,
SelectionOption::RecursiveMatch,
],
return_options: vec![ReturnOption::Children],
},
),
(
"a1 LIST \"\" (\"foo\")\r\n",
list::Arguments::Extended {
tag: "a1".into(),
reference_name: "".into(),
mailbox_name: vec!["foo".into()],
selection_options: vec![],
return_options: vec![],
},
),
(
"a3.1 LIST \"\" (% music/rock)\r\n",
list::Arguments::Extended {
tag: "a3.1".into(),
reference_name: "".into(),
mailbox_name: vec!["%".into(), "music/rock".into()],
selection_options: vec![],
return_options: vec![],
},
),
(
"BBB LIST \"\" (\"INBOX\" \"Drafts\" \"Sent/%\")\r\n",
list::Arguments::Extended {
tag: "BBB".into(),
reference_name: "".into(),
mailbox_name: vec!["INBOX".into(), "Drafts".into(), "Sent/%".into()],
selection_options: vec![],
return_options: vec![],
},
),
(
"A01 LIST \"\" % RETURN (STATUS (MESSAGES UNSEEN))\r\n",
list::Arguments::Extended {
tag: "A01".into(),
reference_name: "".into(),
mailbox_name: vec!["%".into()],
selection_options: vec![],
return_options: vec![ReturnOption::Status(vec![
Status::Messages,
Status::Unseen,
])],
},
),
(
concat!(
"A02 LIST (SUBSCRIBED RECURSIVEMATCH) \"\" ",
"% RETURN (CHILDREN STATUS (MESSAGES))\r\n"
),
list::Arguments::Extended {
tag: "A02".into(),
reference_name: "".into(),
mailbox_name: vec!["%".into()],
selection_options: vec![
SelectionOption::Subscribed,
SelectionOption::RecursiveMatch,
],
return_options: vec![
ReturnOption::Children,
ReturnOption::Status(vec![Status::Messages]),
],
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_list(true)
.unwrap(),
arguments
);
}
}
}
+76
View File
@@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::login,
receiver::{Request, bad},
};
impl Request<Command> {
pub fn parse_login(self) -> trc::Result<login::Arguments> {
match self.tokens.len() {
2 => {
let mut tokens = self.tokens.into_iter();
Ok(login::Arguments {
username: tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
password: tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
tag: self.tag,
})
}
0 => Err(self.into_error("Missing arguments.")),
_ => Err(self.into_error("Too many arguments.")),
}
}
}
#[cfg(test)]
mod tests {
use crate::{protocol::login, receiver::Receiver};
#[test]
fn parse_login() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"a001 LOGIN SMITH SESAME\r\n",
login::Arguments {
tag: "a001".into(),
username: "SMITH".into(),
password: "SESAME".into(),
},
),
(
"A001 LOGIN {11+}\r\nFRED FOOBAR {7+}\r\nfat man\r\n",
login::Arguments {
tag: "A001".into(),
username: "FRED FOOBAR".into(),
password: "fat man".into(),
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_login()
.unwrap(),
arguments
);
}
}
}
+88
View File
@@ -0,0 +1,88 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::list::{self, SelectionOption},
receiver::{Request, bad},
utf7::utf7_maybe_decode,
};
impl Request<Command> {
pub fn parse_lsub(self, is_utf8: bool) -> trc::Result<list::Arguments> {
if self.tokens.len() > 1 {
let mut tokens = self.tokens.into_iter();
Ok(list::Arguments::Extended {
reference_name: tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing reference name."))?
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
mailbox_name: vec![utf7_maybe_decode(
tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing mailbox name."))?
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
)],
selection_options: vec![SelectionOption::Subscribed],
return_options: vec![],
tag: self.tag,
})
} else {
Err(self.into_error("Missing arguments."))
}
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::list::{self, SelectionOption},
receiver::Receiver,
};
#[test]
fn parse_lsub() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A002 LSUB \"#news.\" \"comp.mail.*\"\r\n",
list::Arguments::Extended {
tag: "A002".into(),
reference_name: "#news.".into(),
mailbox_name: vec!["comp.mail.*".into()],
selection_options: vec![SelectionOption::Subscribed],
return_options: vec![],
},
),
(
"A002 LSUB \"#news.\" \"comp.%\"\r\n",
list::Arguments::Extended {
tag: "A002".into(),
reference_name: "#news.".into(),
mailbox_name: vec!["comp.%".into()],
selection_options: vec![SelectionOption::Subscribed],
return_options: vec![],
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_lsub(false)
.unwrap(),
arguments
);
}
}
}
+502
View File
@@ -0,0 +1,502 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod acl;
pub mod append;
pub mod authenticate;
pub mod copy_move;
pub mod create;
pub mod delete;
pub mod enable;
pub mod fetch;
pub mod list;
pub mod login;
pub mod lsub;
pub mod quota;
pub mod rename;
pub mod search;
pub mod select;
pub mod sort;
pub mod status;
pub mod store;
pub mod subscribe;
pub mod thread;
pub mod uidbatches;
use std::{borrow::Cow, str::FromStr};
use chrono::{DateTime, NaiveDate};
use crate::{
Command,
protocol::{Flag, Sequence},
receiver::CommandParser,
};
pub type Result<T> = std::result::Result<T, Cow<'static, str>>;
impl CommandParser for Command {
fn parse(value: &[u8], uid: bool) -> Option<Self> {
hashify::tiny_map!(value,
"CAPABILITY" => Command::Capability,
"NOOP" => Command::Noop,
"LOGOUT" => Command::Logout,
"STARTTLS" => Command::StartTls,
"AUTHENTICATE" => Command::Authenticate,
"LOGIN" => Command::Login,
"ENABLE" => Command::Enable,
"SELECT" => Command::Select,
"EXAMINE" => Command::Examine,
"CREATE" => Command::Create,
"DELETE" => Command::Delete,
"RENAME" => Command::Rename,
"SUBSCRIBE" => Command::Subscribe,
"UNSUBSCRIBE" => Command::Unsubscribe,
"LIST" => Command::List,
"NAMESPACE" => Command::Namespace,
"STATUS" => Command::Status,
"APPEND" => Command::Append,
"IDLE" => Command::Idle,
"CLOSE" => Command::Close,
"UNSELECT" => Command::Unselect,
"EXPUNGE" => Command::Expunge(uid),
"SEARCH" => Command::Search(uid),
"FETCH" => Command::Fetch(uid),
"STORE" => Command::Store(uid),
"COPY" => Command::Copy(uid),
"MOVE" => Command::Move(uid),
"SORT" => Command::Sort(uid),
"THREAD" => Command::Thread(uid),
"LSUB" => Command::Lsub,
"CHECK" => Command::Check,
"SETACL" => Command::SetAcl,
"DELETEACL" => Command::DeleteAcl,
"GETACL" => Command::GetAcl,
"LISTRIGHTS" => Command::ListRights,
"MYRIGHTS" => Command::MyRights,
"UNAUTHENTICATE" => Command::Unauthenticate,
"ID" => Command::Id,
"GETQUOTA" => Command::GetQuota,
"GETQUOTAROOT" => Command::GetQuotaRoot,
"GETJMAPACCESS" => Command::GetJmapAccess,
"UIDBATCHES" => Command::UidBatches,
)
}
#[inline(always)]
fn tokenize_brackets(&self) -> bool {
matches!(self, Command::Fetch(_))
}
}
impl Flag {
pub fn parse_imap(value: Vec<u8>) -> Result<Self> {
if !value.is_empty() {
let flag = hashify::tiny_map_ignore_case!(value.as_slice(),
"\\Seen" => Flag::Seen,
"\\Answered" => Flag::Answered,
"\\Flagged" => Flag::Flagged,
"\\Deleted" => Flag::Deleted,
"\\Draft" => Flag::Draft,
"\\Recent" => Flag::Recent,
"\\Important" => Flag::Important,
"$Forwarded" => Flag::Forwarded,
"$MDNSent" => Flag::MDNSent,
"$Junk" => Flag::Junk,
"$NotJunk" => Flag::NotJunk,
"$Phishing" => Flag::Phishing,
"$Important" => Flag::Important,
"$autosent" => Flag::Autosent,
"$canunsubscribe" => Flag::CanUnsubscribe,
"$followed" => Flag::Followed,
"$hasattachment" => Flag::HasAttachment,
"$hasmemo" => Flag::HasMemo,
"$hasnoattachment" => Flag::HasNoAttachment,
"$imported" => Flag::Imported,
"$istrusted" => Flag::IsTrusted,
"$MailFlagBit0" => Flag::MailFlagBit0,
"$MailFlagBit1" => Flag::MailFlagBit1,
"$MailFlagBit2" => Flag::MailFlagBit2,
"$maskedemail" => Flag::MaskedEmail,
"$memo" => Flag::Memo,
"$muted" => Flag::Muted,
"$new" => Flag::New,
"$notify" => Flag::Notify,
"$unsubscribed" => Flag::Unsubscribed,
);
if let Some(flag) = flag {
Ok(flag)
} else {
String::from_utf8(value)
.map_err(|_| Cow::from("Invalid UTF-8."))
.map(|v| Flag::Keyword(v.into_boxed_str()))
}
} else {
Err(Cow::from("Null flags are not allowed."))
}
}
pub fn parse_jmap(value: String) -> Self {
if value.starts_with('$') {
hashify::tiny_map_ignore_case!(value.as_bytes(),
"$seen" => Flag::Seen,
"$draft" => Flag::Draft,
"$flagged" => Flag::Flagged,
"$answered" => Flag::Answered,
"$recent" => Flag::Recent,
"$important" => Flag::Important,
"$phishing" => Flag::Phishing,
"$junk" => Flag::Junk,
"$notjunk" => Flag::NotJunk,
"$deleted" => Flag::Deleted,
"$forwarded" => Flag::Forwarded,
"$mdnsent" => Flag::MDNSent,
"$autosent" => Flag::Autosent,
"$canunsubscribe" => Flag::CanUnsubscribe,
"$followed" => Flag::Followed,
"$hasattachment" => Flag::HasAttachment,
"$hasmemo" => Flag::HasMemo,
"$hasnoattachment" => Flag::HasNoAttachment,
"$imported" => Flag::Imported,
"$istrusted" => Flag::IsTrusted,
"$MailFlagBit0" => Flag::MailFlagBit0,
"$MailFlagBit1" => Flag::MailFlagBit1,
"$MailFlagBit2" => Flag::MailFlagBit2,
"$maskedemail" => Flag::MaskedEmail,
"$memo" => Flag::Memo,
"$muted" => Flag::Muted,
"$new" => Flag::New,
"$notify" => Flag::Notify,
"$unsubscribed" => Flag::Unsubscribed,
)
.unwrap_or_else(|| Flag::Keyword(value.into_boxed_str()))
} else {
let mut keyword = String::with_capacity(value.len());
for c in value.chars() {
if c.is_ascii_alphanumeric() {
keyword.push(c);
} else {
keyword.push('_');
}
}
Flag::Keyword(keyword.into_boxed_str())
}
}
}
pub fn parse_datetime(value: &[u8]) -> Result<i64> {
std::str::from_utf8(value)
.map_err(|_| Cow::from("Expected date/time, found an invalid UTF-8 string."))
.and_then(|datetime| {
DateTime::parse_from_str(datetime.trim(), "%d-%b-%Y %H:%M:%S %z")
.map_err(|_| Cow::from(format!("Failed to parse date/time '{}'.", datetime)))
.map(|dt| dt.timestamp())
})
}
pub fn parse_date(value: &[u8]) -> Result<i64> {
std::str::from_utf8(value)
.map_err(|_| Cow::from("Expected date, found an invalid UTF-8 string."))
.and_then(|date| {
NaiveDate::parse_from_str(date.trim(), "%d-%b-%Y")
.map_err(|_| Cow::from(format!("Failed to parse date '{}'.", date)))
.map(|dt| {
dt.and_hms_opt(0, 0, 0)
.unwrap_or_default()
.and_utc()
.timestamp()
})
})
}
pub fn parse_number<T: FromStr>(value: &[u8]) -> Result<T> {
std::str::from_utf8(value)
.map_err(|_| Cow::from("Expected a number, found an invalid UTF-8 string."))
.and_then(|string| {
string
.parse::<T>()
.map_err(|_| Cow::from(format!("Expected a number, found {:?}.", string)))
})
}
pub fn parse_sequence_set(value: &[u8]) -> Result<Sequence> {
let mut sequence_set = Vec::new();
let mut range_start = None;
let mut token_start = None;
let mut is_wildcard = false;
let mut is_range = false;
let mut is_saved_search = false;
for (mut pos, ch) in value.iter().enumerate() {
let mut add_token = false;
match ch {
b',' => {
add_token = true;
}
b':' => {
if !is_range {
if let Some(from_pos) = token_start {
range_start =
parse_number::<u32>(value.get(from_pos..pos).ok_or_else(|| {
Cow::from(format!(
"Invalid sequence set {:?}, parse error.",
String::from_utf8_lossy(value)
))
})?)?
.into();
token_start = None;
} else if is_wildcard {
is_wildcard = false;
} else {
return Err(Cow::from(format!(
"Invalid sequence set {:?}, number expected before ':'.",
String::from_utf8_lossy(value)
)));
}
is_range = true;
} else {
return Err(Cow::from(format!(
"Invalid sequence set {:?}, ':' appears multiple times.",
String::from_utf8_lossy(value)
)));
}
}
b'*' => {
if !is_wildcard {
if value.len() == 1 {
return Ok(Sequence::Range {
start: None,
end: None,
});
} else if token_start.is_none() {
is_wildcard = true;
} else {
return Err(Cow::from(format!(
"Invalid sequence set {:?}, invalid use of '*'.",
String::from_utf8_lossy(value)
)));
}
} else {
return Err(Cow::from(format!(
"Invalid sequence set {:?}, '*' appears multiple times.",
String::from_utf8_lossy(value)
)));
}
}
b'$' => {
if value.get(pos + 1).is_none_or(|&ch| ch == b',') {
is_saved_search = true;
} else {
return Err(Cow::from(format!(
"Invalid sequence set {:?}, unexpected token after '$'.",
String::from_utf8_lossy(value)
)));
}
}
_ => {
if ch.is_ascii_digit() {
if is_wildcard {
return Err(Cow::from(format!(
"Invalid sequence set {:?}, invalid use of '*'.",
String::from_utf8_lossy(value)
)));
}
if token_start.is_none() {
token_start = pos.into();
}
} else {
return Err(Cow::from(format!(
"Invalid sequence set {:?}, found invalid character '{}' at position {}.",
String::from_utf8_lossy(value),
ch,
pos
)));
}
}
}
if add_token || pos == value.len() - 1 {
if is_range {
sequence_set.push(Sequence::Range {
start: range_start,
end: if !is_wildcard {
if !add_token {
pos += 1;
}
parse_number::<u32>(
value
.get(
token_start.ok_or_else(|| {
Cow::from(format!(
"Invalid sequence set {:?}, expected number.",
String::from_utf8_lossy(value)
))
})?..pos,
)
.ok_or_else(|| {
Cow::from(format!(
"Invalid sequence set {:?}, parse error.",
String::from_utf8_lossy(value)
))
})?,
)?
.into()
} else {
is_wildcard = false;
None
},
});
is_range = false;
range_start = None;
} else {
if !add_token {
pos += 1;
}
if is_wildcard {
sequence_set.push(Sequence::Range {
start: None,
end: None,
});
is_wildcard = false;
} else if is_saved_search {
sequence_set.push(Sequence::SavedSearch);
is_saved_search = false;
} else {
sequence_set.push(Sequence::Number {
value: parse_number(
value
.get(
token_start.ok_or_else(|| {
Cow::from(format!(
"Invalid sequence set {:?}, expected number.",
String::from_utf8_lossy(value)
))
})?..pos,
)
.ok_or_else(|| {
Cow::from(format!(
"Invalid sequence set {:?}, parse error.",
String::from_utf8_lossy(value)
))
})?,
)?,
});
}
}
token_start = None;
}
}
match sequence_set.len() {
1 => Ok(sequence_set.pop().unwrap()),
0 => Err(Cow::from("Invalid empty sequence set.")),
_ => Ok(Sequence::List {
items: sequence_set,
}),
}
}
pub trait PushUnique<T> {
fn push_unique(&mut self, value: T);
}
impl<T: PartialEq> PushUnique<T> for Vec<T> {
fn push_unique(&mut self, value: T) {
if !self.contains(&value) {
self.push(value);
}
}
}
#[cfg(test)]
mod tests {
use crate::{Command, protocol::Sequence, receiver::CommandParser};
#[test]
fn parse_command() {
assert_eq!(
Command::parse(b"GETJMAPACCESS", false),
Some(Command::GetJmapAccess)
);
assert_eq!(Command::parse(b"NOTACOMMAND", false), None);
}
#[test]
fn parse_sequence_set() {
for (sequence, expected_result) in [
("$", Sequence::SavedSearch),
(
"*",
Sequence::Range {
start: None,
end: None,
},
),
(
"1,3000:3021",
Sequence::List {
items: vec![
Sequence::Number { value: 1 },
Sequence::Range {
start: 3000.into(),
end: 3021.into(),
},
],
},
),
(
"2,4:7,9,12:*",
Sequence::List {
items: vec![
Sequence::Number { value: 2 },
Sequence::Range {
start: 4.into(),
end: 7.into(),
},
Sequence::Number { value: 9 },
Sequence::Range {
start: 12.into(),
end: None,
},
],
},
),
(
"*:4,5:7",
Sequence::List {
items: vec![
Sequence::Range {
start: None,
end: 4.into(),
},
Sequence::Range {
start: 5.into(),
end: 7.into(),
},
],
},
),
(
"2,4,5",
Sequence::List {
items: vec![
Sequence::Number { value: 2 },
Sequence::Number { value: 4 },
Sequence::Number { value: 5 },
],
},
),
] {
assert_eq!(
super::parse_sequence_set(sequence.as_bytes()).unwrap(),
expected_result
);
}
}
}
+94
View File
@@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::quota,
receiver::{Request, bad},
utf7::utf7_maybe_decode,
};
impl Request<Command> {
pub fn parse_get_quota_root(self, is_utf8: bool) -> trc::Result<quota::Arguments> {
match self.tokens.len() {
1 => Ok(quota::Arguments {
name: utf7_maybe_decode(
self.tokens
.into_iter()
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
),
tag: self.tag,
}),
0 => Err(self.into_error("Missing mailbox name.")),
_ => Err(self.into_error("Too many arguments.")),
}
}
pub fn parse_get_quota(self) -> trc::Result<quota::Arguments> {
match self.tokens.len() {
1 => Ok(quota::Arguments {
name: self
.tokens
.into_iter()
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
tag: self.tag,
}),
0 => Err(self.into_error("Missing quota root.")),
_ => Err(self.into_error("Too many arguments.")),
}
}
}
#[cfg(test)]
mod tests {
use crate::{protocol::quota, receiver::Receiver};
#[test]
fn parse_quota() {
let mut receiver = Receiver::new();
let (command, arguments) = (
"A142 GETQUOTAROOT INBOX\r\n",
quota::Arguments {
name: "INBOX".into(),
tag: "A142".into(),
},
);
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_get_quota_root(true)
.unwrap(),
arguments
);
let (command, arguments) = (
"A142 GETQUOTA \"my funky mailbox\"\r\n",
quota::Arguments {
name: "my funky mailbox".into(),
tag: "A142".into(),
},
);
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_get_quota()
.unwrap(),
arguments
);
}
}
+84
View File
@@ -0,0 +1,84 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::rename,
receiver::{Request, bad},
utf7::utf7_maybe_decode,
};
impl Request<Command> {
pub fn parse_rename(self, is_utf8: bool) -> trc::Result<rename::Arguments> {
match self.tokens.len() {
2 => {
let mut tokens = self.tokens.into_iter();
Ok(rename::Arguments {
mailbox_name: utf7_maybe_decode(
tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
),
new_mailbox_name: utf7_maybe_decode(
tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
),
tag: self.tag,
})
}
0 => Err(self.into_error("Missing argument.")),
1 => Err(self.into_error("Missing new mailbox name.")),
_ => Err(self.into_error("Too many arguments.")),
}
}
}
#[cfg(test)]
mod tests {
use crate::{protocol::rename, receiver::Receiver};
#[test]
fn parse_rename() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A142 RENAME \"my funky mailbox\" Private\r\n",
rename::Arguments {
mailbox_name: "my funky mailbox".into(),
new_mailbox_name: "Private".into(),
tag: "A142".into(),
},
),
(
"A142 RENAME {1+}\r\na {1+}\r\nb\r\n",
rename::Arguments {
mailbox_name: "a".into(),
new_mailbox_name: "b".into(),
tag: "A142".into(),
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_rename(true)
.unwrap(),
arguments
);
}
}
}
+890
View File
@@ -0,0 +1,890 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use std::iter::Peekable;
use std::vec::IntoIter;
use compact_str::ToCompactString;
use mail_parser::decoders::charsets::DecoderFnc;
use mail_parser::decoders::charsets::map::charset_decoder;
use crate::Command;
use crate::protocol::search::{self, Filter};
use crate::protocol::search::{ModSeqEntry, ResultOption};
use crate::protocol::{Flag, ProtocolVersion};
use crate::receiver::{Request, Token, bad};
use super::{parse_date, parse_number, parse_sequence_set};
impl Request<Command> {
#[allow(clippy::while_let_on_iterator)]
pub fn parse_search(self, version: ProtocolVersion) -> trc::Result<search::Arguments> {
if self.tokens.is_empty() {
return Err(self.into_error("Missing search criteria."));
}
let mut tokens = self.tokens.into_iter().peekable();
let mut result_options = Vec::new();
let mut decoder = None;
let mut is_esearch = version.is_rev2();
loop {
match tokens.peek() {
Some(Token::Argument(value)) if value.eq_ignore_ascii_case(b"return") => {
tokens.next();
is_esearch = true;
result_options = parse_result_options(&mut tokens)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
}
Some(Token::Argument(value)) if value.eq_ignore_ascii_case(b"charset") => {
tokens.next();
decoder = charset_decoder(
&tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing charset."))?
.unwrap_bytes(),
);
}
_ => break,
}
}
let filter = parse_filters(&mut tokens, decoder)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
match filter.len() {
0 => Err(bad(
self.tag.to_compact_string(),
"No filters found in command.",
)),
_ => Ok(search::Arguments {
tag: self.tag,
result_options,
filter,
sort: None,
is_esearch,
}),
}
}
}
pub fn parse_result_options(
tokens: &mut Peekable<IntoIter<Token>>,
) -> super::Result<Vec<ResultOption>> {
let mut result_options = Vec::new();
if tokens
.next()
.is_none_or(|token| !token.is_parenthesis_open())
{
return Err(Cow::from("Invalid result option, expected parenthesis."));
}
for token in tokens {
match token {
Token::ParenthesisClose => break,
Token::Argument(value) => {
result_options.push(ResultOption::parse(&value)?);
}
_ => return Err(Cow::from("Invalid result option argument.")),
}
}
Ok(result_options)
}
pub fn parse_filters(
tokens: &mut Peekable<IntoIter<Token>>,
decoder: Option<DecoderFnc>,
) -> super::Result<Vec<Filter>> {
let mut filters = Vec::new();
let mut filters_len = 0;
let mut filters_stack = Vec::new();
let mut operator = Filter::And;
while let Some(token) = tokens.next() {
let mut found_parenthesis = false;
match token {
Token::Argument(value) => {
hashify::fnc_map_ignore_case!(value.as_slice(),
"ALL" => {
filters.push(Filter::All);
},
"ANSWERED" => {
filters.push(Filter::Answered);
},
"BCC" => {
filters.push(Filter::Bcc(decode_argument(tokens, decoder)?));
},
"BEFORE" => {
filters.push(Filter::Before(parse_date(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected date"))?
.unwrap_bytes(),
)?));
},
"BODY" => {
filters.push(Filter::Body(decode_argument(tokens, decoder)?));
},
"CC" => {
filters.push(Filter::Cc(decode_argument(tokens, decoder)?));
},
"DELETED" => {
filters.push(Filter::Deleted);
},
"DRAFT" => {
filters.push(Filter::Draft);
},
"FLAGGED" => {
filters.push(Filter::Flagged);
},
"FROM" => {
filters.push(Filter::From(decode_argument(tokens, decoder)?));
},
"HEADER" => {
filters.push(Filter::Header(
decode_argument(tokens, decoder)?,
decode_argument(tokens, decoder)?,
));
},
"KEYWORD" => {
filters.push(Filter::Keyword(Flag::parse_imap(
tokens
.next()
.ok_or_else(|| Cow::from("Expected keyword"))?
.unwrap_bytes(),
)?));
},
"LARGER" => {
filters.push(Filter::Larger(parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected integer"))?
.unwrap_bytes(),
)?));
},
"ON" => {
filters.push(Filter::On(parse_date(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected date"))?
.unwrap_bytes(),
)?));
},
"SEEN" => {
filters.push(Filter::Seen);
},
"SENTBEFORE" => {
filters.push(Filter::SentBefore(parse_date(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected date"))?
.unwrap_bytes(),
)?));
},
"SENTON" => {
filters.push(Filter::SentOn(parse_date(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected date"))?
.unwrap_bytes(),
)?));
},
"SENTSINCE" => {
filters.push(Filter::SentSince(parse_date(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected date"))?
.unwrap_bytes(),
)?));
},
"SINCE" => {
filters.push(Filter::Since(parse_date(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected date"))?
.unwrap_bytes(),
)?));
},
"SMALLER" => {
filters.push(Filter::Smaller(parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected integer"))?
.unwrap_bytes(),
)?));
},
"SUBJECT" => {
filters.push(Filter::Subject(decode_argument(tokens, decoder)?));
},
"TEXT" => {
filters.push(Filter::Text(decode_argument(tokens, decoder)?));
},
"TO" => {
filters.push(Filter::To(decode_argument(tokens, decoder)?));
},
"UID" => {
filters.push(Filter::Sequence(
parse_sequence_set(
&tokens
.next()
.ok_or_else(|| Cow::from("Missing sequence set."))?
.unwrap_bytes(),
)?,
true,
));
},
"UNANSWERED" => {
filters.push(Filter::Unanswered);
},
"UNDELETED" => {
filters.push(Filter::Undeleted);
},
"UNDRAFT" => {
filters.push(Filter::Undraft);
},
"UNFLAGGED" => {
filters.push(Filter::Unflagged);
},
"UNKEYWORD" => {
filters.push(Filter::Unkeyword(Flag::parse_imap(
tokens
.next()
.ok_or_else(|| Cow::from("Expected keyword"))?
.unwrap_bytes(),
)?));
},
"UNSEEN" => {
filters.push(Filter::Unseen);
},
"UIDAFTER" => {
filters.push(Filter::UidAfter(parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected integer"))?
.unwrap_bytes(),
)?));
},
"UIDBEFORE" => {
filters.push(Filter::UidBefore(parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected integer"))?
.unwrap_bytes(),
)?));
},
"OLDER" => {
filters.push(Filter::Older(parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected integer"))?
.unwrap_bytes(),
)?));
},
"YOUNGER" => {
filters.push(Filter::Younger(parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| Cow::from("Expected integer"))?
.unwrap_bytes(),
)?));
},
"OLD" => {
filters.push(Filter::Old);
},
"NEW" => {
filters.push(Filter::New);
},
"RECENT" => {
filters.push(Filter::Recent);
},
"MODSEQ" => {
let param = tokens
.next()
.ok_or_else(|| Cow::from("Missing MODSEQ parameters."))?
.unwrap_bytes();
if param.is_empty() || param.iter().any(|ch| !ch.is_ascii_digit()) {
if param.len() <= 7 || !param.starts_with(b"/flags/") {
return Err(format!(
"Unsupported MODSEQ parameter '{}'.",
String::from_utf8_lossy(&param)
)
.into());
}
let flag = Flag::parse_imap((param[7..]).to_vec())?;
let mod_seq_entry = match tokens.next() {
Some(Token::Argument(value)) if value.eq_ignore_ascii_case(b"all") => {
ModSeqEntry::All(flag)
}
Some(Token::Argument(value))
if value.eq_ignore_ascii_case(b"shared") =>
{
ModSeqEntry::Shared(flag)
}
Some(Token::Argument(value)) if value.eq_ignore_ascii_case(b"priv") => {
ModSeqEntry::Private(flag)
}
Some(token) => {
return Err(
format!("Unsupported MODSEQ parameter '{}'.", token).into()
);
}
None => {
return Err("Missing MODSEQ entry-type-req parameter.".into());
}
};
filters.push(Filter::ModSeq((
parse_number::<u64>(
&tokens
.next()
.ok_or_else(|| {
Cow::from("Missing MODSEQ mod-sequence-valzer parameter.")
})?
.unwrap_bytes(),
)?,
mod_seq_entry,
)));
} else {
filters.push(Filter::ModSeq((
parse_number::<u64>(&param)?,
ModSeqEntry::None,
)));
}
},
"EMAILID" => {
filters.push(Filter::EmailId(
tokens
.next()
.ok_or_else(|| Cow::from("Expected an EMAILID value."))?
.unwrap_string()?,
));
},
"THREADID" => {
filters.push(Filter::ThreadId(
tokens
.next()
.ok_or_else(|| Cow::from("Expected an THREADID value."))?
.unwrap_string()?,
));
},
"OR" => {
if filters_stack.len() > 10 {
return Err(Cow::from("Too many nested filters"));
}
filters_stack.push((filters, operator, filters_len));
filters_len = 0;
filters = Vec::with_capacity(2);
operator = Filter::Or;
continue;
},
"NOT" => {
if filters_stack.len() > 10 {
return Err(Cow::from("Too many nested filters"));
}
filters_stack.push((filters, operator, filters_len));
filters_len = 0;
filters = Vec::with_capacity(1);
operator = Filter::Not;
continue;
},
_ => {
filters.push(Filter::Sequence(parse_sequence_set(&value)?, false));
}
);
filters_len += 1;
}
Token::ParenthesisOpen => {
if filters_stack.len() > 10 {
return Err(Cow::from("Too many nested filters"));
}
filters_stack.push((filters, operator, filters_len));
filters_len = 0;
filters = Vec::with_capacity(5);
operator = Filter::And;
continue;
}
Token::ParenthesisClose => {
if filters_stack.is_empty() {
return Err(Cow::from("Unexpected parenthesis."));
}
found_parenthesis = true;
}
token => return Err(format!("Unexpected token {:?}.", token.to_string()).into()),
}
if !filters_stack.is_empty()
&& (found_parenthesis
|| (operator == Filter::Or && filters_len == 2)
|| (operator == Filter::Not && filters_len == 1))
{
while let Some((mut prev_filters, prev_operator, prev_filters_len)) =
filters_stack.pop()
{
if operator == Filter::And && (prev_operator != Filter::Or || filters_len == 1) {
prev_filters.extend(filters);
filters_len += prev_filters_len;
} else {
prev_filters.push(operator);
prev_filters.extend(filters);
prev_filters.push(Filter::End);
filters_len = prev_filters_len + 1;
}
operator = prev_operator;
filters = prev_filters;
if operator == Filter::And || (operator == Filter::Or && filters_len < 2) {
break;
}
}
}
}
Ok(filters)
}
pub fn decode_argument(
tokens: &mut Peekable<IntoIter<Token>>,
decoder: Option<DecoderFnc>,
) -> super::Result<String> {
let argument = tokens
.next()
.ok_or_else(|| Cow::from("Expected string."))?
.unwrap_bytes();
if let Some(decoder) = decoder {
Ok(decoder(&argument))
} else {
Ok(String::from_utf8(argument).map_err(|_| Cow::from("Invalid UTF-8 argument."))?)
}
}
impl ResultOption {
pub fn parse(value: &[u8]) -> super::Result<Self> {
hashify::tiny_map_ignore_case!(
value,
"min" => Self::Min,
"max" => Self::Max,
"all" => Self::All,
"count" => Self::Count,
"save" => Self::Save,
"context" => Self::Context,
)
.ok_or_else(|| {
format!(
"Invalid result option '{}'.",
String::from_utf8_lossy(value)
)
.into()
})
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{
Flag, ProtocolVersion, Sequence,
search::{self, Filter, ModSeqEntry, ResultOption},
},
receiver::Receiver,
};
#[test]
fn parse_search() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
b"A282 SEARCH RETURN (MIN COUNT) FLAGGED SINCE 1-Feb-1994 NOT FROM \"Smith\"\r\n"
.to_vec(),
search::Arguments {
tag: "A282".into(),
result_options: vec![ResultOption::Min, ResultOption::Count],
filter: vec![
Filter::Flagged,
Filter::Since(760060800),
Filter::Not,
Filter::From("Smith".into()),
Filter::End,
],
is_esearch: true,
sort: None,
},
),
(
b"A283 SEARCH RETURN () FLAGGED SINCE 1-Feb-1994 NOT FROM \"Smith\"\r\n".to_vec(),
search::Arguments {
tag: "A283".into(),
result_options: vec![],
filter: vec![
Filter::Flagged,
Filter::Since(760060800),
Filter::Not,
Filter::From("Smith".into()),
Filter::End,
],
is_esearch: true,
sort: None,
},
),
(
b"A301 SEARCH $ SMALLER 4096\r\n".to_vec(),
search::Arguments {
tag: "A301".into(),
result_options: vec![],
filter: vec![Filter::seq_saved_search(), Filter::Smaller(4096)],
is_esearch: true,
sort: None,
},
),
(
"P283 SEARCH CHARSET UTF-8 (OR $ 1,3000:3021) TEXT {8+}\r\nмать\r\n"
.as_bytes()
.to_vec(),
search::Arguments {
tag: "P283".into(),
result_options: vec![],
filter: vec![
Filter::Or,
Filter::seq_saved_search(),
Filter::Sequence(
Sequence::List {
items: vec![
Sequence::number(1),
Sequence::range(3000.into(), 3021.into()),
],
},
false,
),
Filter::End,
Filter::Text("мать".into()),
],
is_esearch: true,
sort: None,
},
),
(
b"F282 SEARCH RETURN (SAVE) KEYWORD $Junk\r\n".to_vec(),
search::Arguments {
tag: "F282".into(),
result_options: vec![ResultOption::Save],
filter: vec![Filter::Keyword(Flag::Junk)],
is_esearch: true,
sort: None,
},
),
(
[
b"F282 SEARCH OR OR FROM [email protected] TO ".to_vec(),
b"[email protected] OR BCC [email protected] ".to_vec(),
b"CC [email protected]\r\n".to_vec(),
]
.concat(),
search::Arguments {
tag: "F282".into(),
result_options: vec![],
filter: vec![
Filter::Or,
Filter::Or,
Filter::From("[email protected]".into()),
Filter::To("[email protected]".into()),
Filter::End,
Filter::Or,
Filter::Bcc("[email protected]".into()),
Filter::Cc("[email protected]".into()),
Filter::End,
Filter::End,
],
is_esearch: true,
sort: None,
},
),
(
[
b"abc SEARCH OR SMALLER 10000 OR ".to_vec(),
b"HEADER Subject \"ravioli festival\" ".to_vec(),
b"HEADER From \"dr. ravioli\"\r\n".to_vec(),
]
.concat(),
search::Arguments {
tag: "abc".into(),
result_options: vec![],
filter: vec![
Filter::Or,
Filter::Smaller(10000),
Filter::Or,
Filter::Header("Subject".into(), "ravioli festival".into()),
Filter::Header("From".into(), "dr. ravioli".into()),
Filter::End,
Filter::End,
],
is_esearch: true,
sort: None,
},
),
(
[
b"abc SEARCH (DELETED SEEN ANSWERED) ".to_vec(),
b"NOT (FROM john TO jane BCC bill) ".to_vec(),
b"(1,30:* UID 1,2,3,4 $)\r\n".to_vec(),
]
.concat(),
search::Arguments {
tag: "abc".into(),
result_options: vec![],
filter: vec![
Filter::Deleted,
Filter::Seen,
Filter::Answered,
Filter::Not,
Filter::From("john".into()),
Filter::To("jane".into()),
Filter::Bcc("bill".into()),
Filter::End,
Filter::Sequence(
Sequence::List {
items: vec![Sequence::number(1), Sequence::range(30.into(), None)],
},
false,
),
Filter::Sequence(
Sequence::List {
items: vec![
Sequence::number(1),
Sequence::number(2),
Sequence::number(3),
Sequence::number(4),
],
},
true,
),
Filter::seq_saved_search(),
],
is_esearch: true,
sort: None,
},
),
(
[
b"abc SEARCH *:* UID *:100,100:* ".to_vec(),
b"(FLAGGED (DRAFT (DELETED (ANSWERED)))) ".to_vec(),
b"OR (SENTON 20-Nov-2022) (LARGER 8196)\r\n".to_vec(),
]
.concat(),
search::Arguments {
tag: "abc".into(),
result_options: vec![],
filter: vec![
Filter::seq_range(None, None),
Filter::Sequence(
Sequence::List {
items: vec![
Sequence::range(None, 100.into()),
Sequence::range(100.into(), None),
],
},
true,
),
Filter::Flagged,
Filter::Draft,
Filter::Deleted,
Filter::Answered,
Filter::Or,
Filter::SentOn(1668902400),
Filter::Larger(8196),
Filter::End,
],
is_esearch: true,
sort: None,
},
),
(
[
b"abc SEARCH NOT (FROM john OR TO jane CC bill) ".to_vec(),
b"OR (UNDELETED ALL) ($ NOT FLAGGED) ".to_vec(),
b"(((KEYWORD \"tps report\")))\r\n".to_vec(),
]
.concat(),
search::Arguments {
tag: "abc".into(),
result_options: vec![],
filter: vec![
Filter::Not,
Filter::From("john".into()),
Filter::Or,
Filter::To("jane".into()),
Filter::Cc("bill".into()),
Filter::End,
Filter::End,
Filter::Or,
Filter::And,
Filter::Undeleted,
Filter::All,
Filter::End,
Filter::And,
Filter::seq_saved_search(),
Filter::Not,
Filter::Flagged,
Filter::End,
Filter::End,
Filter::End,
Filter::Keyword(Flag::Keyword("tps report".into())),
],
is_esearch: true,
sort: None,
},
),
(
[
b"B283 SEARCH RETURN (SAVE MIN MAX) CHARSET KOI8-R TEXT ".to_vec(),
b"{11+}\r\n\xf0\xd2\xc9\xd7\xc5\xd4, \xcd\xc9\xd2\r\n".to_vec(),
]
.concat(),
search::Arguments {
tag: "B283".into(),
result_options: vec![ResultOption::Save, ResultOption::Min, ResultOption::Max],
filter: vec![Filter::Text("Привет, мир".into())],
is_esearch: true,
sort: None,
},
),
(
b"B283 SEARCH CHARSET BIG5 FROM \"\xa7A\xa6n\xa1A\xa5@\xac\xc9\"\r\n".to_vec(),
search::Arguments {
tag: "B283".into(),
result_options: vec![],
filter: vec![Filter::From("你好,世界".into())],
is_esearch: true,
sort: None,
},
),
(
b"a SEARCH MODSEQ \"/flags/\\draft\" all 620162338\r\n".to_vec(),
search::Arguments {
tag: "a".into(),
result_options: vec![],
filter: vec![Filter::ModSeq((620162338, ModSeqEntry::All(Flag::Draft)))],
is_esearch: true,
sort: None,
},
),
(
b"t SEARCH OR NOT MODSEQ 720162338 LARGER 50000\r\n".to_vec(),
search::Arguments {
tag: "t".into(),
result_options: vec![],
filter: vec![
Filter::Or,
Filter::Not,
Filter::ModSeq((720162338, ModSeqEntry::None)),
Filter::End,
Filter::Larger(50000),
Filter::End,
],
is_esearch: true,
sort: None,
},
),
(
b"5 UID SEARCH BEFORE 1-Dec-2023\r\n".to_vec(),
search::Arguments {
tag: "5".into(),
result_options: vec![],
filter: vec![Filter::Before(1701388800)],
is_esearch: true,
sort: None,
},
),
] {
let command_str = String::from_utf8_lossy(&command).into_owned();
assert_eq!(
receiver
.parse(&mut command.iter())
.unwrap()
.parse_search(ProtocolVersion::Rev2)
.expect(&command_str),
arguments,
"{}",
command_str
);
}
}
}
+511
View File
@@ -0,0 +1,511 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::{CompactString, ToCompactString, format_compact};
use std::str::FromStr;
use types::id::Id;
use crate::{
Command,
protocol::{
ObjectId,
select::{self, QResync},
},
receiver::{Request, Token, bad},
utf7::utf7_maybe_decode,
};
use super::{parse_number, parse_sequence_set};
impl Request<Command> {
pub fn parse_select(self, is_utf8: bool) -> trc::Result<select::Arguments> {
if !self.tokens.is_empty() {
let mut tokens = self.tokens.into_iter().peekable();
// Mailbox name
let mailbox_name = utf7_maybe_decode(
tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
);
// CONDSTORE parameters
let mut condstore = false;
let mut qresync = None;
let mut objectid = None;
match tokens.next() {
Some(Token::ParenthesisOpen) => {
while let Some(token) = tokens.next() {
match token {
Token::Argument(param) if param.eq_ignore_ascii_case(b"CONDSTORE") => {
condstore = true;
}
Token::Argument(param) if param.eq_ignore_ascii_case(b"QRESYNC") => {
if tokens
.next()
.is_none_or(|token| !token.is_parenthesis_open())
{
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Expected '(' after 'QRESYNC'.",
));
}
let uid_validity = parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| {
bad(
self.tag.to_compact_string(),
"Missing uidvalidity parameter for QRESYNC.",
)
})?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
let modseq = parse_number::<u64>(
&tokens
.next()
.ok_or_else(|| {
bad(
self.tag.to_compact_string(),
"Missing modseq parameter for QRESYNC.",
)
})?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
let mut known_uids = None;
let mut seq_match = None;
let has_seq_match = match tokens.peek() {
Some(Token::Argument(value)) => {
known_uids = parse_sequence_set(value)
.map_err(|v| bad(self.tag.to_compact_string(), v))?
.into();
tokens.next();
if matches!(tokens.peek(), Some(Token::ParenthesisOpen)) {
tokens.next();
true
} else {
false
}
}
Some(Token::ParenthesisOpen) => {
tokens.next();
true
}
_ => false,
};
if has_seq_match {
seq_match = Some((
parse_sequence_set(
&tokens
.next()
.ok_or_else(|| {
bad(
self.tag.to_compact_string(),
"Missing known-sequence-set parameter for QRESYNC.",
)
})?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
parse_sequence_set(
&tokens
.next()
.ok_or_else(|| {
bad(
self.tag.to_compact_string(),
"Missing known-uid-set parameter for QRESYNC.",
)
})?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
));
if tokens
.next()
.is_none_or(|token| !token.is_parenthesis_close())
{
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Missing ')' for 'QRESYNC'.",
));
}
}
if tokens
.next()
.is_none_or(|token| !token.is_parenthesis_close())
{
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Missing ')' for 'QRESYNC'.",
));
}
qresync = QResync {
uid_validity,
modseq,
known_uids,
seq_match,
}
.into();
}
Token::Argument(param) if param.eq_ignore_ascii_case(b"OBJECTID") => {
let mut oid = ObjectId::default();
if matches!(tokens.peek(), Some(Token::ParenthesisOpen)) {
tokens.next();
while let Some(token) = tokens.next() {
match token {
Token::ParenthesisClose => break,
Token::Argument(key) => {
let value = tokens
.next()
.ok_or_else(|| {
bad(
self.tag.to_compact_string(),
"Expected value after OBJECTID key.",
)
})?
.unwrap_bytes();
let id = std::str::from_utf8(&value)
.ok()
.and_then(|v| Id::from_str(v).ok());
hashify::fnc_map_ignore_case!(key.as_slice(),
"MAILBOXID" => { oid.mailbox_id = id; },
"ACCOUNTID" => { oid.account_id = id; },
"EMAILID" => { oid.email_id = id; },
"THREADID" => { oid.thread_id = id; },
_ => {}
);
}
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
format_compact!(
"Unexpected value '{}'.",
token
),
));
}
}
}
}
objectid = Some(oid);
}
Token::ParenthesisClose => {
break;
}
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
format_compact!("Unexpected value '{}'.", token),
));
}
}
}
}
Some(token) => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
format_compact!("Unexpected value '{}'.", token),
));
}
None => (),
}
Ok(select::Arguments {
mailbox_name,
tag: self.tag,
condstore,
qresync,
objectid,
})
} else {
Err(self.into_error("Missing mailbox name."))
}
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{
ObjectId, Sequence,
select::{self, QResync},
},
receiver::Receiver,
};
use std::str::FromStr;
use types::id::Id;
#[test]
fn parse_select_objectid() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A1 SELECT \"foo\" (OBJECTID)\r\n",
select::Arguments {
mailbox_name: "foo".into(),
tag: "A1".into(),
condstore: false,
qresync: None,
objectid: Some(ObjectId::default()),
},
),
(
"A2 SELECT \"foo\" (OBJECTID (MAILBOXID abc ACCOUNTID xyz))\r\n",
select::Arguments {
mailbox_name: "foo".into(),
tag: "A2".into(),
condstore: false,
qresync: None,
objectid: Some(ObjectId {
mailbox_id: Some(Id::from_str("abc").unwrap()),
account_id: Some(Id::from_str("xyz").unwrap()),
..Default::default()
}),
},
),
(
"A3 EXAMINE \"foo\" (OBJECTID (MAILBOXID abc))\r\n",
select::Arguments {
mailbox_name: "foo".into(),
tag: "A3".into(),
condstore: false,
qresync: None,
objectid: Some(ObjectId {
mailbox_id: Some(Id::from_str("abc").unwrap()),
..Default::default()
}),
},
),
(
"A4 SELECT \"foo\" (CONDSTORE OBJECTID)\r\n",
select::Arguments {
mailbox_name: "foo".into(),
tag: "A4".into(),
condstore: true,
qresync: None,
objectid: Some(ObjectId::default()),
},
),
(
"A5 SELECT \"foo\" (OBJECTID (FOOBAR baz MAILBOXID abc))\r\n",
select::Arguments {
mailbox_name: "foo".into(),
tag: "A5".into(),
condstore: false,
qresync: None,
objectid: Some(ObjectId {
mailbox_id: Some(Id::from_str("abc").unwrap()),
..Default::default()
}),
},
),
(
"A6 SELECT \"foo\" (OBJECTID (MAILBOXID 456))\r\n",
select::Arguments {
mailbox_name: "foo".into(),
tag: "A6".into(),
condstore: false,
qresync: None,
objectid: Some(ObjectId::default()),
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_select(true)
.unwrap(),
arguments,
"Failed to parse {command}"
);
}
}
#[test]
fn parse_select() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A142 SELECT INBOX\r\n",
select::Arguments {
mailbox_name: "INBOX".into(),
tag: "A142".into(),
condstore: false,
qresync: None,
objectid: None,
},
),
(
"A142 SELECT \"my funky mailbox\"\r\n",
select::Arguments {
mailbox_name: "my funky mailbox".into(),
tag: "A142".into(),
condstore: false,
qresync: None,
objectid: None,
},
),
(
"A142 SELECT INBOX (CONDSTORE)\r\n",
select::Arguments {
mailbox_name: "INBOX".into(),
tag: "A142".into(),
condstore: true,
qresync: None,
objectid: None,
},
),
(
"A142 SELECT INBOX (QRESYNC (3857529045 20010715194032001 1:198))\r\n",
select::Arguments {
mailbox_name: "INBOX".into(),
tag: "A142".into(),
condstore: false,
qresync: QResync {
uid_validity: 3857529045,
modseq: 20010715194032001,
known_uids: Some(Sequence::Range {
start: Some(1),
end: Some(198),
}),
seq_match: None,
}
.into(),
objectid: None,
},
),
(
concat!(
"A03 SELECT INBOX (QRESYNC (67890007 90060115194045000 ",
"41:211,214:541) CONDSTORE)\r\n"
),
select::Arguments {
mailbox_name: "INBOX".into(),
tag: "A03".into(),
condstore: true,
qresync: QResync {
uid_validity: 67890007,
modseq: 90060115194045000,
known_uids: Some(Sequence::List {
items: vec![
Sequence::Range {
start: Some(41),
end: Some(211),
},
Sequence::Range {
start: Some(214),
end: Some(541),
},
],
}),
seq_match: None,
}
.into(),
objectid: None,
},
),
(
concat!(
"B04 SELECT INBOX (QRESYNC (67890007 ",
"90060115194045000 1:29997 (5000,7500,9000,9990:9999 15000,",
"22500,27000,29970,29973,29976,29979,29982,29985,29988,29991,",
"29994,29997)))\r\n"
),
select::Arguments {
mailbox_name: "INBOX".into(),
tag: "B04".into(),
condstore: false,
qresync: QResync {
uid_validity: 67890007,
modseq: 90060115194045000,
known_uids: Some(Sequence::Range {
start: Some(1),
end: Some(29997),
}),
seq_match: Some((
Sequence::List {
items: vec![
Sequence::Number { value: 5000 },
Sequence::Number { value: 7500 },
Sequence::Number { value: 9000 },
Sequence::Range {
start: Some(9990),
end: Some(9999),
},
],
},
Sequence::List {
items: vec![
Sequence::Number { value: 15000 },
Sequence::Number { value: 22500 },
Sequence::Number { value: 27000 },
Sequence::Number { value: 29970 },
Sequence::Number { value: 29973 },
Sequence::Number { value: 29976 },
Sequence::Number { value: 29979 },
Sequence::Number { value: 29982 },
Sequence::Number { value: 29985 },
Sequence::Number { value: 29988 },
Sequence::Number { value: 29991 },
Sequence::Number { value: 29994 },
Sequence::Number { value: 29997 },
],
},
)),
}
.into(),
objectid: None,
},
),
(
"A12 SELECT \"INBOX\" (QRESYNC (1693237464 16582))\r\n",
select::Arguments {
mailbox_name: "INBOX".into(),
tag: "A12".into(),
condstore: false,
qresync: QResync {
uid_validity: 1693237464,
modseq: 16582,
known_uids: None,
seq_match: None,
}
.into(),
objectid: None,
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap_or_else(|err| panic!(
"Failed to parse command '{}': {:?}",
command, err
))
.parse_select(true)
.unwrap_or_else(|err| panic!(
"Failed to parse command '{}': {:?}",
command, err
)),
arguments,
"Failed to parse {}",
command
);
}
}
}
+242
View File
@@ -0,0 +1,242 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use mail_parser::decoders::charsets::map::charset_decoder;
use crate::{
Command,
protocol::search::{Arguments, Comparator, Sort},
receiver::{Request, Token, bad},
};
use super::search::{parse_filters, parse_result_options};
impl Request<Command> {
#[allow(clippy::while_let_on_iterator)]
pub fn parse_sort(self) -> trc::Result<Arguments> {
if self.tokens.is_empty() {
return Err(self.into_error("Missing sort criteria."));
}
let mut tokens = self.tokens.into_iter().peekable();
let mut sort = Vec::new();
let (result_options, is_esearch) = match tokens.peek() {
Some(Token::Argument(value)) if value.eq_ignore_ascii_case(b"return") => {
tokens.next();
(
parse_result_options(&mut tokens)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
true,
)
}
_ => (Vec::new(), false),
};
if tokens
.next()
.is_none_or(|token| !token.is_parenthesis_open())
{
return Err(bad(
self.tag.to_compact_string(),
"Expected sort criteria between parentheses.",
));
}
let mut is_ascending = true;
while let Some(token) = tokens.next() {
match token {
Token::ParenthesisClose => break,
Token::Argument(value) => {
if value.eq_ignore_ascii_case(b"REVERSE") {
is_ascending = false;
} else {
sort.push(Comparator {
sort: Sort::parse(&value)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
ascending: is_ascending,
});
is_ascending = true;
}
}
_ => {
return Err(bad(
self.tag.to_compact_string(),
"Invalid result option argument.",
));
}
}
}
if sort.is_empty() {
return Err(bad(self.tag.to_compact_string(), "Missing sort criteria."));
}
let decoder = charset_decoder(
&tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing charset."))?
.unwrap_bytes(),
);
let filter = parse_filters(&mut tokens, decoder)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
match filter.len() {
0 => Err(bad(
self.tag.to_compact_string(),
"No filters found in command.",
)),
_ => Ok(Arguments {
sort: sort.into(),
result_options,
filter,
is_esearch,
tag: self.tag,
}),
}
}
}
impl Sort {
pub fn parse(value: &[u8]) -> super::Result<Self> {
hashify::tiny_map_ignore_case!(value,
"ARRIVAL" => Self::Arrival,
"CC" => Self::Cc,
"DATE" => Self::Date,
"FROM" => Self::From,
"SIZE" => Self::Size,
"SUBJECT" => Self::Subject,
"TO" => Self::To,
"DISPLAYFROM" => Self::DisplayFrom,
"DISPLAYTO" => Self::DisplayTo,
)
.ok_or_else(|| format!("Invalid sort criteria {:?}", String::from_utf8_lossy(value)).into())
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{
Flag,
search::{Arguments, Comparator, Filter, ResultOption, Sort},
},
receiver::Receiver,
};
#[test]
fn parse_sort() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
b"A282 SORT (SUBJECT) UTF-8 SINCE 1-Feb-1994\r\n".to_vec(),
Arguments {
sort: vec![Comparator {
sort: Sort::Subject,
ascending: true,
}]
.into(),
filter: vec![Filter::Since(760060800)],
result_options: Vec::new(),
is_esearch: false,
tag: "A282".into(),
},
),
(
b"A283 SORT (SUBJECT REVERSE DATE) UTF-8 ALL\r\n".to_vec(),
Arguments {
sort: vec![
Comparator {
sort: Sort::Subject,
ascending: true,
},
Comparator {
sort: Sort::Date,
ascending: false,
},
]
.into(),
filter: vec![Filter::All],
result_options: Vec::new(),
is_esearch: false,
tag: "A283".into(),
},
),
(
b"A284 SORT (SUBJECT) US-ASCII TEXT \"not in mailbox\"\r\n".to_vec(),
Arguments {
sort: vec![Comparator {
sort: Sort::Subject,
ascending: true,
}]
.into(),
filter: vec![Filter::Text("not in mailbox".into())],
result_options: Vec::new(),
is_esearch: false,
tag: "A284".into(),
},
),
(
[
b"A284 SORT (REVERSE ARRIVAL FROM) iso-8859-6 SUBJECT ".to_vec(),
b"\"\xe5\xd1\xcd\xc8\xc7 \xc8\xc7\xe4\xd9\xc7\xe4\xe5\"\r\n".to_vec(),
]
.concat(),
Arguments {
sort: vec![
Comparator {
sort: Sort::Arrival,
ascending: false,
},
Comparator {
sort: Sort::From,
ascending: true,
},
]
.into(),
filter: vec![Filter::Subject("مرحبا بالعالم".into())],
result_options: Vec::new(),
is_esearch: false,
tag: "A284".into(),
},
),
(
[
b"E01 UID SORT RETURN (COUNT) (REVERSE DATE) ".to_vec(),
b"UTF-8 UNDELETED UNKEYWORD $Junk\r\n".to_vec(),
]
.concat(),
Arguments {
sort: vec![Comparator {
sort: Sort::Date,
ascending: false,
}]
.into(),
filter: vec![Filter::Undeleted, Filter::Unkeyword(Flag::Junk)],
result_options: vec![ResultOption::Count],
is_esearch: true,
tag: "E01".into(),
},
),
] {
let command_str = String::from_utf8_lossy(&command).into_owned();
assert_eq!(
receiver
.parse(&mut command.iter())
.unwrap()
.parse_sort()
.expect(&command_str),
arguments,
"{}",
command_str
);
}
}
}
+150
View File
@@ -0,0 +1,150 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::{CompactString, ToCompactString};
use crate::Command;
use crate::protocol::status;
use crate::protocol::status::Status;
use crate::receiver::{Request, Token, bad};
use crate::utf7::utf7_maybe_decode;
impl Request<Command> {
pub fn parse_status(self, is_utf8: bool) -> trc::Result<status::Arguments> {
match self.tokens.len() {
0..=3 => Err(self.into_error("Missing arguments.")),
len => {
let mut tokens = self.tokens.into_iter();
let mailbox_name = utf7_maybe_decode(
tokens
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
);
let mut items = Vec::with_capacity(len - 2);
if tokens
.next()
.is_none_or(|token| !token.is_parenthesis_open())
{
return Err(bad(
self.tag.to_compact_string(),
"Expected parenthesis after mailbox name.",
));
}
#[allow(clippy::while_let_on_iterator)]
while let Some(token) = tokens.next() {
match token {
Token::ParenthesisClose => break,
Token::Argument(value) => {
items.push(
Status::parse(&value)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
);
}
_ => {
return Err(bad(
self.tag.to_compact_string(),
"Invalid status return option argument.",
));
}
}
}
if !items.is_empty() {
Ok(status::Arguments {
tag: self.tag,
mailbox_name,
items,
})
} else {
Err(bad(
CompactString::from_string_buffer(self.tag),
"At least one status item is required.",
))
}
}
}
}
}
impl Status {
pub fn parse(value: &[u8]) -> super::Result<Self> {
hashify::tiny_map_ignore_case!(value,
"MESSAGES" => Self::Messages,
"UIDNEXT" => Self::UidNext,
"UIDVALIDITY" => Self::UidValidity,
"UNSEEN" => Self::Unseen,
"DELETED" => Self::Deleted,
"SIZE" => Self::Size,
"HIGHESTMODSEQ" => Self::HighestModSeq,
"OBJECTID" => Self::ObjectId,
"RECENT" => Self::Recent,
"DELETED-STORAGE" => Self::DeletedStorage
)
.ok_or_else(|| {
format!(
"Invalid status option '{}'.",
String::from_utf8_lossy(value)
)
.into()
})
}
}
#[cfg(test)]
mod tests {
use crate::{protocol::status, receiver::Receiver};
#[test]
fn parse_status() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A042 STATUS blurdybloop (UIDNEXT MESSAGES)\r\n",
status::Arguments {
tag: "A042".into(),
mailbox_name: "blurdybloop".into(),
items: vec![status::Status::UidNext, status::Status::Messages],
},
),
(
"A043 STATUS foo (OBJECTID)\r\n",
status::Arguments {
tag: "A043".into(),
mailbox_name: "foo".into(),
items: vec![status::Status::ObjectId],
},
),
(
"A044 STATUS foo (MESSAGES OBJECTID UIDVALIDITY)\r\n",
status::Arguments {
tag: "A044".into(),
mailbox_name: "foo".into(),
items: vec![
status::Status::Messages,
status::Status::ObjectId,
status::Status::UidValidity,
],
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_status(true)
.unwrap(),
arguments,
"Failed to parse {command}"
);
}
}
}
+219
View File
@@ -0,0 +1,219 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::{CompactString, ToCompactString, format_compact};
use crate::{
Command,
protocol::{
Flag,
store::{self, Operation},
},
receiver::{Request, Token, bad},
};
use super::{parse_number, parse_sequence_set};
impl Request<Command> {
pub fn parse_store(self) -> trc::Result<store::Arguments> {
let mut tokens = self.tokens.into_iter().peekable();
// Sequence set
let sequence_set = parse_sequence_set(
&tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing sequence set."))?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
let mut unchanged_since = None;
// CONDSTORE parameters
if let Some(Token::ParenthesisOpen) = tokens.peek() {
tokens.next();
while let Some(token) = tokens.next() {
match token {
Token::Argument(param) if param.eq_ignore_ascii_case(b"UNCHANGEDSINCE") => {
unchanged_since = parse_number::<u64>(
&tokens
.next()
.ok_or_else(|| {
bad(
self.tag.to_compact_string(),
"Missing UNCHANGEDSINCE parameter.",
)
})?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?
.into();
}
Token::ParenthesisClose => {
break;
}
_ => {
return Err(bad(
self.tag.to_compact_string(),
format_compact!("Unsupported parameter '{}'.", token),
));
}
}
}
}
// Operation
let operation = tokens
.next()
.ok_or_else(|| {
bad(
self.tag.to_compact_string(),
"Missing message data item name.",
)
})?
.unwrap_bytes();
let (is_silent, operation) = hashify::tiny_map_ignore_case!(operation.as_slice(),
"FLAGS" => (false, Operation::Set),
"FLAGS.SILENT" => (true, Operation::Set),
"+FLAGS" => (false, Operation::Add),
"+FLAGS.SILENT" => (true, Operation::Add),
"-FLAGS" => (false, Operation::Clear),
"-FLAGS.SILENT" => (true, Operation::Clear),
)
.ok_or_else(|| {
bad(
self.tag.to_compact_string(),
format_compact!(
"Unsupported message data item name: {:?}",
String::from_utf8_lossy(&operation)
),
)
})?;
// Flags
let mut keywords = Vec::new();
match tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing flags to set."))?
{
Token::ParenthesisOpen => {
for token in tokens {
match token {
Token::Argument(flag) => {
keywords.push(
Flag::parse_imap(flag)
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
);
}
Token::ParenthesisClose => {
break;
}
_ => {
return Err(bad(self.tag.to_compact_string(), "Unsupported flag."));
}
}
}
}
Token::Argument(flag) => {
keywords.push(
Flag::parse_imap(flag).map_err(|v| bad(self.tag.to_compact_string(), v))?,
);
}
_ => {
return Err(bad(
CompactString::from_string_buffer(self.tag),
"Invalid flags parameter.",
));
}
}
if !keywords.is_empty() || operation == Operation::Set {
Ok(store::Arguments {
tag: self.tag,
sequence_set,
operation,
is_silent,
keywords,
unchanged_since,
})
} else {
Err(bad(self.tag.to_compact_string(), "Missing flags to set."))
}
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{
Flag, Sequence,
store::{self, Operation},
},
receiver::Receiver,
};
#[test]
fn parse_store() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A003 STORE 2:4 +FLAGS (\\Deleted)\r\n",
store::Arguments {
sequence_set: Sequence::Range {
start: 2.into(),
end: 4.into(),
},
is_silent: false,
operation: Operation::Add,
keywords: vec![Flag::Deleted],
tag: "A003".into(),
unchanged_since: None,
},
),
(
"A004 STORE *:100 -FLAGS.SILENT ($Phishing $Junk)\r\n",
store::Arguments {
sequence_set: Sequence::Range {
start: None,
end: 100.into(),
},
is_silent: true,
operation: Operation::Clear,
keywords: vec![Flag::Phishing, Flag::Junk],
tag: "A004".into(),
unchanged_since: None,
},
),
(
"d105 STORE 7,5,9 (UNCHANGEDSINCE 320162338) +FLAGS.SILENT \\Deleted\r\n",
store::Arguments {
sequence_set: Sequence::List {
items: vec![
Sequence::Number { value: 7 },
Sequence::Number { value: 5 },
Sequence::Number { value: 9 },
],
},
is_silent: true,
operation: Operation::Add,
keywords: vec![Flag::Deleted],
tag: "d105".into(),
unchanged_since: Some(320162338),
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_store()
.unwrap(),
arguments
);
}
}
}
+71
View File
@@ -0,0 +1,71 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use crate::{
Command,
protocol::subscribe,
receiver::{Request, bad},
utf7::utf7_maybe_decode,
};
impl Request<Command> {
pub fn parse_subscribe(self, is_utf8: bool) -> trc::Result<subscribe::Arguments> {
match self.tokens.len() {
1 => Ok(subscribe::Arguments {
mailbox_name: utf7_maybe_decode(
self.tokens
.into_iter()
.next()
.unwrap()
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?,
is_utf8,
),
tag: self.tag,
}),
0 => Err(self.into_error("Missing mailbox name.")),
_ => Err(self.into_error("Too many arguments.")),
}
}
}
#[cfg(test)]
mod tests {
use crate::{protocol::subscribe, receiver::Receiver};
#[test]
fn parse_subscribe() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A142 SUBSCRIBE #news.comp.mail.mime\r\n",
subscribe::Arguments {
mailbox_name: "#news.comp.mail.mime".into(),
tag: "A142".into(),
},
),
(
"A142 SUBSCRIBE \"#news.comp.mail.mime\"\r\n",
subscribe::Arguments {
mailbox_name: "#news.comp.mail.mime".into(),
tag: "A142".into(),
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_subscribe(true)
.unwrap(),
arguments
);
}
}
}
+120
View File
@@ -0,0 +1,120 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use compact_str::ToCompactString;
use mail_parser::decoders::charsets::map::charset_decoder;
use crate::{
Command,
protocol::thread::{self, Algorithm},
receiver::{Request, bad},
};
use super::search::parse_filters;
impl Request<Command> {
#[allow(clippy::while_let_on_iterator)]
pub fn parse_thread(self) -> trc::Result<thread::Arguments> {
if self.tokens.is_empty() {
return Err(self.into_error("Missing thread criteria."));
}
let mut tokens = self.tokens.into_iter().peekable();
let algorithm = Algorithm::parse(
&tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing threading algorithm."))?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
let decoder = charset_decoder(
&tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing charset."))?
.unwrap_bytes(),
);
let filter = parse_filters(&mut tokens, decoder)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
match filter.len() {
0 => Err(bad(
self.tag.to_compact_string(),
"No filters found in command.",
)),
_ => Ok(thread::Arguments {
algorithm,
filter,
tag: self.tag,
}),
}
}
}
impl Algorithm {
pub fn parse(value: &[u8]) -> super::Result<Self> {
hashify::tiny_map_ignore_case!(value,
"ORDEREDSUBJECT" => Self::OrderedSubject,
"REFERENCES" => Self::References,
)
.ok_or_else(|| {
format!(
"Invalid threading algorithm {:?}",
String::from_utf8_lossy(value)
)
.into()
})
}
}
#[cfg(test)]
mod tests {
use crate::{
protocol::{
search::Filter,
thread::{self, Algorithm},
},
receiver::Receiver,
};
#[test]
fn parse_thread() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
b"A283 THREAD ORDEREDSUBJECT UTF-8 SINCE 5-MAR-2000\r\n".to_vec(),
thread::Arguments {
algorithm: Algorithm::OrderedSubject,
filter: vec![Filter::Since(952214400)],
tag: "A283".into(),
},
),
(
b"A284 THREAD REFERENCES US-ASCII TEXT \"gewp\"\r\n".to_vec(),
thread::Arguments {
algorithm: Algorithm::References,
filter: vec![Filter::Text("gewp".into())],
tag: "A284".into(),
},
),
] {
let command_str = String::from_utf8_lossy(&command).into_owned();
assert_eq!(
receiver
.parse(&mut command.iter())
.unwrap()
.parse_thread()
.expect(&command_str),
arguments,
"{}",
command_str
);
}
}
}
+132
View File
@@ -0,0 +1,132 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
Command,
protocol::uidbatches,
receiver::{Request, bad},
};
use compact_str::ToCompactString;
use super::parse_number;
impl Request<Command> {
pub fn parse_uidbatches(self) -> trc::Result<uidbatches::Arguments> {
let mut tokens = self.tokens.into_iter();
let batch_size = parse_number::<u32>(
&tokens
.next()
.ok_or_else(|| bad(self.tag.to_compact_string(), "Missing batch size."))?
.unwrap_bytes(),
)
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
if batch_size == 0 {
return Err(bad(
self.tag.to_compact_string(),
"Batch size cannot be zero.",
));
}
let batch_range = match tokens.next() {
Some(token) => {
let token = token
.unwrap_string()
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
let (from, to) = token.split_once(':').ok_or_else(|| {
bad(
self.tag.to_compact_string(),
"Expected a batch range in the form 'from:to'.",
)
})?;
let from = parse_number::<u32>(from.trim().as_bytes())
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
let to = parse_number::<u32>(to.trim().as_bytes())
.map_err(|v| bad(self.tag.to_compact_string(), v))?;
if from == 0 || to == 0 {
return Err(bad(
self.tag.to_compact_string(),
"Batch numbers start at one.",
));
}
Some((from, to))
}
None => None,
};
if tokens.next().is_some() {
return Err(bad(
self.tag.to_compact_string(),
"Too many arguments for UIDBATCHES.",
));
}
Ok(uidbatches::Arguments {
tag: self.tag,
batch_size,
batch_range,
})
}
}
#[cfg(test)]
mod tests {
use crate::{protocol::uidbatches, receiver::Receiver};
#[test]
fn parse_uidbatches() {
let mut receiver = Receiver::new();
for (command, arguments) in [
(
"A143 UIDBATCHES 2000\r\n",
uidbatches::Arguments {
tag: "A143".into(),
batch_size: 2000,
batch_range: None,
},
),
(
"A302 UIDBATCHES 2000 10:20\r\n",
uidbatches::Arguments {
tag: "A302".into(),
batch_size: 2000,
batch_range: Some((10, 20)),
},
),
] {
assert_eq!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_uidbatches()
.unwrap(),
arguments,
"Failed to parse {command}"
);
}
for command in [
"A1 UIDBATCHES\r\n",
"A2 UIDBATCHES abc\r\n",
"A3 UIDBATCHES 2000 10\r\n",
"A4 UIDBATCHES 0\r\n",
"A5 UIDBATCHES 2000 0:20\r\n",
"A6 UIDBATCHES 2000 1:2 junk\r\n",
] {
assert!(
receiver
.parse(&mut command.as_bytes().iter())
.unwrap()
.parse_uidbatches()
.is_err(),
"Expected an error for {command}"
);
}
}
}