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:
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use protocol::ObjectId;
|
||||
use protocol::capability::Capability;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub mod parser;
|
||||
pub mod protocol;
|
||||
pub mod receiver;
|
||||
pub mod utf7;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub enum Command {
|
||||
// Client Commands - Any State
|
||||
Capability,
|
||||
#[default]
|
||||
Noop,
|
||||
Logout,
|
||||
|
||||
// Client Commands - Not Authenticated State
|
||||
StartTls,
|
||||
Authenticate,
|
||||
Login,
|
||||
|
||||
// Client Commands - Authenticated State
|
||||
Enable,
|
||||
Select,
|
||||
Examine,
|
||||
Create,
|
||||
Delete,
|
||||
Rename,
|
||||
Subscribe,
|
||||
Unsubscribe,
|
||||
List,
|
||||
Namespace,
|
||||
Status,
|
||||
Append,
|
||||
Idle,
|
||||
|
||||
// Client Commands - Selected State
|
||||
Close,
|
||||
Unselect,
|
||||
Expunge(bool),
|
||||
Search(bool),
|
||||
Fetch(bool),
|
||||
Store(bool),
|
||||
Copy(bool),
|
||||
Move(bool),
|
||||
|
||||
// IMAP4rev1
|
||||
Lsub,
|
||||
Check,
|
||||
|
||||
// RFC 5256
|
||||
Sort(bool),
|
||||
Thread(bool),
|
||||
|
||||
// RFC 4314
|
||||
SetAcl,
|
||||
DeleteAcl,
|
||||
GetAcl,
|
||||
ListRights,
|
||||
MyRights,
|
||||
|
||||
// RFC 8437
|
||||
Unauthenticate,
|
||||
|
||||
// RFC 2971
|
||||
Id,
|
||||
|
||||
// RFC 9208
|
||||
GetQuota,
|
||||
GetQuotaRoot,
|
||||
|
||||
// RFC 9698
|
||||
GetJmapAccess,
|
||||
|
||||
// RFC 10022
|
||||
UidBatches,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
pub fn is_uid(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Command::Fetch(true)
|
||||
| Command::Search(true)
|
||||
| Command::Copy(true)
|
||||
| Command::Move(true)
|
||||
| Command::Store(true)
|
||||
| Command::Expunge(true)
|
||||
| Command::Sort(true)
|
||||
| Command::Thread(true)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn requires_uid(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Command::Fetch(false)
|
||||
| Command::Search(false)
|
||||
| Command::Copy(false)
|
||||
| Command::Move(false)
|
||||
| Command::Store(false)
|
||||
| Command::Sort(false)
|
||||
| Command::Thread(false)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ResponseCode {
|
||||
Alert,
|
||||
AlreadyExists,
|
||||
AppendUid {
|
||||
uid_validity: u32,
|
||||
uids: Vec<u32>,
|
||||
},
|
||||
AuthenticationFailed,
|
||||
AuthorizationFailed,
|
||||
BadCharset,
|
||||
Cannot,
|
||||
Capability {
|
||||
capabilities: Vec<Capability>,
|
||||
},
|
||||
ClientBug,
|
||||
Closed,
|
||||
ContactAdmin,
|
||||
CopyUid {
|
||||
uid_validity: u32,
|
||||
src_uids: Vec<u32>,
|
||||
dest_uids: Vec<u32>,
|
||||
},
|
||||
Corruption,
|
||||
Expired,
|
||||
ExpungeIssued,
|
||||
HasChildren,
|
||||
InUse,
|
||||
Limit,
|
||||
NonExistent,
|
||||
NoPerm,
|
||||
OverQuota,
|
||||
Parse,
|
||||
PermanentFlags,
|
||||
PrivacyRequired,
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
ServerBug,
|
||||
TryCreate,
|
||||
UidNext,
|
||||
UidNotSticky,
|
||||
UidValidity,
|
||||
Unavailable,
|
||||
UnknownCte,
|
||||
|
||||
// CONDSTORE
|
||||
Modified {
|
||||
ids: Vec<u32>,
|
||||
},
|
||||
HighestModseq {
|
||||
modseq: u64,
|
||||
},
|
||||
|
||||
// ObjectID
|
||||
ObjectId(ObjectId),
|
||||
|
||||
// USEATTR
|
||||
UseAttr,
|
||||
|
||||
// UIDONLY
|
||||
UidRequired,
|
||||
|
||||
// UIDBATCHES
|
||||
TooFew,
|
||||
TooMany,
|
||||
|
||||
// MESSAGELIMIT
|
||||
MessageLimit {
|
||||
limit: u32,
|
||||
uid: Option<u32>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StatusResponse {
|
||||
pub tag: Option<String>,
|
||||
pub code: Option<ResponseCode>,
|
||||
pub message: Cow<'static, str>,
|
||||
pub rtype: ResponseType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ResponseType {
|
||||
Ok,
|
||||
No,
|
||||
Bad,
|
||||
PreAuth,
|
||||
Bye,
|
||||
}
|
||||
|
||||
impl ResponseCode {
|
||||
pub fn highest_modseq(modseq: u64) -> Self {
|
||||
ResponseCode::HighestModseq {
|
||||
modseq: if modseq > 0 { modseq + 1 } else { 0 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusResponse {
|
||||
pub fn bad(message: impl Into<Cow<'static, str>>) -> Self {
|
||||
StatusResponse {
|
||||
tag: None,
|
||||
code: None,
|
||||
message: message.into(),
|
||||
rtype: ResponseType::Bad,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_error(message: impl Into<Cow<'static, str>>) -> Self {
|
||||
StatusResponse {
|
||||
tag: None,
|
||||
code: ResponseCode::Parse.into(),
|
||||
message: message.into(),
|
||||
rtype: ResponseType::Bad,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn database_failure() -> Self {
|
||||
StatusResponse::no("Database failure.").with_code(ResponseCode::ContactAdmin)
|
||||
}
|
||||
|
||||
pub fn completed(command: Command) -> Self {
|
||||
StatusResponse::ok(format!("{} completed", command))
|
||||
}
|
||||
|
||||
pub fn with_code(mut self, code: ResponseCode) -> Self {
|
||||
self.code = Some(code);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
|
||||
self.tag = Some(tag.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn no(message: impl Into<Cow<'static, str>>) -> Self {
|
||||
StatusResponse {
|
||||
tag: None,
|
||||
code: None,
|
||||
message: message.into(),
|
||||
rtype: ResponseType::No,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ok(message: impl Into<Cow<'static, str>>) -> Self {
|
||||
StatusResponse {
|
||||
tag: None,
|
||||
code: None,
|
||||
message: message.into(),
|
||||
rtype: ResponseType::Ok,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bye(message: impl Into<Cow<'static, str>>) -> Self {
|
||||
StatusResponse {
|
||||
tag: None,
|
||||
code: None,
|
||||
message: message.into(),
|
||||
rtype: ResponseType::Bye,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(¶m)
|
||||
)
|
||||
.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>(¶m)?,
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
/*
|
||||
|
||||
l - lookup (mailbox is visible to LIST/LSUB commands, SUBSCRIBE
|
||||
mailbox)
|
||||
r - read (SELECT the mailbox, perform STATUS)
|
||||
s - keep seen/unseen information across sessions (set or clear
|
||||
\SEEN flag via STORE, also set \SEEN during APPEND/COPY/
|
||||
FETCH BODY[...])
|
||||
w - write (set or clear flags other than \SEEN and \DELETED via
|
||||
STORE, also set them during APPEND/COPY)
|
||||
i - insert (perform APPEND, COPY into mailbox)
|
||||
p - post (send mail to submission address for mailbox,
|
||||
not enforced by IMAP4 itself)
|
||||
k - create mailboxes (CREATE new sub-mailboxes in any
|
||||
implementation-defined hierarchy, parent mailbox for the new
|
||||
mailbox name in RENAME)
|
||||
x - delete mailbox (DELETE mailbox, old mailbox name in RENAME)
|
||||
t - delete messages (set or clear \DELETED flag via STORE, set
|
||||
\DELETED flag during APPEND/COPY)
|
||||
e - perform EXPUNGE and expunge as a part of CLOSE
|
||||
a - administer (perform SETACL/DELETEACL/GETACL/LISTRIGHTS)
|
||||
|
||||
// RFC2086
|
||||
c - create (CREATE new sub-mailboxes in any implementation-defined
|
||||
hierarchy)
|
||||
d - delete (STORE DELETED flag, perform EXPUNGE)
|
||||
|
||||
*/
|
||||
|
||||
use types::acl::Acl;
|
||||
|
||||
use super::quoted_string;
|
||||
use crate::utf7::utf7_encode;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum Rights {
|
||||
Lookup,
|
||||
Read,
|
||||
Seen,
|
||||
Write,
|
||||
Insert,
|
||||
Post,
|
||||
CreateMailbox,
|
||||
DeleteMailbox,
|
||||
DeleteMessages,
|
||||
Expunge,
|
||||
Administer,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ModRights {
|
||||
pub op: ModRightsOp,
|
||||
pub rights: Vec<Rights>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub enum ModRightsOp {
|
||||
Add,
|
||||
Remove,
|
||||
Replace,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub mailbox_name: String,
|
||||
pub identifier: Option<String>,
|
||||
pub mod_rights: Option<ModRights>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct GetAclResponse {
|
||||
pub mailbox_name: String,
|
||||
pub permissions: Vec<(String, Vec<Rights>)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListRightsResponse {
|
||||
pub mailbox_name: String,
|
||||
pub identifier: String,
|
||||
pub permissions: Vec<Vec<Rights>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct MyRightsResponse {
|
||||
pub mailbox_name: String,
|
||||
pub rights: Vec<Rights>,
|
||||
}
|
||||
|
||||
impl GetAclResponse {
|
||||
pub fn into_bytes(self, is_utf8: bool) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(self.mailbox_name.len() + 10 * self.permissions.len() * 5);
|
||||
buf.extend_from_slice(b"* ACL ");
|
||||
if is_utf8 {
|
||||
quoted_string(&mut buf, &self.mailbox_name);
|
||||
} else {
|
||||
quoted_string(&mut buf, &utf7_encode(&self.mailbox_name));
|
||||
}
|
||||
for (identifier, rights) in self.permissions {
|
||||
buf.extend_from_slice(b" ");
|
||||
quoted_string(&mut buf, &identifier);
|
||||
buf.extend_from_slice(b" ");
|
||||
|
||||
for right in rights {
|
||||
buf.push(right.to_char());
|
||||
}
|
||||
}
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl ListRightsResponse {
|
||||
pub fn into_bytes(self, is_utf8: bool) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(
|
||||
self.mailbox_name.len() + self.identifier.len() + 10 * self.permissions.len() * 5,
|
||||
);
|
||||
buf.extend_from_slice(b"* LISTRIGHTS ");
|
||||
if is_utf8 {
|
||||
quoted_string(&mut buf, &self.mailbox_name);
|
||||
} else {
|
||||
quoted_string(&mut buf, &utf7_encode(&self.mailbox_name));
|
||||
}
|
||||
buf.extend_from_slice(b" ");
|
||||
quoted_string(&mut buf, &self.identifier);
|
||||
for rights in self.permissions {
|
||||
buf.extend_from_slice(b" ");
|
||||
for right in rights {
|
||||
buf.push(right.to_char());
|
||||
}
|
||||
}
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl MyRightsResponse {
|
||||
pub fn into_bytes(self, is_utf8: bool) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(self.mailbox_name.len() + 10 + self.rights.len());
|
||||
buf.extend_from_slice(b"* MYRIGHTS ");
|
||||
if is_utf8 {
|
||||
quoted_string(&mut buf, &self.mailbox_name);
|
||||
} else {
|
||||
quoted_string(&mut buf, &utf7_encode(&self.mailbox_name));
|
||||
}
|
||||
buf.extend_from_slice(b" ");
|
||||
for right in self.rights {
|
||||
buf.push(right.to_char());
|
||||
}
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl Rights {
|
||||
pub fn to_char(&self) -> u8 {
|
||||
match self {
|
||||
Rights::Lookup => b'l',
|
||||
Rights::Read => b'r',
|
||||
Rights::Seen => b's',
|
||||
Rights::Write => b'w',
|
||||
Rights::Insert => b'i',
|
||||
Rights::Post => b'p',
|
||||
Rights::CreateMailbox => b'k',
|
||||
Rights::DeleteMailbox => b'x',
|
||||
Rights::DeleteMessages => b't',
|
||||
Rights::Expunge => b'e',
|
||||
Rights::Administer => b'a',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Rights {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Rights::Lookup => write!(f, "l"),
|
||||
Rights::Read => write!(f, "r"),
|
||||
Rights::Seen => write!(f, "s"),
|
||||
Rights::Write => write!(f, "w"),
|
||||
Rights::Insert => write!(f, "i"),
|
||||
Rights::Post => write!(f, "p"),
|
||||
Rights::CreateMailbox => write!(f, "k"),
|
||||
Rights::DeleteMailbox => write!(f, "x"),
|
||||
Rights::DeleteMessages => write!(f, "t"),
|
||||
Rights::Expunge => write!(f, "e"),
|
||||
Rights::Administer => write!(f, "a"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Rights> for Acl {
|
||||
fn from(value: Rights) -> Self {
|
||||
match value {
|
||||
Rights::Lookup => Acl::Read,
|
||||
Rights::Read => Acl::ReadItems,
|
||||
Rights::Seen => Acl::ModifyItems,
|
||||
Rights::Write => Acl::ModifyItems,
|
||||
Rights::Insert => Acl::AddItems,
|
||||
Rights::Post => Acl::Submit,
|
||||
Rights::CreateMailbox => Acl::CreateChild,
|
||||
Rights::DeleteMailbox => Acl::Delete,
|
||||
Rights::DeleteMessages => Acl::RemoveItems,
|
||||
Rights::Expunge => Acl::RemoveItems,
|
||||
Rights::Administer => Acl::Share,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::protocol::acl::{GetAclResponse, ListRightsResponse, MyRightsResponse, Rights};
|
||||
|
||||
#[test]
|
||||
fn serialize_acl() {
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
GetAclResponse {
|
||||
mailbox_name: "INBOX".into(),
|
||||
permissions: vec![
|
||||
(
|
||||
"Fred".into(),
|
||||
vec![
|
||||
Rights::Lookup,
|
||||
Rights::Read,
|
||||
Rights::Seen,
|
||||
Rights::Write,
|
||||
Rights::Insert,
|
||||
Rights::CreateMailbox,
|
||||
Rights::DeleteMessages,
|
||||
Rights::Administer,
|
||||
]
|
||||
),
|
||||
(
|
||||
"David".into(),
|
||||
vec![
|
||||
Rights::CreateMailbox,
|
||||
Rights::DeleteMessages,
|
||||
Rights::Administer,
|
||||
]
|
||||
)
|
||||
]
|
||||
}
|
||||
.into_bytes(true)
|
||||
)
|
||||
.unwrap(),
|
||||
"* ACL \"INBOX\" \"Fred\" lrswikta \"David\" kta\r\n"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
ListRightsResponse {
|
||||
mailbox_name: "Deleted Items".into(),
|
||||
identifier: "Fred".into(),
|
||||
permissions: vec![
|
||||
vec![Rights::Lookup, Rights::Read],
|
||||
vec![Rights::Administer],
|
||||
vec![Rights::DeleteMailbox]
|
||||
]
|
||||
}
|
||||
.into_bytes(true)
|
||||
)
|
||||
.unwrap(),
|
||||
"* LISTRIGHTS \"Deleted Items\" \"Fred\" lr a x\r\n"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
MyRightsResponse {
|
||||
mailbox_name: "Important".into(),
|
||||
rights: vec![Rights::Lookup, Rights::Read, Rights::DeleteMailbox]
|
||||
}
|
||||
.into_bytes(true)
|
||||
)
|
||||
.unwrap(),
|
||||
"* MYRIGHTS \"Important\" lrx\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::Flag;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub mailbox_name: String,
|
||||
pub messages: Vec<Message>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Message {
|
||||
pub message: Vec<u8>,
|
||||
pub flags: Vec<Flag>,
|
||||
pub received_at: Option<i64>,
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub mechanism: Mechanism,
|
||||
pub params: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Mechanism {
|
||||
Plain,
|
||||
CramMd5,
|
||||
DigestMd5,
|
||||
ScramSha1,
|
||||
ScramSha256,
|
||||
Apop,
|
||||
Ntlm,
|
||||
Gssapi,
|
||||
Anonymous,
|
||||
External,
|
||||
OAuthBearer,
|
||||
XOauth2,
|
||||
}
|
||||
|
||||
impl Mechanism {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(match self {
|
||||
Mechanism::Plain => b"PLAIN",
|
||||
Mechanism::CramMd5 => b"CRAM-MD5",
|
||||
Mechanism::DigestMd5 => b"DIGEST-MD5",
|
||||
Mechanism::ScramSha1 => b"SCRAM-SHA-1",
|
||||
Mechanism::ScramSha256 => b"SCRAM-SHA-256",
|
||||
Mechanism::Apop => b"APOP",
|
||||
Mechanism::Ntlm => b"NTLM",
|
||||
Mechanism::Gssapi => b"GSSAPI",
|
||||
Mechanism::Anonymous => b"ANONYMOUS",
|
||||
Mechanism::External => b"EXTERNAL",
|
||||
Mechanism::OAuthBearer => b"OAUTHBEARER",
|
||||
Mechanism::XOauth2 => b"XOAUTH2",
|
||||
});
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(10);
|
||||
self.serialize(&mut buf);
|
||||
buf
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapResponse, authenticate::Mechanism};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Response {
|
||||
pub capabilities: Vec<Capability>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Capability {
|
||||
IMAP4rev2,
|
||||
IMAP4rev1,
|
||||
StartTLS,
|
||||
LoginDisabled,
|
||||
Idle,
|
||||
Namespace,
|
||||
Id,
|
||||
Rights,
|
||||
Children,
|
||||
MultiAppend,
|
||||
Binary,
|
||||
Unselect,
|
||||
ACL,
|
||||
UIDPlus,
|
||||
ESearch,
|
||||
SASLIR, //SASL-IR
|
||||
Within,
|
||||
Enable,
|
||||
SearchRes,
|
||||
Sort,
|
||||
Thread, //THREAD=REFERENCES
|
||||
ListExtended, //LIST-EXTENDED
|
||||
ListStatus, //LIST-STATUS
|
||||
ESort,
|
||||
SortDisplay, //SORT=DISPLAY
|
||||
SpecialUse, //SPECIAL-USE
|
||||
CreateSpecialUse, //CREATE-SPECIAL-USEE
|
||||
Move,
|
||||
CondStore,
|
||||
QResync,
|
||||
LiteralPlus, //LITERAL+
|
||||
UnAuthenticate,
|
||||
StatusSize, //STATUS=SIZE
|
||||
ObjectIdPlus,
|
||||
Preview,
|
||||
Utf8Accept,
|
||||
Auth(Mechanism),
|
||||
Quota,
|
||||
QuotaResource(QuotaResourceName),
|
||||
QuotaSet,
|
||||
JmapAccess,
|
||||
UidOnly,
|
||||
UidBatches,
|
||||
MessageLimit(u32),
|
||||
SaveLimit(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum QuotaResourceName {
|
||||
Storage,
|
||||
Message,
|
||||
Mailbox,
|
||||
AnnotationStorage,
|
||||
}
|
||||
|
||||
impl Capability {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(match self {
|
||||
Capability::Auth(mechanism) => {
|
||||
buf.extend_from_slice(b"AUTH=");
|
||||
mechanism.serialize(buf);
|
||||
return;
|
||||
}
|
||||
Capability::IMAP4rev2 => b"IMAP4rev2",
|
||||
Capability::IMAP4rev1 => b"IMAP4rev1",
|
||||
Capability::StartTLS => b"STARTTLS",
|
||||
Capability::LoginDisabled => b"LOGINDISABLED",
|
||||
Capability::CondStore => b"CONDSTORE",
|
||||
Capability::QResync => b"QRESYNC",
|
||||
Capability::LiteralPlus => b"LITERAL+",
|
||||
Capability::UnAuthenticate => b"UNAUTHENTICATE",
|
||||
Capability::StatusSize => b"STATUS=SIZE",
|
||||
Capability::ObjectIdPlus => b"OBJECTID+",
|
||||
Capability::Preview => b"PREVIEW",
|
||||
Capability::Idle => b"IDLE",
|
||||
Capability::Namespace => b"NAMESPACE",
|
||||
Capability::Id => b"ID",
|
||||
Capability::Children => b"CHILDREN",
|
||||
Capability::MultiAppend => b"MULTIAPPEND",
|
||||
Capability::Binary => b"BINARY",
|
||||
Capability::Unselect => b"UNSELECT",
|
||||
Capability::ACL => b"ACL",
|
||||
Capability::Rights => b"RIGHTS=texk",
|
||||
Capability::UIDPlus => b"UIDPLUS",
|
||||
Capability::ESearch => b"ESEARCH",
|
||||
Capability::SASLIR => b"SASL-IR",
|
||||
Capability::Within => b"WITHIN",
|
||||
Capability::Enable => b"ENABLE",
|
||||
Capability::SearchRes => b"SEARCHRES",
|
||||
Capability::Sort => b"SORT",
|
||||
Capability::Thread => b"THREAD=REFERENCES",
|
||||
Capability::ListExtended => b"LIST-EXTENDED",
|
||||
Capability::ListStatus => b"LIST-STATUS",
|
||||
Capability::ESort => b"ESORT",
|
||||
Capability::SortDisplay => b"SORT=DISPLAY",
|
||||
Capability::SpecialUse => b"SPECIAL-USE",
|
||||
Capability::CreateSpecialUse => b"CREATE-SPECIAL-USE",
|
||||
Capability::Move => b"MOVE",
|
||||
Capability::Utf8Accept => b"UTF8=ACCEPT",
|
||||
Capability::Quota => b"QUOTA",
|
||||
Capability::QuotaResource(quota_resource) => {
|
||||
buf.extend_from_slice(b"QUOTA=RES-");
|
||||
buf.extend_from_slice(match quota_resource {
|
||||
QuotaResourceName::Storage => b"STORAGE",
|
||||
QuotaResourceName::Message => b"MESSAGE",
|
||||
QuotaResourceName::Mailbox => b"MAILBOX",
|
||||
QuotaResourceName::AnnotationStorage => b"ANNOTATION-STORAGE",
|
||||
});
|
||||
return;
|
||||
}
|
||||
Capability::QuotaSet => b"QUOTA=SET",
|
||||
Capability::JmapAccess => b"JMAPACCESS",
|
||||
Capability::UidOnly => b"UIDONLY",
|
||||
Capability::UidBatches => b"UIDBATCHES",
|
||||
Capability::MessageLimit(limit) => {
|
||||
buf.extend_from_slice(b"MESSAGELIMIT=");
|
||||
buf.extend_from_slice(limit.to_string().as_bytes());
|
||||
return;
|
||||
}
|
||||
Capability::SaveLimit(limit) => {
|
||||
buf.extend_from_slice(b"SAVELIMIT=");
|
||||
buf.extend_from_slice(limit.to_string().as_bytes());
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn all_capabilities(
|
||||
is_authenticated: bool,
|
||||
offer_tls: bool,
|
||||
allow_auth: bool,
|
||||
message_limit: u32,
|
||||
save_limit: u32,
|
||||
) -> Vec<Capability> {
|
||||
let mut capabilities = vec![
|
||||
Capability::IMAP4rev2,
|
||||
Capability::IMAP4rev1,
|
||||
Capability::Enable,
|
||||
Capability::SASLIR,
|
||||
Capability::LiteralPlus,
|
||||
Capability::Id,
|
||||
Capability::Utf8Accept,
|
||||
];
|
||||
|
||||
if is_authenticated {
|
||||
capabilities.extend([
|
||||
Capability::JmapAccess,
|
||||
Capability::Idle,
|
||||
Capability::Namespace,
|
||||
Capability::Children,
|
||||
Capability::MultiAppend,
|
||||
Capability::Binary,
|
||||
Capability::Unselect,
|
||||
Capability::ACL,
|
||||
Capability::UIDPlus,
|
||||
Capability::ESearch,
|
||||
Capability::Within,
|
||||
Capability::SearchRes,
|
||||
Capability::Sort,
|
||||
Capability::Thread,
|
||||
Capability::ListExtended,
|
||||
Capability::ListStatus,
|
||||
Capability::ESort,
|
||||
Capability::SortDisplay,
|
||||
Capability::SpecialUse,
|
||||
Capability::CreateSpecialUse,
|
||||
Capability::Move,
|
||||
Capability::CondStore,
|
||||
Capability::QResync,
|
||||
Capability::UnAuthenticate,
|
||||
Capability::StatusSize,
|
||||
Capability::ObjectIdPlus,
|
||||
Capability::Preview,
|
||||
Capability::Rights,
|
||||
Capability::Quota,
|
||||
Capability::QuotaResource(QuotaResourceName::Storage),
|
||||
Capability::UidOnly,
|
||||
Capability::UidBatches,
|
||||
Capability::MessageLimit(message_limit),
|
||||
Capability::SaveLimit(save_limit),
|
||||
]);
|
||||
} else if allow_auth {
|
||||
capabilities.extend([
|
||||
Capability::Auth(Mechanism::Plain),
|
||||
Capability::Auth(Mechanism::OAuthBearer),
|
||||
Capability::Auth(Mechanism::XOauth2),
|
||||
]);
|
||||
} else {
|
||||
capabilities.push(Capability::LoginDisabled);
|
||||
}
|
||||
if offer_tls {
|
||||
capabilities.push(Capability::StartTLS);
|
||||
}
|
||||
|
||||
capabilities
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapResponse for Response {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(64);
|
||||
buf.extend_from_slice(b"* CAPABILITY");
|
||||
for capability in self.capabilities.iter() {
|
||||
buf.push(b' ');
|
||||
capability.serialize(&mut buf);
|
||||
}
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::protocol::{
|
||||
ImapResponse,
|
||||
capability::{Capability, Response},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn serialize_capability() {
|
||||
assert_eq!(
|
||||
&Response {
|
||||
capabilities: vec![
|
||||
Capability::IMAP4rev2,
|
||||
Capability::StartTLS,
|
||||
Capability::LoginDisabled
|
||||
],
|
||||
}
|
||||
.serialize(),
|
||||
"* CAPABILITY IMAP4rev2 STARTTLS LOGINDISABLED\r\n".as_bytes()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::Sequence;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub sequence_set: Sequence,
|
||||
pub mailbox_name: String,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::list::Attribute;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub mailbox_name: String,
|
||||
pub mailbox_role: Option<Attribute>,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub mailbox_name: String,
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapResponse, capability::Capability};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub capabilities: Vec<Capability>,
|
||||
}
|
||||
|
||||
pub struct Response {
|
||||
pub enabled: Vec<Capability>,
|
||||
}
|
||||
|
||||
impl ImapResponse for Response {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
if !self.enabled.is_empty() {
|
||||
let mut buf = Vec::with_capacity(64);
|
||||
buf.extend(b"* ENABLED");
|
||||
for capability in self.enabled {
|
||||
buf.push(b' ');
|
||||
capability.serialize(&mut buf);
|
||||
}
|
||||
buf.push(b'\r');
|
||||
buf.push(b'\n');
|
||||
buf
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapResponse, serialize_sequence};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Response {
|
||||
pub use_vanished: bool,
|
||||
pub ids: Vec<u32>,
|
||||
}
|
||||
|
||||
impl ImapResponse for Response {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(64);
|
||||
self.serialize_to(&mut buf);
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn serialize_to(self, buf: &mut Vec<u8>) {
|
||||
if !self.use_vanished {
|
||||
for (num_deletions, id) in self.ids.into_iter().enumerate() {
|
||||
buf.extend_from_slice(b"* ");
|
||||
buf.extend_from_slice(
|
||||
id.saturating_sub(num_deletions as u32)
|
||||
.to_string()
|
||||
.as_bytes(),
|
||||
);
|
||||
buf.extend_from_slice(b" EXPUNGE\r\n");
|
||||
}
|
||||
} else {
|
||||
Vanished {
|
||||
earlier: false,
|
||||
ids: self.ids,
|
||||
}
|
||||
.serialize(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Vanished {
|
||||
pub earlier: bool,
|
||||
pub ids: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Vanished {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
if self.earlier {
|
||||
buf.extend_from_slice(b"* VANISHED (EARLIER) ");
|
||||
} else {
|
||||
buf.extend_from_slice(b"* VANISHED ");
|
||||
}
|
||||
serialize_sequence(buf, &self.ids);
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::protocol::ImapResponse;
|
||||
|
||||
#[test]
|
||||
fn serialize_expunge() {
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
super::Response {
|
||||
use_vanished: false,
|
||||
ids: vec![3, 4, 5]
|
||||
}
|
||||
.serialize()
|
||||
)
|
||||
.unwrap(),
|
||||
concat!("* 3 EXPUNGE\r\n", "* 3 EXPUNGE\r\n", "* 3 EXPUNGE\r\n",)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
super::Response {
|
||||
use_vanished: false,
|
||||
ids: vec![3, 4, 7, 9, 11]
|
||||
}
|
||||
.serialize()
|
||||
)
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"* 3 EXPUNGE\r\n",
|
||||
"* 3 EXPUNGE\r\n",
|
||||
"* 5 EXPUNGE\r\n",
|
||||
"* 6 EXPUNGE\r\n",
|
||||
"* 7 EXPUNGE\r\n",
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
super::Response {
|
||||
use_vanished: true,
|
||||
ids: vec![3, 4, 5]
|
||||
}
|
||||
.serialize()
|
||||
)
|
||||
.unwrap(),
|
||||
"* VANISHED 3:5\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,414 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::utf7::utf7_encode;
|
||||
|
||||
use super::{
|
||||
ImapResponse, quoted_string,
|
||||
status::{Status, StatusItem},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Arguments {
|
||||
Basic {
|
||||
tag: String,
|
||||
reference_name: String,
|
||||
mailbox_name: String,
|
||||
},
|
||||
Extended {
|
||||
tag: String,
|
||||
reference_name: String,
|
||||
mailbox_name: Vec<String>,
|
||||
selection_options: Vec<SelectionOption>,
|
||||
return_options: Vec<ReturnOption>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Response {
|
||||
pub is_rev2: bool,
|
||||
pub is_utf8: bool,
|
||||
pub is_lsub: bool,
|
||||
pub list_items: Vec<ListItem>,
|
||||
pub status_items: Vec<StatusItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum SelectionOption {
|
||||
Subscribed,
|
||||
Remote,
|
||||
RecursiveMatch,
|
||||
SpecialUse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ReturnOption {
|
||||
Subscribed,
|
||||
Children,
|
||||
Status(Vec<Status>),
|
||||
SpecialUse,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Attribute {
|
||||
NoInferiors,
|
||||
NoSelect,
|
||||
Marked,
|
||||
Unmarked,
|
||||
NonExistent,
|
||||
HasChildren,
|
||||
HasNoChildren,
|
||||
Subscribed,
|
||||
Remote,
|
||||
All,
|
||||
Archive,
|
||||
Drafts,
|
||||
Flagged,
|
||||
Junk,
|
||||
Sent,
|
||||
Trash,
|
||||
Important,
|
||||
Memos,
|
||||
Scheduled,
|
||||
Snoozed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ChildInfo {
|
||||
Subscribed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Tag {
|
||||
ChildInfo(Vec<ChildInfo>),
|
||||
OldName(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ListItem {
|
||||
pub mailbox_name: String,
|
||||
pub attributes: Vec<Attribute>,
|
||||
pub tags: Vec<Tag>,
|
||||
}
|
||||
|
||||
impl Arguments {
|
||||
pub fn is_separator_query(&self) -> bool {
|
||||
match self {
|
||||
Arguments::Basic {
|
||||
mailbox_name,
|
||||
reference_name,
|
||||
..
|
||||
} => mailbox_name.is_empty() && reference_name.is_empty(),
|
||||
Arguments::Extended {
|
||||
mailbox_name,
|
||||
reference_name,
|
||||
..
|
||||
} => mailbox_name.is_empty() && reference_name.is_empty(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unwrap_tag(self) -> String {
|
||||
match self {
|
||||
Arguments::Basic { tag, .. } => tag,
|
||||
Arguments::Extended { tag, .. } => tag,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Attribute {
|
||||
pub fn is_rev1(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Attribute::NoInferiors | Attribute::NoSelect | Attribute::Marked | Attribute::Unmarked
|
||||
)
|
||||
}
|
||||
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(match self {
|
||||
Attribute::NoInferiors => b"\\NoInferiors",
|
||||
Attribute::NoSelect => b"\\NoSelect",
|
||||
Attribute::Marked => b"\\Marked",
|
||||
Attribute::Unmarked => b"\\Unmarked",
|
||||
Attribute::NonExistent => b"\\NonExistent",
|
||||
Attribute::HasChildren => b"\\HasChildren",
|
||||
Attribute::HasNoChildren => b"\\HasNoChildren",
|
||||
Attribute::Subscribed => b"\\Subscribed",
|
||||
Attribute::Remote => b"\\Remote",
|
||||
Attribute::All => b"\\All",
|
||||
Attribute::Archive => b"\\Archive",
|
||||
Attribute::Drafts => b"\\Drafts",
|
||||
Attribute::Flagged => b"\\Flagged",
|
||||
Attribute::Junk => b"\\Junk",
|
||||
Attribute::Sent => b"\\Sent",
|
||||
Attribute::Trash => b"\\Trash",
|
||||
Attribute::Important => b"\\Important",
|
||||
Attribute::Memos => b"\\Memos",
|
||||
Attribute::Scheduled => b"\\Scheduled",
|
||||
Attribute::Snoozed => b"\\Snoozed",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Attribute {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
hashify::tiny_map!(value.as_bytes(),
|
||||
"archive" => Attribute::Archive,
|
||||
"drafts" => Attribute::Drafts,
|
||||
"junk" => Attribute::Junk,
|
||||
"sent" => Attribute::Sent,
|
||||
"trash" => Attribute::Trash,
|
||||
"important" => Attribute::Important,
|
||||
"memos" => Attribute::Memos,
|
||||
"scheduled" => Attribute::Scheduled,
|
||||
"snoozed" => Attribute::Snoozed,
|
||||
)
|
||||
.ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ChildInfo {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.push(b'\"');
|
||||
buf.extend_from_slice(match self {
|
||||
ChildInfo::Subscribed => b"SUBSCRIBED",
|
||||
});
|
||||
buf.push(b'\"');
|
||||
}
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
match self {
|
||||
Tag::ChildInfo(child_info) => {
|
||||
buf.extend_from_slice(b"\"CHILDINFO\" (");
|
||||
for (pos, child_info) in child_info.iter().enumerate() {
|
||||
if pos > 0 {
|
||||
buf.push(b' ');
|
||||
}
|
||||
child_info.serialize(buf);
|
||||
}
|
||||
buf.push(b')');
|
||||
}
|
||||
Tag::OldName(old_name) => {
|
||||
buf.extend_from_slice(b"\"OLDNAME\" (");
|
||||
quoted_string(buf, old_name);
|
||||
buf.push(b')');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
pub fn new(name: impl Into<String>) -> Self {
|
||||
ListItem {
|
||||
mailbox_name: name.into(),
|
||||
attributes: Vec::new(),
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>, is_rev2: bool, is_utf8: bool, is_lsub: bool) {
|
||||
let normalized_mailbox_name = utf7_encode(&self.mailbox_name);
|
||||
if !is_lsub {
|
||||
buf.extend_from_slice(b"* LIST (");
|
||||
} else {
|
||||
buf.extend_from_slice(b"* LSUB (");
|
||||
}
|
||||
for (pos, attr) in self.attributes.iter().enumerate() {
|
||||
if pos > 0 {
|
||||
buf.push(b' ');
|
||||
}
|
||||
attr.serialize(buf);
|
||||
}
|
||||
buf.extend_from_slice(b") \"/\" ");
|
||||
let mut extra_tags = Vec::new();
|
||||
|
||||
if normalized_mailbox_name != self.mailbox_name {
|
||||
if is_rev2 || is_utf8 {
|
||||
quoted_string(buf, &self.mailbox_name);
|
||||
if is_rev2 {
|
||||
extra_tags.push(Tag::OldName(normalized_mailbox_name));
|
||||
}
|
||||
} else {
|
||||
quoted_string(buf, &normalized_mailbox_name);
|
||||
}
|
||||
} else {
|
||||
quoted_string(buf, &self.mailbox_name);
|
||||
}
|
||||
|
||||
if !extra_tags.is_empty() || !self.tags.is_empty() {
|
||||
buf.extend_from_slice(b" (");
|
||||
for (pos, tag) in extra_tags.iter().chain(self.tags.iter()).enumerate() {
|
||||
if pos > 0 {
|
||||
buf.push(b' ');
|
||||
}
|
||||
tag.serialize(buf);
|
||||
}
|
||||
buf.extend_from_slice(b")\r\n");
|
||||
} else {
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapResponse for Response {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(100);
|
||||
|
||||
match (self.list_items.is_empty(), self.status_items.is_empty()) {
|
||||
(false, false) => {
|
||||
for (list_item, status_item) in self.list_items.iter().zip(self.status_items.iter())
|
||||
{
|
||||
list_item.serialize(&mut buf, self.is_rev2, self.is_utf8, self.is_lsub);
|
||||
status_item.serialize(&mut buf, self.is_rev2);
|
||||
}
|
||||
}
|
||||
(false, true) => {
|
||||
for list_item in &self.list_items {
|
||||
list_item.serialize(&mut buf, self.is_rev2, self.is_utf8, self.is_lsub);
|
||||
}
|
||||
}
|
||||
(true, false) => {
|
||||
for status_item in &self.status_items {
|
||||
status_item.serialize(&mut buf, self.is_rev2);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::protocol::{
|
||||
ImapResponse,
|
||||
status::{Status, StatusItem, StatusItemType},
|
||||
};
|
||||
|
||||
use super::{Attribute, ChildInfo, ListItem, Tag};
|
||||
|
||||
#[test]
|
||||
fn serialize_list_item() {
|
||||
for (response, expected_v2, expected_v1) in [
|
||||
(
|
||||
super::ListItem {
|
||||
mailbox_name: "".into(),
|
||||
attributes: vec![],
|
||||
tags: vec![],
|
||||
},
|
||||
"* LIST () \"/\" \"\"\r\n",
|
||||
"* LIST () \"/\" \"\"\r\n",
|
||||
),
|
||||
(
|
||||
super::ListItem {
|
||||
mailbox_name: "中國書店".into(),
|
||||
attributes: vec![Attribute::NoInferiors, Attribute::Drafts],
|
||||
tags: vec![],
|
||||
},
|
||||
concat!(
|
||||
"* LIST (\\NoInferiors \\Drafts) \"/\" \"中國書店\" ",
|
||||
"(\"OLDNAME\" (\"&Ti1XC2b4Xpc-\"))\r\n"
|
||||
),
|
||||
"* LIST (\\NoInferiors \\Drafts) \"/\" \"&Ti1XC2b4Xpc-\"\r\n",
|
||||
),
|
||||
(
|
||||
super::ListItem {
|
||||
mailbox_name: "☺".into(),
|
||||
attributes: vec![Attribute::Subscribed, Attribute::Remote],
|
||||
tags: vec![Tag::ChildInfo(vec![ChildInfo::Subscribed])],
|
||||
},
|
||||
concat!(
|
||||
"* LIST (\\Subscribed \\Remote) \"/\" \"☺\" ",
|
||||
"(\"OLDNAME\" (\"&Jjo-\") \"CHILDINFO\" (\"SUBSCRIBED\"))\r\n"
|
||||
),
|
||||
concat!(
|
||||
"* LIST (\\Subscribed \\Remote) \"/\" \"&Jjo-\" ",
|
||||
"(\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n"
|
||||
),
|
||||
),
|
||||
(
|
||||
super::ListItem {
|
||||
mailbox_name: "foo".into(),
|
||||
attributes: vec![Attribute::HasNoChildren],
|
||||
tags: vec![Tag::ChildInfo(vec![ChildInfo::Subscribed])],
|
||||
},
|
||||
"* LIST (\\HasNoChildren) \"/\" \"foo\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n",
|
||||
"* LIST (\\HasNoChildren) \"/\" \"foo\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n",
|
||||
),
|
||||
] {
|
||||
let mut buf_1 = Vec::with_capacity(100);
|
||||
let mut buf_2 = Vec::with_capacity(100);
|
||||
|
||||
response.serialize(&mut buf_1, false, false, false);
|
||||
response.serialize(&mut buf_2, true, true, false);
|
||||
|
||||
let response_v1 = String::from_utf8(buf_1).unwrap();
|
||||
let response_v2 = String::from_utf8(buf_2).unwrap();
|
||||
|
||||
assert_eq!(response_v2, expected_v2);
|
||||
assert_eq!(response_v1, expected_v1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialize_list() {
|
||||
let mut response = super::Response {
|
||||
list_items: vec![
|
||||
ListItem {
|
||||
mailbox_name: "INBOX".into(),
|
||||
attributes: vec![Attribute::Subscribed],
|
||||
tags: vec![],
|
||||
},
|
||||
ListItem {
|
||||
mailbox_name: "foo".into(),
|
||||
attributes: vec![],
|
||||
tags: vec![Tag::ChildInfo(vec![ChildInfo::Subscribed])],
|
||||
},
|
||||
],
|
||||
status_items: vec![
|
||||
StatusItem {
|
||||
mailbox_name: "INBOX".into(),
|
||||
items: vec![(Status::Messages, StatusItemType::Number(17))],
|
||||
},
|
||||
StatusItem {
|
||||
mailbox_name: "foo".into(),
|
||||
items: vec![
|
||||
(Status::Messages, StatusItemType::Number(30)),
|
||||
(Status::Unseen, StatusItemType::Number(29)),
|
||||
],
|
||||
},
|
||||
],
|
||||
is_lsub: false,
|
||||
is_rev2: true,
|
||||
is_utf8: true,
|
||||
};
|
||||
let expected_v2 = concat!(
|
||||
"* LIST (\\Subscribed) \"/\" \"INBOX\"\r\n",
|
||||
"* STATUS \"INBOX\" (MESSAGES 17)\r\n",
|
||||
"* LIST () \"/\" \"foo\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n",
|
||||
"* STATUS \"foo\" (MESSAGES 30 UNSEEN 29)\r\n",
|
||||
);
|
||||
let expected_v1 = concat!(
|
||||
"* LSUB (\\Subscribed) \"/\" \"INBOX\"\r\n",
|
||||
"* LSUB () \"/\" \"foo\" (\"CHILDINFO\" (\"SUBSCRIBED\"))\r\n",
|
||||
);
|
||||
|
||||
let response_v2 = String::from_utf8(response.clone().serialize()).unwrap();
|
||||
response.is_rev2 = false;
|
||||
response.is_utf8 = false;
|
||||
response.is_lsub = true;
|
||||
response.status_items.clear();
|
||||
let response_v1 = String::from_utf8(response.serialize()).unwrap();
|
||||
|
||||
assert_eq!(response_v2, expected_v2);
|
||||
assert_eq!(response_v1, expected_v1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
@@ -0,0 +1,879 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{Command, ResponseCode, ResponseType, StatusResponse};
|
||||
use ahash::AHashSet;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use chrono::{DateTime, Utc};
|
||||
use compact_str::CompactString;
|
||||
use std::{cmp::Ordering, fmt::Display};
|
||||
use types::id::Id;
|
||||
use types::keyword::{ArchivedKeyword, Keyword};
|
||||
use utils::chained_bytes::SliceRange;
|
||||
|
||||
pub mod acl;
|
||||
pub mod append;
|
||||
pub mod authenticate;
|
||||
pub mod capability;
|
||||
pub mod copy_move;
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod enable;
|
||||
pub mod expunge;
|
||||
pub mod fetch;
|
||||
pub mod list;
|
||||
pub mod login;
|
||||
pub mod namespace;
|
||||
pub mod quota;
|
||||
pub mod rename;
|
||||
pub mod search;
|
||||
pub mod select;
|
||||
pub mod status;
|
||||
pub mod store;
|
||||
pub mod subscribe;
|
||||
pub mod thread;
|
||||
pub mod uidbatches;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ProtocolVersion {
|
||||
Rev1,
|
||||
Rev2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct ObjectId {
|
||||
pub mailbox_id: Option<Id>,
|
||||
pub account_id: Option<Id>,
|
||||
pub email_id: Option<Id>,
|
||||
pub thread_id: Option<Id>,
|
||||
}
|
||||
|
||||
impl ObjectId {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.mailbox_id.is_none()
|
||||
&& self.account_id.is_none()
|
||||
&& self.email_id.is_none()
|
||||
&& self.thread_id.is_none()
|
||||
}
|
||||
|
||||
pub fn serialize_kvpairs(&self, buf: &mut Vec<u8>) {
|
||||
buf.push(b'(');
|
||||
let mut first = true;
|
||||
for (key, value) in [
|
||||
(&b"ACCOUNTID "[..], &self.account_id),
|
||||
(&b"MAILBOXID "[..], &self.mailbox_id),
|
||||
(&b"EMAILID "[..], &self.email_id),
|
||||
(&b"THREADID "[..], &self.thread_id),
|
||||
] {
|
||||
if let Some(value) = value {
|
||||
if !first {
|
||||
buf.push(b' ');
|
||||
}
|
||||
first = false;
|
||||
buf.extend_from_slice(key);
|
||||
buf.extend_from_slice(value.to_string().as_bytes());
|
||||
}
|
||||
}
|
||||
buf.push(b')');
|
||||
}
|
||||
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(b"OBJECTID ");
|
||||
self.serialize_kvpairs(buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Sequence {
|
||||
Number {
|
||||
value: u32,
|
||||
},
|
||||
Range {
|
||||
start: Option<u32>,
|
||||
end: Option<u32>,
|
||||
},
|
||||
SavedSearch,
|
||||
List {
|
||||
items: Vec<Sequence>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Sequence {
|
||||
pub fn number(value: u32) -> Sequence {
|
||||
Sequence::Number { value }
|
||||
}
|
||||
|
||||
pub fn range(start: Option<u32>, end: Option<u32>) -> Sequence {
|
||||
Sequence::Range { start, end }
|
||||
}
|
||||
|
||||
pub fn contains(&self, value: u32, max_value: u32) -> bool {
|
||||
match self {
|
||||
Sequence::Number { value: number } => *number == value,
|
||||
Sequence::Range { start, end } => match (start, end) {
|
||||
(Some(start), Some(end)) => {
|
||||
value >= *start && value <= *end || value >= *end && value <= *start
|
||||
}
|
||||
(Some(range), None) | (None, Some(range)) => {
|
||||
value >= *range && value <= max_value || value >= max_value && value <= *range
|
||||
}
|
||||
(None, None) => value == max_value,
|
||||
},
|
||||
Sequence::List { items } => {
|
||||
for item in items {
|
||||
if item.contains(value, max_value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
Sequence::SavedSearch => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_saved_search(&self) -> bool {
|
||||
match self {
|
||||
Sequence::SavedSearch => true,
|
||||
Sequence::List { items } => items.iter().any(|s| s.is_saved_search()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand(&self, max_value: u32) -> AHashSet<u32> {
|
||||
match self {
|
||||
Sequence::Number { value } => AHashSet::from_iter([*value]),
|
||||
Sequence::List { items } => {
|
||||
let mut result = AHashSet::with_capacity(items.len());
|
||||
for item in items {
|
||||
match item {
|
||||
Sequence::Number { value } => {
|
||||
result.insert(*value);
|
||||
}
|
||||
Sequence::Range { start, end } => {
|
||||
let start = start.unwrap_or(max_value);
|
||||
let end = end.unwrap_or(max_value);
|
||||
match start.cmp(&end) {
|
||||
Ordering::Equal => {
|
||||
result.insert(start);
|
||||
}
|
||||
Ordering::Less => {
|
||||
result.extend(start..=end);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
result.extend(end..=start);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
Sequence::Range { start, end } => {
|
||||
let mut result = AHashSet::new();
|
||||
let start = start.unwrap_or(max_value);
|
||||
let end = end.unwrap_or(max_value);
|
||||
match start.cmp(&end) {
|
||||
Ordering::Equal => {
|
||||
result.insert(start);
|
||||
}
|
||||
Ordering::Less => {
|
||||
result.extend(start..=end);
|
||||
}
|
||||
Ordering::Greater => {
|
||||
result.extend(end..=start);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
_ => AHashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ImapResponse {
|
||||
fn serialize(self) -> Vec<u8>;
|
||||
}
|
||||
|
||||
pub fn quoted_string(buf: &mut Vec<u8>, text: &str) {
|
||||
buf.push(b'"');
|
||||
for &c in text.as_bytes() {
|
||||
if c == b'\\' || c == b'"' {
|
||||
buf.push(b'\\');
|
||||
}
|
||||
buf.push(c);
|
||||
}
|
||||
buf.push(b'"');
|
||||
}
|
||||
|
||||
pub fn quoted_or_literal_string(buf: &mut Vec<u8>, text: &str) {
|
||||
if text.as_bytes().iter().any(|ch| b"\\\"\r\n".contains(ch)) {
|
||||
literal_string(buf, text.as_bytes())
|
||||
} else {
|
||||
buf.push(b'"');
|
||||
buf.extend_from_slice(text.as_bytes());
|
||||
buf.push(b'"');
|
||||
}
|
||||
}
|
||||
pub fn quoted_or_literal_string_or_nil(buf: &mut Vec<u8>, text: Option<&str>) {
|
||||
if let Some(text) = text {
|
||||
quoted_or_literal_string(buf, text);
|
||||
} else {
|
||||
buf.extend_from_slice(b"NIL");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quoted_or_literal_encoded_string(buf: &mut Vec<u8>, text: &str, is_utf8: bool) {
|
||||
if is_utf8 || text.is_ascii() {
|
||||
quoted_or_literal_string(buf, text);
|
||||
} else {
|
||||
buf.extend_from_slice(b"\"=?utf-8?B?");
|
||||
buf.extend_from_slice(STANDARD.encode(text.as_bytes()).as_bytes());
|
||||
buf.extend_from_slice(b"?=\"");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quoted_or_literal_encoded_string_or_nil(
|
||||
buf: &mut Vec<u8>,
|
||||
text: Option<&str>,
|
||||
is_utf8: bool,
|
||||
) {
|
||||
if let Some(text) = text {
|
||||
quoted_or_literal_encoded_string(buf, text, is_utf8);
|
||||
} else {
|
||||
buf.extend_from_slice(b"NIL");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn quoted_string_or_nil(buf: &mut Vec<u8>, text: Option<&str>) {
|
||||
if let Some(text) = text {
|
||||
quoted_string(buf, text);
|
||||
} else {
|
||||
buf.extend_from_slice(b"NIL");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn literal_string(buf: &mut Vec<u8>, text: &[u8]) {
|
||||
buf.push(b'{');
|
||||
buf.extend_from_slice(text.len().to_string().as_bytes());
|
||||
buf.extend_from_slice(b"}\r\n");
|
||||
buf.extend_from_slice(text);
|
||||
}
|
||||
|
||||
pub fn literal_string_slice(buf: &mut Vec<u8>, text: &SliceRange<'_>) {
|
||||
buf.push(b'{');
|
||||
buf.extend_from_slice(text.len().to_string().as_bytes());
|
||||
buf.extend_from_slice(b"}\r\n");
|
||||
buf.extend(*text);
|
||||
}
|
||||
|
||||
pub fn quoted_timestamp(buf: &mut Vec<u8>, timestamp: i64) {
|
||||
buf.push(b'"');
|
||||
buf.extend_from_slice(
|
||||
DateTime::<Utc>::from_timestamp(timestamp, 0)
|
||||
.unwrap_or_default()
|
||||
.format("%d-%b-%Y %H:%M:%S %z")
|
||||
.to_string()
|
||||
.as_bytes(),
|
||||
);
|
||||
buf.push(b'"');
|
||||
}
|
||||
|
||||
pub fn quoted_rfc2822(buf: &mut Vec<u8>, timestamp: &mail_parser::DateTime) {
|
||||
buf.push(b'"');
|
||||
buf.extend_from_slice(timestamp.to_rfc822().as_bytes());
|
||||
buf.push(b'"');
|
||||
}
|
||||
|
||||
pub fn quoted_rfc2822_or_nil(buf: &mut Vec<u8>, timestamp: &Option<mail_parser::DateTime>) {
|
||||
if let Some(timestamp) = timestamp {
|
||||
quoted_rfc2822(buf, timestamp);
|
||||
} else {
|
||||
buf.extend_from_slice(b"NIL");
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Flag {
|
||||
Seen,
|
||||
Draft,
|
||||
Flagged,
|
||||
Answered,
|
||||
Recent,
|
||||
Important,
|
||||
Phishing,
|
||||
Junk,
|
||||
NotJunk,
|
||||
Deleted,
|
||||
Forwarded,
|
||||
MDNSent,
|
||||
Autosent,
|
||||
CanUnsubscribe,
|
||||
Followed,
|
||||
HasAttachment,
|
||||
HasMemo,
|
||||
HasNoAttachment,
|
||||
Imported,
|
||||
IsTrusted,
|
||||
MailFlagBit0,
|
||||
MailFlagBit1,
|
||||
MailFlagBit2,
|
||||
MaskedEmail,
|
||||
Memo,
|
||||
Muted,
|
||||
New,
|
||||
Notify,
|
||||
Unsubscribed,
|
||||
Keyword(Box<str>),
|
||||
}
|
||||
|
||||
impl Flag {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(match self {
|
||||
Flag::Seen => b"\\Seen",
|
||||
Flag::Draft => b"\\Draft",
|
||||
Flag::Flagged => b"\\Flagged",
|
||||
Flag::Answered => b"\\Answered",
|
||||
Flag::Recent => b"\\Recent",
|
||||
Flag::Important => b"\\Important",
|
||||
Flag::Phishing => b"$Phishing",
|
||||
Flag::Junk => b"$Junk",
|
||||
Flag::NotJunk => b"$NotJunk",
|
||||
Flag::Deleted => b"\\Deleted",
|
||||
Flag::Forwarded => b"$Forwarded",
|
||||
Flag::MDNSent => b"$MDNSent",
|
||||
Flag::Autosent => b"$autosent",
|
||||
Flag::CanUnsubscribe => b"$canunsubscribe",
|
||||
Flag::Followed => b"$followed",
|
||||
Flag::HasAttachment => b"$hasattachment",
|
||||
Flag::HasMemo => b"$hasmemo",
|
||||
Flag::HasNoAttachment => b"$hasnoattachment",
|
||||
Flag::Imported => b"$imported",
|
||||
Flag::IsTrusted => b"$istrusted",
|
||||
Flag::MailFlagBit0 => b"$MailFlagBit0",
|
||||
Flag::MailFlagBit1 => b"$MailFlagBit1",
|
||||
Flag::MailFlagBit2 => b"$MailFlagBit2",
|
||||
Flag::MaskedEmail => b"$maskedemail",
|
||||
Flag::Memo => b"$memo",
|
||||
Flag::Muted => b"$muted",
|
||||
Flag::New => b"$new",
|
||||
Flag::Notify => b"$notify",
|
||||
Flag::Unsubscribed => b"$unsubscribed",
|
||||
Flag::Keyword(keyword) => keyword.as_bytes(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Keyword> for Flag {
|
||||
fn from(value: Keyword) -> Self {
|
||||
match value {
|
||||
Keyword::Seen => Flag::Seen,
|
||||
Keyword::Draft => Flag::Draft,
|
||||
Keyword::Flagged => Flag::Flagged,
|
||||
Keyword::Answered => Flag::Answered,
|
||||
Keyword::Recent => Flag::Recent,
|
||||
Keyword::Important => Flag::Important,
|
||||
Keyword::Phishing => Flag::Phishing,
|
||||
Keyword::Junk => Flag::Junk,
|
||||
Keyword::NotJunk => Flag::NotJunk,
|
||||
Keyword::Deleted => Flag::Deleted,
|
||||
Keyword::Forwarded => Flag::Forwarded,
|
||||
Keyword::MdnSent => Flag::MDNSent,
|
||||
Keyword::Autosent => Flag::Autosent,
|
||||
Keyword::CanUnsubscribe => Flag::CanUnsubscribe,
|
||||
Keyword::Followed => Flag::Followed,
|
||||
Keyword::HasAttachment => Flag::HasAttachment,
|
||||
Keyword::HasMemo => Flag::HasMemo,
|
||||
Keyword::HasNoAttachment => Flag::HasNoAttachment,
|
||||
Keyword::Imported => Flag::Imported,
|
||||
Keyword::IsTrusted => Flag::IsTrusted,
|
||||
Keyword::MailFlagBit0 => Flag::MailFlagBit0,
|
||||
Keyword::MailFlagBit1 => Flag::MailFlagBit1,
|
||||
Keyword::MailFlagBit2 => Flag::MailFlagBit2,
|
||||
Keyword::MaskedEmail => Flag::MaskedEmail,
|
||||
Keyword::Memo => Flag::Memo,
|
||||
Keyword::Muted => Flag::Muted,
|
||||
Keyword::New => Flag::New,
|
||||
Keyword::Notify => Flag::Notify,
|
||||
Keyword::Unsubscribed => Flag::Unsubscribed,
|
||||
Keyword::Other(value) => Flag::Keyword(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ArchivedKeyword> for Flag {
|
||||
fn from(value: &ArchivedKeyword) -> Self {
|
||||
match value {
|
||||
ArchivedKeyword::Seen => Flag::Seen,
|
||||
ArchivedKeyword::Draft => Flag::Draft,
|
||||
ArchivedKeyword::Flagged => Flag::Flagged,
|
||||
ArchivedKeyword::Answered => Flag::Answered,
|
||||
ArchivedKeyword::Recent => Flag::Recent,
|
||||
ArchivedKeyword::Important => Flag::Important,
|
||||
ArchivedKeyword::Phishing => Flag::Phishing,
|
||||
ArchivedKeyword::Junk => Flag::Junk,
|
||||
ArchivedKeyword::NotJunk => Flag::NotJunk,
|
||||
ArchivedKeyword::Deleted => Flag::Deleted,
|
||||
ArchivedKeyword::Forwarded => Flag::Forwarded,
|
||||
ArchivedKeyword::MdnSent => Flag::MDNSent,
|
||||
ArchivedKeyword::Autosent => Flag::Autosent,
|
||||
ArchivedKeyword::CanUnsubscribe => Flag::CanUnsubscribe,
|
||||
ArchivedKeyword::Followed => Flag::Followed,
|
||||
ArchivedKeyword::HasAttachment => Flag::HasAttachment,
|
||||
ArchivedKeyword::HasMemo => Flag::HasMemo,
|
||||
ArchivedKeyword::HasNoAttachment => Flag::HasNoAttachment,
|
||||
ArchivedKeyword::Imported => Flag::Imported,
|
||||
ArchivedKeyword::IsTrusted => Flag::IsTrusted,
|
||||
ArchivedKeyword::MailFlagBit0 => Flag::MailFlagBit0,
|
||||
ArchivedKeyword::MailFlagBit1 => Flag::MailFlagBit1,
|
||||
ArchivedKeyword::MailFlagBit2 => Flag::MailFlagBit2,
|
||||
ArchivedKeyword::MaskedEmail => Flag::MaskedEmail,
|
||||
ArchivedKeyword::Memo => Flag::Memo,
|
||||
ArchivedKeyword::Muted => Flag::Muted,
|
||||
ArchivedKeyword::New => Flag::New,
|
||||
ArchivedKeyword::Notify => Flag::Notify,
|
||||
ArchivedKeyword::Unsubscribed => Flag::Unsubscribed,
|
||||
ArchivedKeyword::Other(value) => Flag::Keyword(value.as_ref().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Flag> for Keyword {
|
||||
fn from(value: Flag) -> Self {
|
||||
match value {
|
||||
Flag::Seen => Keyword::Seen,
|
||||
Flag::Draft => Keyword::Draft,
|
||||
Flag::Flagged => Keyword::Flagged,
|
||||
Flag::Answered => Keyword::Answered,
|
||||
Flag::Recent => Keyword::Recent,
|
||||
Flag::Important => Keyword::Important,
|
||||
Flag::Phishing => Keyword::Phishing,
|
||||
Flag::Junk => Keyword::Junk,
|
||||
Flag::NotJunk => Keyword::NotJunk,
|
||||
Flag::Deleted => Keyword::Deleted,
|
||||
Flag::Forwarded => Keyword::Forwarded,
|
||||
Flag::MDNSent => Keyword::MdnSent,
|
||||
Flag::Autosent => Keyword::Autosent,
|
||||
Flag::CanUnsubscribe => Keyword::CanUnsubscribe,
|
||||
Flag::Followed => Keyword::Followed,
|
||||
Flag::HasAttachment => Keyword::HasAttachment,
|
||||
Flag::HasMemo => Keyword::HasMemo,
|
||||
Flag::HasNoAttachment => Keyword::HasNoAttachment,
|
||||
Flag::Imported => Keyword::Imported,
|
||||
Flag::IsTrusted => Keyword::IsTrusted,
|
||||
Flag::MailFlagBit0 => Keyword::MailFlagBit0,
|
||||
Flag::MailFlagBit1 => Keyword::MailFlagBit1,
|
||||
Flag::MailFlagBit2 => Keyword::MailFlagBit2,
|
||||
Flag::MaskedEmail => Keyword::MaskedEmail,
|
||||
Flag::Memo => Keyword::Memo,
|
||||
Flag::Muted => Keyword::Muted,
|
||||
Flag::New => Keyword::New,
|
||||
Flag::Notify => Keyword::Notify,
|
||||
Flag::Unsubscribed => Keyword::Unsubscribed,
|
||||
Flag::Keyword(value) => Keyword::from_boxed_other(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseCode {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(match self {
|
||||
ResponseCode::Alert => b"ALERT",
|
||||
ResponseCode::AlreadyExists => b"ALREADYEXISTS",
|
||||
ResponseCode::AppendUid { uid_validity, uids } => {
|
||||
buf.extend_from_slice(b"APPENDUID ");
|
||||
buf.extend_from_slice(uid_validity.to_string().as_bytes());
|
||||
buf.push(b' ');
|
||||
serialize_sequence(buf, uids);
|
||||
return;
|
||||
}
|
||||
ResponseCode::AuthenticationFailed => b"AUTHENTICATIONFAILED",
|
||||
ResponseCode::AuthorizationFailed => b"AUTHORIZATIONFAILED",
|
||||
ResponseCode::BadCharset => b"BADCHARSET",
|
||||
ResponseCode::Cannot => b"CANNOT",
|
||||
ResponseCode::Capability { capabilities } => {
|
||||
buf.extend_from_slice(b"CAPABILITY");
|
||||
for capability in capabilities {
|
||||
buf.push(b' ');
|
||||
capability.serialize(buf);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ResponseCode::ClientBug => b"CLIENTBUG",
|
||||
ResponseCode::Closed => b"CLOSED",
|
||||
ResponseCode::ContactAdmin => b"CONTACTADMIN",
|
||||
ResponseCode::CopyUid {
|
||||
uid_validity,
|
||||
src_uids,
|
||||
dest_uids,
|
||||
} => {
|
||||
buf.extend_from_slice(b"COPYUID ");
|
||||
buf.extend_from_slice(uid_validity.to_string().as_bytes());
|
||||
buf.push(b' ');
|
||||
serialize_sequence(buf, src_uids);
|
||||
buf.push(b' ');
|
||||
serialize_sequence(buf, dest_uids);
|
||||
return;
|
||||
}
|
||||
ResponseCode::Corruption => b"CORRUPTION",
|
||||
ResponseCode::Expired => b"EXPIRED",
|
||||
ResponseCode::ExpungeIssued => b"EXPUNGEISSUED",
|
||||
ResponseCode::HasChildren => b"HASCHILDREN",
|
||||
ResponseCode::InUse => b"INUSE",
|
||||
ResponseCode::Limit => b"LIMIT",
|
||||
ResponseCode::NonExistent => b"NONEXISTENT",
|
||||
ResponseCode::NoPerm => b"NOPERM",
|
||||
ResponseCode::OverQuota => b"OVERQUOTA",
|
||||
ResponseCode::Parse => b"PARSE",
|
||||
ResponseCode::PermanentFlags => b"PERMANENTFLAGS",
|
||||
ResponseCode::PrivacyRequired => b"PRIVACYREQUIRED",
|
||||
ResponseCode::ReadOnly => b"READ-ONLY",
|
||||
ResponseCode::ReadWrite => b"READ-WRITE",
|
||||
ResponseCode::ServerBug => b"SERVERBUG",
|
||||
ResponseCode::TryCreate => b"TRYCREATE",
|
||||
ResponseCode::UidNext => b"UIDNEXT",
|
||||
ResponseCode::UidNotSticky => b"UIDNOTSTICKY",
|
||||
ResponseCode::UidValidity => b"UIDVALIDITY",
|
||||
ResponseCode::Unavailable => b"UNAVAILABLE",
|
||||
ResponseCode::UnknownCte => b"UNKNOWN-CTE",
|
||||
ResponseCode::Modified { ids } => {
|
||||
buf.extend_from_slice(b"MODIFIED ");
|
||||
serialize_sequence(buf, ids);
|
||||
return;
|
||||
}
|
||||
ResponseCode::ObjectId(object_id) => {
|
||||
object_id.serialize(buf);
|
||||
return;
|
||||
}
|
||||
ResponseCode::HighestModseq { modseq } => {
|
||||
buf.extend_from_slice(b"HIGHESTMODSEQ ");
|
||||
buf.extend_from_slice(modseq.to_string().as_bytes());
|
||||
return;
|
||||
}
|
||||
ResponseCode::UseAttr => b"USEATTR",
|
||||
ResponseCode::UidRequired => b"UIDREQUIRED",
|
||||
ResponseCode::TooFew => b"TOOFEW",
|
||||
ResponseCode::TooMany => b"TOOMANY",
|
||||
ResponseCode::MessageLimit { limit, uid } => {
|
||||
buf.extend_from_slice(b"MESSAGELIMIT ");
|
||||
buf.extend_from_slice(limit.to_string().as_bytes());
|
||||
if let Some(uid) = uid {
|
||||
buf.push(b' ');
|
||||
buf.extend_from_slice(uid.to_string().as_bytes());
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
// Only returns the name without arguments
|
||||
match self {
|
||||
ResponseCode::Alert => "ALERT",
|
||||
ResponseCode::AlreadyExists => "ALREADYEXISTS",
|
||||
ResponseCode::AppendUid { .. } => "APPENDUID",
|
||||
ResponseCode::AuthenticationFailed => "AUTHENTICATIONFAILED",
|
||||
ResponseCode::AuthorizationFailed => "AUTHORIZATIONFAILED",
|
||||
ResponseCode::BadCharset => "BADCHARSET",
|
||||
ResponseCode::Cannot => "CANNOT",
|
||||
ResponseCode::Capability { .. } => "CAPABILITY",
|
||||
ResponseCode::ClientBug => "CLIENTBUG",
|
||||
ResponseCode::Closed => "CLOSED",
|
||||
ResponseCode::ContactAdmin => "CONTACTADMIN",
|
||||
ResponseCode::CopyUid { .. } => "COPYUID",
|
||||
ResponseCode::Corruption => "CORRUPTION",
|
||||
ResponseCode::Expired => "EXPIRED",
|
||||
ResponseCode::ExpungeIssued => "EXPUNGEISSUED",
|
||||
ResponseCode::HasChildren => "HASCHILDREN",
|
||||
ResponseCode::InUse => "INUSE",
|
||||
ResponseCode::Limit => "LIMIT",
|
||||
ResponseCode::NonExistent => "NONEXISTENT",
|
||||
ResponseCode::NoPerm => "NOPERM",
|
||||
ResponseCode::OverQuota => "OVERQUOTA",
|
||||
ResponseCode::Parse => "PARSE",
|
||||
ResponseCode::PermanentFlags => "PERMANENTFLAGS",
|
||||
ResponseCode::PrivacyRequired => "PRIVACYREQUIRED",
|
||||
ResponseCode::ReadOnly => "READ-ONLY",
|
||||
ResponseCode::ReadWrite => "READ-WRITE",
|
||||
ResponseCode::ServerBug => "SERVERBUG",
|
||||
ResponseCode::TryCreate => "TRYCREATE",
|
||||
ResponseCode::UidNext => "UIDNEXT",
|
||||
ResponseCode::UidNotSticky => "UIDNOTSTICKY",
|
||||
ResponseCode::UidValidity => "UIDVALIDITY",
|
||||
ResponseCode::Unavailable => "UNAVAILABLE",
|
||||
ResponseCode::UnknownCte => "UNKNOWN-CTE",
|
||||
ResponseCode::Modified { .. } => "MODIFIED",
|
||||
ResponseCode::ObjectId { .. } => "OBJECTID",
|
||||
ResponseCode::HighestModseq { .. } => "HIGHESTMODSEQ",
|
||||
ResponseCode::UseAttr => "USEATTR",
|
||||
ResponseCode::UidRequired => "UIDREQUIRED",
|
||||
ResponseCode::TooFew => "TOOFEW",
|
||||
ResponseCode::TooMany => "TOOMANY",
|
||||
ResponseCode::MessageLimit { .. } => "MESSAGELIMIT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseType {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(self.as_str().as_bytes());
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
ResponseType::Ok => "OK",
|
||||
ResponseType::No => "NO",
|
||||
ResponseType::Bad => "BAD",
|
||||
ResponseType::PreAuth => "PREAUTH",
|
||||
ResponseType::Bye => "BYE",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseCode> for trc::Value {
|
||||
fn from(value: ResponseCode) -> Self {
|
||||
trc::Value::String(CompactString::const_new(value.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseType> for trc::Value {
|
||||
fn from(value: ResponseType) -> Self {
|
||||
trc::Value::String(CompactString::const_new(value.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusResponse {
|
||||
pub fn serialize(self, mut buf: Vec<u8>) -> Vec<u8> {
|
||||
if let Some(tag) = &self.tag {
|
||||
buf.extend_from_slice(tag.as_bytes());
|
||||
} else {
|
||||
buf.push(b'*');
|
||||
}
|
||||
buf.push(b' ');
|
||||
self.rtype.serialize(&mut buf);
|
||||
buf.push(b' ');
|
||||
if let Some(code) = &self.code {
|
||||
buf.push(b'[');
|
||||
code.serialize(&mut buf);
|
||||
buf.extend_from_slice(b"] ");
|
||||
}
|
||||
buf.extend_from_slice(self.message.as_bytes());
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
self.serialize(Vec::with_capacity(16))
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SerializeResponse {
|
||||
fn serialize(&self) -> Vec<u8>;
|
||||
}
|
||||
|
||||
impl SerializeResponse for trc::Error {
|
||||
fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(128);
|
||||
if let Some(tag) = self.value_as_str(trc::Key::Id) {
|
||||
buf.extend_from_slice(tag.as_bytes());
|
||||
} else {
|
||||
buf.push(b'*');
|
||||
}
|
||||
buf.push(b' ');
|
||||
buf.extend_from_slice(self.value_as_str(trc::Key::Type).unwrap_or("NO").as_bytes());
|
||||
buf.push(b' ');
|
||||
if let Some(code) = self
|
||||
.value_as_str(trc::Key::Code)
|
||||
.or_else(|| match self.as_ref() {
|
||||
trc::EventType::Store(trc::StoreEvent::NotFound) => {
|
||||
Some(ResponseCode::NonExistent.as_str())
|
||||
}
|
||||
trc::EventType::Store(_) => Some(ResponseCode::ContactAdmin.as_str()),
|
||||
trc::EventType::Limit(trc::LimitEvent::Quota) => {
|
||||
Some(ResponseCode::OverQuota.as_str())
|
||||
}
|
||||
trc::EventType::Limit(_) => Some(ResponseCode::Limit.as_str()),
|
||||
trc::EventType::Auth(_) => Some(ResponseCode::AuthenticationFailed.as_str()),
|
||||
trc::EventType::Security(_) => Some(ResponseCode::AuthorizationFailed.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
buf.push(b'[');
|
||||
buf.extend_from_slice(code.as_bytes());
|
||||
buf.extend_from_slice(b"] ");
|
||||
}
|
||||
buf.extend_from_slice(
|
||||
self.value_as_str(trc::Key::Details)
|
||||
.unwrap_or_else(|| self.as_ref().message())
|
||||
.as_bytes(),
|
||||
);
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtocolVersion {
|
||||
#[inline(always)]
|
||||
pub fn is_rev2(&self) -> bool {
|
||||
matches!(self, ProtocolVersion::Rev2)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_rev1(&self) -> bool {
|
||||
matches!(self, ProtocolVersion::Rev1)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_sequence(buf: &mut Vec<u8>, list: &[u32]) {
|
||||
let mut ids = list.iter().peekable();
|
||||
while let Some(&id) = ids.next() {
|
||||
buf.extend_from_slice(id.to_string().as_bytes());
|
||||
let mut range_id = id;
|
||||
loop {
|
||||
match ids.peek() {
|
||||
Some(&&next_id) if next_id == range_id + 1 => {
|
||||
range_id += 1;
|
||||
ids.next();
|
||||
}
|
||||
next => {
|
||||
if range_id != id {
|
||||
buf.push(b':');
|
||||
buf.extend_from_slice(range_id.to_string().as_bytes());
|
||||
}
|
||||
if next.is_some() {
|
||||
buf.push(b',');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Command {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Command::UidBatches => write!(f, "UIDBATCHES"),
|
||||
Command::Capability => write!(f, "CAPABILITY"),
|
||||
Command::Noop => write!(f, "NOOP"),
|
||||
Command::Logout => write!(f, "LOGOUT"),
|
||||
Command::StartTls => write!(f, "STARTTLS"),
|
||||
Command::Authenticate => write!(f, "AUTHENTICATE"),
|
||||
Command::Login => write!(f, "LOGIN"),
|
||||
Command::Enable => write!(f, "ENABLE"),
|
||||
Command::Select => write!(f, "SELECT"),
|
||||
Command::Examine => write!(f, "EXAMINE"),
|
||||
Command::Create => write!(f, "CREATE"),
|
||||
Command::Delete => write!(f, "DELETE"),
|
||||
Command::Rename => write!(f, "RENAME"),
|
||||
Command::Subscribe => write!(f, "SUBSCRIBE"),
|
||||
Command::Unsubscribe => write!(f, "UNSUBSCRIBE"),
|
||||
Command::List => write!(f, "LIST"),
|
||||
Command::Namespace => write!(f, "NAMESPACE"),
|
||||
Command::Status => write!(f, "STATUS"),
|
||||
Command::Append => write!(f, "APPEND"),
|
||||
Command::Idle => write!(f, "IDLE"),
|
||||
Command::Close => write!(f, "CLOSE"),
|
||||
Command::Unselect => write!(f, "UNSELECT"),
|
||||
Command::Expunge(false) => write!(f, "EXPUNGE"),
|
||||
Command::Search(false) => write!(f, "SEARCH"),
|
||||
Command::Fetch(false) => write!(f, "FETCH"),
|
||||
Command::Store(false) => write!(f, "STORE"),
|
||||
Command::Copy(false) => write!(f, "COPY"),
|
||||
Command::Move(false) => write!(f, "MOVE"),
|
||||
Command::Sort(false) => write!(f, "SORT"),
|
||||
Command::Thread(false) => write!(f, "THREAD"),
|
||||
Command::Expunge(true) => write!(f, "UID EXPUNGE"),
|
||||
Command::Search(true) => write!(f, "UID SEARCH"),
|
||||
Command::Fetch(true) => write!(f, "UID FETCH"),
|
||||
Command::Store(true) => write!(f, "UID STORE"),
|
||||
Command::Copy(true) => write!(f, "UID COPY"),
|
||||
Command::Move(true) => write!(f, "UID MOVE"),
|
||||
Command::Sort(true) => write!(f, "UID SORT"),
|
||||
Command::Thread(true) => write!(f, "UID THREAD"),
|
||||
Command::Lsub => write!(f, "LSUB"),
|
||||
Command::Check => write!(f, "CHECK"),
|
||||
Command::SetAcl => write!(f, "SETACL"),
|
||||
Command::DeleteAcl => write!(f, "DELETEACL"),
|
||||
Command::GetAcl => write!(f, "GETACL"),
|
||||
Command::ListRights => write!(f, "LISTRIGHTS"),
|
||||
Command::MyRights => write!(f, "MYRIGHTS"),
|
||||
Command::Unauthenticate => write!(f, "UNAUTHENTICATE"),
|
||||
Command::Id => write!(f, "ID"),
|
||||
Command::GetQuota => write!(f, "GETQUOTA"),
|
||||
Command::GetQuotaRoot => write!(f, "GETQUOTAROOT"),
|
||||
Command::GetJmapAccess => write!(f, "GETJMAPACCESS"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::parser::parse_sequence_set;
|
||||
use crate::protocol::ObjectId;
|
||||
use types::id::Id;
|
||||
|
||||
#[test]
|
||||
fn serialize_objectid_compound() {
|
||||
// Empty compound
|
||||
let mut buf = Vec::new();
|
||||
ObjectId::default().serialize(&mut buf);
|
||||
assert_eq!(String::from_utf8(buf).unwrap(), "OBJECTID ()");
|
||||
|
||||
// Mailbox context: MAILBOXID + ACCOUNTID
|
||||
let mut buf = Vec::new();
|
||||
ObjectId {
|
||||
mailbox_id: Some(Id::from(1u32)),
|
||||
account_id: Some(Id::from(2u32)),
|
||||
..Default::default()
|
||||
}
|
||||
.serialize(&mut buf);
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
format!(
|
||||
"OBJECTID (ACCOUNTID {} MAILBOXID {})",
|
||||
Id::from(2u32),
|
||||
Id::from(1u32)
|
||||
)
|
||||
);
|
||||
|
||||
// Message context: EMAILID + THREADID only
|
||||
let mut buf = Vec::new();
|
||||
ObjectId {
|
||||
email_id: Some(Id::from_parts(3, 4)),
|
||||
thread_id: Some(Id::from(3u32)),
|
||||
..Default::default()
|
||||
}
|
||||
.serialize(&mut buf);
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
format!(
|
||||
"OBJECTID (EMAILID {} THREADID {})",
|
||||
Id::from_parts(3, 4),
|
||||
Id::from(3u32)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sequence_set_contains() {
|
||||
for (sequence, expected_result, max_value) in [
|
||||
("1,5:10", vec![1, 5, 6, 7, 8, 9, 10], 10),
|
||||
("2,4:7,9,12:*", vec![2, 4, 5, 6, 7, 9, 12, 13, 14, 15], 15),
|
||||
("*:4,5:7", vec![4, 5, 6, 7], 7),
|
||||
("2,4,5", vec![2, 4, 5], 5),
|
||||
] {
|
||||
let sequence = parse_sequence_set(sequence.as_bytes()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
(1..=15)
|
||||
.filter(|num| sequence.contains(*num, max_value))
|
||||
.collect::<Vec<_>>(),
|
||||
expected_result
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapResponse, quoted_string};
|
||||
|
||||
pub struct Response {
|
||||
pub shared_prefix: Option<String>,
|
||||
}
|
||||
|
||||
impl ImapResponse for Response {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(64);
|
||||
if let Some(shared_prefix) = &self.shared_prefix {
|
||||
buf.extend_from_slice(b"* NAMESPACE ((\"\" \"/\")) ((");
|
||||
quoted_string(&mut buf, shared_prefix);
|
||||
buf.extend_from_slice(b" \"/\")) NIL\r\n");
|
||||
} else {
|
||||
buf.extend_from_slice(b"* NAMESPACE ((\"\" \"/\")) NIL NIL\r\n");
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapResponse, capability::QuotaResourceName, quoted_string};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct QuotaItem {
|
||||
pub name: String,
|
||||
pub resources: Vec<QuotaResource>,
|
||||
}
|
||||
|
||||
pub struct QuotaResource {
|
||||
pub resource: QuotaResourceName,
|
||||
pub total: u64,
|
||||
pub used: u64,
|
||||
}
|
||||
|
||||
pub struct Response {
|
||||
pub quota_root_items: Vec<String>,
|
||||
pub quota_items: Vec<QuotaItem>,
|
||||
}
|
||||
|
||||
impl ImapResponse for Response {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(64);
|
||||
if !self.quota_root_items.is_empty() {
|
||||
buf.extend_from_slice(b"* QUOTAROOT");
|
||||
for item in &self.quota_root_items {
|
||||
buf.push(b' ');
|
||||
quoted_string(&mut buf, item);
|
||||
}
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
}
|
||||
|
||||
if !self.quota_items.is_empty() {
|
||||
for item in &self.quota_items {
|
||||
buf.extend_from_slice(b"* QUOTA ");
|
||||
quoted_string(&mut buf, &item.name);
|
||||
buf.extend_from_slice(b" (");
|
||||
for (pos, resource) in item.resources.iter().enumerate() {
|
||||
if pos > 0 {
|
||||
buf.push(b' ');
|
||||
}
|
||||
|
||||
let mut total = resource.total;
|
||||
let mut used = resource.used;
|
||||
|
||||
match resource.resource {
|
||||
QuotaResourceName::Storage => {
|
||||
total /= 1024;
|
||||
used /= 1024;
|
||||
|
||||
buf.extend_from_slice(b"STORAGE ")
|
||||
}
|
||||
QuotaResourceName::Message => buf.extend_from_slice(b"MESSAGE "),
|
||||
QuotaResourceName::Mailbox => buf.extend_from_slice(b"MAILBOX "),
|
||||
QuotaResourceName::AnnotationStorage => {
|
||||
buf.extend_from_slice(b"ANNOTATION-STORAGE ")
|
||||
}
|
||||
}
|
||||
|
||||
buf.extend_from_slice(format!("{used} {total}").as_bytes());
|
||||
}
|
||||
buf.extend_from_slice(b")\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::protocol::{ImapResponse, capability::QuotaResourceName};
|
||||
|
||||
use super::{QuotaItem, QuotaResource};
|
||||
|
||||
#[test]
|
||||
fn serialize_quota() {
|
||||
for (response, expected) in [
|
||||
(
|
||||
super::Response {
|
||||
quota_root_items: vec!["INBOX".into(), "#test".into()],
|
||||
quota_items: vec![],
|
||||
},
|
||||
"* QUOTAROOT \"INBOX\" \"#test\"\r\n",
|
||||
),
|
||||
(
|
||||
super::Response {
|
||||
quota_root_items: vec![],
|
||||
quota_items: vec![QuotaItem {
|
||||
name: "INBOX".into(),
|
||||
resources: vec![QuotaResource {
|
||||
resource: QuotaResourceName::Storage,
|
||||
total: 1073741824,
|
||||
used: 1048576,
|
||||
}],
|
||||
}],
|
||||
},
|
||||
"* QUOTA \"INBOX\" (STORAGE 1024 1048576)\r\n",
|
||||
),
|
||||
(
|
||||
super::Response {
|
||||
quota_root_items: vec!["my mailbox".into(), "".into()],
|
||||
quota_items: vec![QuotaItem {
|
||||
name: "INBOX".into(),
|
||||
resources: vec![
|
||||
QuotaResource {
|
||||
resource: QuotaResourceName::Storage,
|
||||
total: 1073741824,
|
||||
used: 1048576,
|
||||
},
|
||||
QuotaResource {
|
||||
resource: QuotaResourceName::Message,
|
||||
total: 100,
|
||||
used: 2,
|
||||
},
|
||||
],
|
||||
}],
|
||||
},
|
||||
concat!(
|
||||
"* QUOTAROOT \"my mailbox\" \"\"\r\n",
|
||||
"* QUOTA \"INBOX\" (STORAGE 1024 1048576 MESSAGE 2 100)\r\n"
|
||||
),
|
||||
),
|
||||
] {
|
||||
assert_eq!(String::from_utf8(response.serialize()).unwrap(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub mailbox_name: String,
|
||||
pub new_mailbox_name: String,
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{Flag, Sequence, quoted_string, serialize_sequence};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub is_esearch: bool,
|
||||
pub sort: Option<Vec<Comparator>>,
|
||||
pub result_options: Vec<ResultOption>,
|
||||
pub filter: Vec<Filter>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Sort {
|
||||
Arrival,
|
||||
Cc,
|
||||
Date,
|
||||
From,
|
||||
DisplayFrom,
|
||||
Size,
|
||||
Subject,
|
||||
To,
|
||||
DisplayTo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Comparator {
|
||||
pub sort: Sort,
|
||||
pub ascending: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Response {
|
||||
pub is_uid: bool,
|
||||
pub is_esearch: bool,
|
||||
pub is_sort: bool,
|
||||
pub ids: Vec<u32>,
|
||||
pub min: Option<u32>,
|
||||
pub max: Option<u32>,
|
||||
pub count: Option<u32>,
|
||||
pub highest_modseq: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ResultOption {
|
||||
Min,
|
||||
Max,
|
||||
All,
|
||||
Count,
|
||||
Save,
|
||||
Context,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Filter {
|
||||
Sequence(Sequence, bool),
|
||||
All,
|
||||
Answered,
|
||||
Bcc(String),
|
||||
Before(i64),
|
||||
Body(String),
|
||||
Cc(String),
|
||||
Deleted,
|
||||
Draft,
|
||||
Flagged,
|
||||
From(String),
|
||||
Header(String, String),
|
||||
Keyword(Flag),
|
||||
Larger(u32),
|
||||
On(i64),
|
||||
Seen,
|
||||
SentBefore(i64),
|
||||
SentOn(i64),
|
||||
SentSince(i64),
|
||||
Since(i64),
|
||||
Smaller(u32),
|
||||
Subject(String),
|
||||
Text(String),
|
||||
To(String),
|
||||
Unanswered,
|
||||
Undeleted,
|
||||
Undraft,
|
||||
Unflagged,
|
||||
Unkeyword(Flag),
|
||||
Unseen,
|
||||
|
||||
// Logical operators
|
||||
And,
|
||||
Or,
|
||||
Not,
|
||||
End,
|
||||
|
||||
// Imap4rev1
|
||||
Recent,
|
||||
New,
|
||||
Old,
|
||||
|
||||
// RFC 5032 - WITHIN
|
||||
Older(u32),
|
||||
Younger(u32),
|
||||
|
||||
// RFC 4551 - CONDSTORE
|
||||
ModSeq((u64, ModSeqEntry)),
|
||||
|
||||
// RFC 8474 - ObjectID
|
||||
EmailId(String),
|
||||
ThreadId(String),
|
||||
|
||||
// RFC 9738 - MESSAGELIMIT
|
||||
UidAfter(u32),
|
||||
UidBefore(u32),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ModSeqEntry {
|
||||
Shared(Flag),
|
||||
Private(Flag),
|
||||
All(Flag),
|
||||
None,
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub fn seq_saved_search() -> Filter {
|
||||
Filter::Sequence(Sequence::SavedSearch, false)
|
||||
}
|
||||
|
||||
pub fn seq_range(start: Option<u32>, end: Option<u32>) -> Filter {
|
||||
Filter::Sequence(Sequence::Range { start, end }, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn serialize(self, tag: &str) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(64);
|
||||
if self.is_esearch {
|
||||
buf.extend_from_slice(b"* ESEARCH (TAG ");
|
||||
quoted_string(&mut buf, tag);
|
||||
buf.extend_from_slice(b")");
|
||||
if self.is_uid {
|
||||
buf.extend_from_slice(b" UID");
|
||||
}
|
||||
if let Some(count) = &self.count {
|
||||
buf.extend_from_slice(b" COUNT ");
|
||||
buf.extend_from_slice(count.to_string().as_bytes());
|
||||
}
|
||||
if let Some(min) = &self.min {
|
||||
buf.extend_from_slice(b" MIN ");
|
||||
buf.extend_from_slice(min.to_string().as_bytes());
|
||||
}
|
||||
if let Some(max) = &self.max {
|
||||
buf.extend_from_slice(b" MAX ");
|
||||
buf.extend_from_slice(max.to_string().as_bytes());
|
||||
}
|
||||
if !self.ids.is_empty() {
|
||||
buf.extend_from_slice(b" ALL ");
|
||||
serialize_sequence(&mut buf, &self.ids);
|
||||
}
|
||||
if let Some(highest_modseq) = self.highest_modseq {
|
||||
buf.extend_from_slice(b" MODSEQ ");
|
||||
buf.extend_from_slice(highest_modseq.to_string().as_bytes());
|
||||
}
|
||||
} else {
|
||||
if !self.is_sort {
|
||||
buf.extend_from_slice(b"* SEARCH");
|
||||
} else {
|
||||
buf.extend_from_slice(b"* SORT");
|
||||
}
|
||||
if !self.ids.is_empty() {
|
||||
for id in &self.ids {
|
||||
buf.push(b' ');
|
||||
buf.extend_from_slice(id.to_string().as_bytes());
|
||||
}
|
||||
}
|
||||
if let Some(highest_modseq) = self.highest_modseq {
|
||||
buf.extend_from_slice(b" (MODSEQ ");
|
||||
buf.extend_from_slice(highest_modseq.to_string().as_bytes());
|
||||
buf.push(b')');
|
||||
}
|
||||
}
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn serialize_search() {
|
||||
for (mut response, tag, expected_v2, expected_v1) in [
|
||||
(
|
||||
super::Response {
|
||||
is_uid: false,
|
||||
is_esearch: true,
|
||||
is_sort: false,
|
||||
ids: vec![2, 10, 11],
|
||||
min: 2.into(),
|
||||
max: 11.into(),
|
||||
count: 3.into(),
|
||||
highest_modseq: None,
|
||||
},
|
||||
"A283",
|
||||
"* ESEARCH (TAG \"A283\") COUNT 3 MIN 2 MAX 11 ALL 2,10:11\r\n",
|
||||
"* SEARCH 2 10 11\r\n",
|
||||
),
|
||||
(
|
||||
super::Response {
|
||||
is_uid: false,
|
||||
is_esearch: true,
|
||||
is_sort: false,
|
||||
ids: vec![
|
||||
1, 2, 3, 5, 10, 11, 12, 13, 90, 92, 93, 94, 95, 96, 97, 98, 99,
|
||||
],
|
||||
min: None,
|
||||
max: None,
|
||||
count: None,
|
||||
highest_modseq: None,
|
||||
},
|
||||
"A283",
|
||||
"* ESEARCH (TAG \"A283\") ALL 1:3,5,10:13,90,92:99\r\n",
|
||||
"* SEARCH 1 2 3 5 10 11 12 13 90 92 93 94 95 96 97 98 99\r\n",
|
||||
),
|
||||
(
|
||||
super::Response {
|
||||
is_uid: false,
|
||||
is_esearch: true,
|
||||
is_sort: false,
|
||||
ids: vec![],
|
||||
min: None,
|
||||
max: None,
|
||||
count: None,
|
||||
highest_modseq: None,
|
||||
},
|
||||
"A283",
|
||||
"* ESEARCH (TAG \"A283\")\r\n",
|
||||
"* SEARCH\r\n",
|
||||
),
|
||||
(
|
||||
super::Response {
|
||||
is_uid: false,
|
||||
is_esearch: true,
|
||||
is_sort: false,
|
||||
ids: vec![10, 11, 12, 13, 21],
|
||||
min: None,
|
||||
max: None,
|
||||
count: None,
|
||||
highest_modseq: 12345.into(),
|
||||
},
|
||||
"A283",
|
||||
"* ESEARCH (TAG \"A283\") ALL 10:13,21 MODSEQ 12345\r\n",
|
||||
"* SEARCH 10 11 12 13 21 (MODSEQ 12345)\r\n",
|
||||
),
|
||||
] {
|
||||
let response_v2 = String::from_utf8(response.clone().serialize(tag)).unwrap();
|
||||
response.is_esearch = false;
|
||||
let response_v1 = String::from_utf8(response.serialize(tag)).unwrap();
|
||||
|
||||
assert_eq!(response_v2, expected_v2);
|
||||
assert_eq!(response_v1, expected_v1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{ResponseCode, StatusResponse};
|
||||
|
||||
use super::{ImapResponse, ObjectId, Sequence, list::ListItem};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub mailbox_name: String,
|
||||
pub condstore: bool,
|
||||
pub qresync: Option<QResync>,
|
||||
pub objectid: Option<ObjectId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct QResync {
|
||||
pub uid_validity: u32,
|
||||
pub modseq: u64,
|
||||
pub known_uids: Option<Sequence>,
|
||||
pub seq_match: Option<(Sequence, Sequence)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HighestModSeq(u64);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Response {
|
||||
pub mailbox: ListItem,
|
||||
pub total_messages: usize,
|
||||
pub recent_messages: usize,
|
||||
pub unseen_seq: u32,
|
||||
pub uid_validity: u32,
|
||||
pub uid_next: u32,
|
||||
pub is_rev2: bool,
|
||||
pub is_utf8: bool,
|
||||
pub closed_previous: bool,
|
||||
pub highest_modseq: Option<HighestModSeq>,
|
||||
pub objectid: Option<ObjectId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Exists {
|
||||
pub total_messages: usize,
|
||||
}
|
||||
|
||||
impl ImapResponse for Response {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(100);
|
||||
if self.closed_previous {
|
||||
buf = StatusResponse::ok("Closed previous mailbox")
|
||||
.with_code(ResponseCode::Closed)
|
||||
.serialize(buf);
|
||||
}
|
||||
buf.extend_from_slice(b"* ");
|
||||
buf.extend_from_slice(self.total_messages.to_string().as_bytes());
|
||||
if !self.is_rev2 && self.recent_messages > 0 {
|
||||
buf.extend_from_slice(
|
||||
b" EXISTS\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\Recent)\r\n",
|
||||
);
|
||||
} else {
|
||||
buf.extend_from_slice(
|
||||
b" EXISTS\r\n* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n",
|
||||
);
|
||||
}
|
||||
if self.is_rev2 {
|
||||
self.mailbox
|
||||
.serialize(&mut buf, self.is_rev2, self.is_utf8, false);
|
||||
} else {
|
||||
buf.extend_from_slice(b"* ");
|
||||
buf.extend_from_slice(self.recent_messages.to_string().as_bytes());
|
||||
buf.extend_from_slice(b" RECENT\r\n");
|
||||
if self.unseen_seq > 0 {
|
||||
buf.extend_from_slice(b"* OK [UNSEEN ");
|
||||
buf.extend_from_slice(self.unseen_seq.to_string().as_bytes());
|
||||
buf.extend_from_slice(b"] Unseen messages\r\n");
|
||||
}
|
||||
}
|
||||
buf.extend_from_slice(
|
||||
b"* OK [PERMANENTFLAGS (\\Deleted \\Seen \\Answered \\Flagged \\Draft \\*)] All allowed\r\n",
|
||||
);
|
||||
buf.extend_from_slice(b"* OK [UIDVALIDITY ");
|
||||
buf.extend_from_slice(self.uid_validity.to_string().as_bytes());
|
||||
buf.extend_from_slice(b"] UIDs valid\r\n* OK [UIDNEXT ");
|
||||
buf.extend_from_slice(self.uid_next.to_string().as_bytes());
|
||||
buf.extend_from_slice(b"] Next predicted UID\r\n");
|
||||
if let Some(highest_modseq) = self.highest_modseq {
|
||||
highest_modseq.serialize(&mut buf);
|
||||
}
|
||||
if let Some(objectid) = &self.objectid {
|
||||
buf.extend_from_slice(b"* OK [");
|
||||
objectid.serialize(&mut buf);
|
||||
buf.extend_from_slice(b"] Object identifiers\r\n");
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl HighestModSeq {
|
||||
pub fn new(modseq: u64) -> Self {
|
||||
Self(modseq)
|
||||
}
|
||||
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(b"* OK [HIGHESTMODSEQ ");
|
||||
buf.extend_from_slice(self.0.to_string().as_bytes());
|
||||
buf.extend_from_slice(b"] Highest Modseq\r\n");
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(40);
|
||||
self.serialize(&mut buf);
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
impl Exists {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>) {
|
||||
buf.extend_from_slice(b"* ");
|
||||
buf.extend_from_slice(self.total_messages.to_string().as_bytes());
|
||||
buf.extend_from_slice(b" EXISTS\r\n");
|
||||
}
|
||||
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(15);
|
||||
self.serialize(&mut buf);
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::protocol::{ImapResponse, ObjectId, list::ListItem};
|
||||
use types::id::Id;
|
||||
|
||||
use super::HighestModSeq;
|
||||
|
||||
#[test]
|
||||
fn serialize_select() {
|
||||
let objectid = ObjectId {
|
||||
mailbox_id: Some(Id::from(1u32)),
|
||||
account_id: Some(Id::from(2u32)),
|
||||
..Default::default()
|
||||
};
|
||||
let mut objectid_line = b"* OK [".to_vec();
|
||||
objectid.serialize(&mut objectid_line);
|
||||
objectid_line.extend_from_slice(b"] Object identifiers\r\n");
|
||||
let objectid_line = String::from_utf8(objectid_line).unwrap();
|
||||
|
||||
for (mut response, _tag, expected_v2, expected_v1) in [
|
||||
(
|
||||
super::Response {
|
||||
mailbox: ListItem::new("INBOX"),
|
||||
total_messages: 172,
|
||||
recent_messages: 5,
|
||||
unseen_seq: 3,
|
||||
uid_validity: 3857529045,
|
||||
uid_next: 4392,
|
||||
closed_previous: false,
|
||||
is_rev2: true,
|
||||
is_utf8: true,
|
||||
highest_modseq: HighestModSeq::new(100).into(),
|
||||
objectid: Some(objectid.clone()),
|
||||
},
|
||||
"A142",
|
||||
format!(
|
||||
concat!(
|
||||
"* 172 EXISTS\r\n",
|
||||
"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n",
|
||||
"* LIST () \"/\" \"INBOX\"\r\n",
|
||||
"* OK [PERMANENTFLAGS (\\Deleted \\Seen \\Answered \\Flagged \\Draft \\*)] All allowed\r\n",
|
||||
"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n",
|
||||
"* OK [UIDNEXT 4392] Next predicted UID\r\n",
|
||||
"* OK [HIGHESTMODSEQ 100] Highest Modseq\r\n",
|
||||
"{}"
|
||||
),
|
||||
objectid_line
|
||||
),
|
||||
format!(
|
||||
concat!(
|
||||
"* 172 EXISTS\r\n",
|
||||
"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\Recent)\r\n",
|
||||
"* 5 RECENT\r\n",
|
||||
"* OK [UNSEEN 3] Unseen messages\r\n",
|
||||
"* OK [PERMANENTFLAGS (\\Deleted \\Seen \\Answered \\Flagged \\Draft \\*)] All allowed\r\n",
|
||||
"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n",
|
||||
"* OK [UIDNEXT 4392] Next predicted UID\r\n",
|
||||
"* OK [HIGHESTMODSEQ 100] Highest Modseq\r\n",
|
||||
"{}"
|
||||
),
|
||||
objectid_line
|
||||
),
|
||||
),
|
||||
(
|
||||
super::Response {
|
||||
mailbox: ListItem::new("~peter/mail/台北/日本語"),
|
||||
total_messages: 172,
|
||||
recent_messages: 5,
|
||||
unseen_seq: 3,
|
||||
uid_validity: 3857529045,
|
||||
uid_next: 4392,
|
||||
closed_previous: true,
|
||||
is_rev2: true,
|
||||
is_utf8: true,
|
||||
highest_modseq: None,
|
||||
objectid: Some(objectid.clone()),
|
||||
},
|
||||
"A142",
|
||||
format!(
|
||||
concat!(
|
||||
"* OK [CLOSED] Closed previous mailbox\r\n",
|
||||
"* 172 EXISTS\r\n",
|
||||
"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft)\r\n",
|
||||
"* LIST () \"/\" \"~peter/mail/台北/日本語\" (\"OLDNAME\" ",
|
||||
"(\"~peter/mail/&U,BTFw-/&ZeVnLIqe-\"))\r\n",
|
||||
"* OK [PERMANENTFLAGS (\\Deleted \\Seen \\Answered \\Flagged \\Draft \\*)] All allowed\r\n",
|
||||
"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n",
|
||||
"* OK [UIDNEXT 4392] Next predicted UID\r\n",
|
||||
"{}"
|
||||
),
|
||||
objectid_line
|
||||
),
|
||||
format!(
|
||||
concat!(
|
||||
"* OK [CLOSED] Closed previous mailbox\r\n",
|
||||
"* 172 EXISTS\r\n",
|
||||
"* FLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft \\Recent)\r\n",
|
||||
"* 5 RECENT\r\n",
|
||||
"* OK [UNSEEN 3] Unseen messages\r\n",
|
||||
"* OK [PERMANENTFLAGS (\\Deleted \\Seen \\Answered \\Flagged \\Draft \\*)] All allowed\r\n",
|
||||
"* OK [UIDVALIDITY 3857529045] UIDs valid\r\n",
|
||||
"* OK [UIDNEXT 4392] Next predicted UID\r\n",
|
||||
"{}"
|
||||
),
|
||||
objectid_line
|
||||
),
|
||||
),
|
||||
] {
|
||||
let response_v2 = String::from_utf8(response.clone().serialize()).unwrap();
|
||||
response.is_rev2 = false;
|
||||
let response_v1 = String::from_utf8(response.serialize()).unwrap();
|
||||
|
||||
assert_eq!(response_v2, expected_v2);
|
||||
assert_eq!(response_v1, expected_v1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::utf7::utf7_encode;
|
||||
|
||||
use super::{ObjectId, quoted_string};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub mailbox_name: String,
|
||||
pub items: Vec<Status>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Status {
|
||||
Messages,
|
||||
UidNext,
|
||||
UidValidity,
|
||||
Unseen,
|
||||
Deleted,
|
||||
Size,
|
||||
Recent,
|
||||
HighestModSeq,
|
||||
ObjectId,
|
||||
DeletedStorage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct StatusItem {
|
||||
pub mailbox_name: String,
|
||||
pub items: Vec<(Status, StatusItemType)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum StatusItemType {
|
||||
Number(u64),
|
||||
String(String),
|
||||
ObjectId(ObjectId),
|
||||
}
|
||||
|
||||
impl StatusItem {
|
||||
pub fn serialize(&self, buf: &mut Vec<u8>, is_utf8: bool) {
|
||||
buf.extend_from_slice(b"* STATUS ");
|
||||
if is_utf8 {
|
||||
quoted_string(buf, &self.mailbox_name);
|
||||
} else {
|
||||
quoted_string(buf, &utf7_encode(&self.mailbox_name));
|
||||
}
|
||||
buf.extend_from_slice(b" (");
|
||||
for (pos, (status_item, value)) in self.items.iter().enumerate() {
|
||||
if pos > 0 {
|
||||
buf.push(b' ');
|
||||
}
|
||||
|
||||
buf.extend_from_slice(match status_item {
|
||||
Status::Messages => b"MESSAGES ",
|
||||
Status::UidNext => b"UIDNEXT ",
|
||||
Status::UidValidity => b"UIDVALIDITY ",
|
||||
Status::Unseen => b"UNSEEN ",
|
||||
Status::Deleted => b"DELETED ",
|
||||
Status::Size => b"SIZE ",
|
||||
Status::HighestModSeq => b"HIGHESTMODSEQ ",
|
||||
Status::ObjectId => b"OBJECTID ",
|
||||
Status::Recent => b"RECENT ",
|
||||
Status::DeletedStorage => b"DELETED-STORAGE ",
|
||||
});
|
||||
|
||||
match value {
|
||||
StatusItemType::Number(num) => {
|
||||
buf.extend_from_slice(num.to_string().as_bytes());
|
||||
}
|
||||
StatusItemType::String(str) => {
|
||||
buf.push(b'(');
|
||||
buf.extend_from_slice(str.as_bytes());
|
||||
buf.push(b')');
|
||||
}
|
||||
StatusItemType::ObjectId(object_id) => {
|
||||
object_id.serialize_kvpairs(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
buf.extend_from_slice(b")\r\n");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::protocol::{
|
||||
ObjectId,
|
||||
status::{Status, StatusItem, StatusItemType},
|
||||
};
|
||||
use types::id::Id;
|
||||
|
||||
#[test]
|
||||
fn serialize_status() {
|
||||
let objectid = ObjectId {
|
||||
mailbox_id: Some(Id::from(1u32)),
|
||||
account_id: Some(Id::from(2u32)),
|
||||
..Default::default()
|
||||
};
|
||||
let mut kvpairs = Vec::new();
|
||||
objectid.serialize_kvpairs(&mut kvpairs);
|
||||
let kvpairs = String::from_utf8(kvpairs).unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
StatusItem {
|
||||
mailbox_name: "blurdybloop".into(),
|
||||
items: vec![
|
||||
(Status::Messages, StatusItemType::Number(231)),
|
||||
(Status::UidNext, StatusItemType::Number(44292)),
|
||||
(Status::ObjectId, StatusItemType::ObjectId(objectid.clone())),
|
||||
],
|
||||
}
|
||||
.serialize(&mut buf, true);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(buf).unwrap(),
|
||||
format!("* STATUS \"blurdybloop\" (MESSAGES 231 UIDNEXT 44292 OBJECTID {kvpairs})\r\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{Flag, ImapResponse, Sequence, fetch::FetchItem};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub sequence_set: Sequence,
|
||||
pub operation: Operation,
|
||||
pub is_silent: bool,
|
||||
pub keywords: Vec<Flag>,
|
||||
pub unchanged_since: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Operation {
|
||||
Set,
|
||||
Add,
|
||||
Clear,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Response<'x> {
|
||||
pub is_utf8: bool,
|
||||
pub items: Vec<FetchItem<'x>>,
|
||||
}
|
||||
|
||||
impl ImapResponse for Response<'_> {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(64);
|
||||
for item in &self.items {
|
||||
item.serialize(&mut buf, self.is_utf8);
|
||||
}
|
||||
buf
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub mailbox_name: String,
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{ImapResponse, search::Filter};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub filter: Vec<Filter>,
|
||||
pub algorithm: Algorithm,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Algorithm {
|
||||
OrderedSubject,
|
||||
References,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Response {
|
||||
pub is_uid: bool,
|
||||
pub threads: Vec<Vec<u32>>,
|
||||
}
|
||||
|
||||
impl ImapResponse for Response {
|
||||
fn serialize(self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(64);
|
||||
buf.extend_from_slice(b"* THREAD ");
|
||||
for thread in &self.threads {
|
||||
buf.push(b'(');
|
||||
for (pos, id) in thread.iter().enumerate() {
|
||||
if pos > 0 {
|
||||
buf.push(b' ');
|
||||
}
|
||||
buf.extend_from_slice(id.to_string().as_bytes());
|
||||
}
|
||||
buf.push(b')');
|
||||
}
|
||||
buf.extend_from_slice(b"\r\n");
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::protocol::ImapResponse;
|
||||
|
||||
#[test]
|
||||
fn serialize_thread() {
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
super::Response {
|
||||
is_uid: true,
|
||||
threads: vec![vec![2, 10, 11], vec![49], vec![1, 3]],
|
||||
}
|
||||
.serialize()
|
||||
)
|
||||
.unwrap(),
|
||||
"* THREAD (2 10 11)(49)(1 3)\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Arguments {
|
||||
pub tag: String,
|
||||
pub batch_size: u32,
|
||||
pub batch_range: Option<(u32, u32)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Response {
|
||||
pub ranges: Vec<(u32, u32)>,
|
||||
}
|
||||
|
||||
impl Response {
|
||||
pub fn serialize(self, tag: &str) -> Vec<u8> {
|
||||
let mut buf = String::with_capacity(32 + (self.ranges.len() * 16));
|
||||
let _ = write!(&mut buf, "* UIDBATCHES (TAG \"{tag}\")");
|
||||
for (pos, (high, low)) in self.ranges.iter().enumerate() {
|
||||
let _ = write!(&mut buf, "{}{high}:{low}", if pos == 0 { ' ' } else { ',' });
|
||||
}
|
||||
buf.push_str("\r\n");
|
||||
buf.into_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Response;
|
||||
|
||||
#[test]
|
||||
fn serialize_uidbatches() {
|
||||
assert_eq!(
|
||||
String::from_utf8(
|
||||
Response {
|
||||
ranges: vec![(215295, 99696), (99695, 20351), (20350, 7830), (7829, 1)],
|
||||
}
|
||||
.serialize("A143")
|
||||
)
|
||||
.unwrap(),
|
||||
concat!(
|
||||
"* UIDBATCHES (TAG \"A143\") ",
|
||||
"215295:99696,99695:20351,20350:7830,7829:1\r\n"
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
String::from_utf8(Response { ranges: vec![] }.serialize("A144")).unwrap(),
|
||||
"* UIDBATCHES (TAG \"A144\")\r\n"
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
// Ported from https://github.com/jstedfast/MailKit/blob/master/MailKit/Net/Imap/ImapEncoding.cs
|
||||
// Author: Jeffrey Stedfast <[email protected]>
|
||||
|
||||
static UTF_7_RANK: &[u8] = &[
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
|
||||
255, 255, 255, 255, 255, 62, 63, 255, 255, 255, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255,
|
||||
255, 255, 255, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18,
|
||||
19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, 255, 26, 27, 28, 29, 30, 31, 32, 33, 34,
|
||||
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 255, 255, 255, 255, 255,
|
||||
];
|
||||
|
||||
static UTF_7_MAP: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
|
||||
|
||||
pub fn utf7_decode(text: &str) -> Option<String> {
|
||||
let mut bytes: Vec<u16> = Vec::with_capacity(text.len());
|
||||
let mut bits = 0;
|
||||
let mut v: u32 = 0;
|
||||
let mut shifted = false;
|
||||
let mut text = text.chars().peekable();
|
||||
|
||||
while let Some(ch) = text.next() {
|
||||
if shifted {
|
||||
if ch == '-' {
|
||||
shifted = false;
|
||||
bits = 0;
|
||||
v = 0;
|
||||
} else if ch as usize > 127 {
|
||||
return None;
|
||||
} else {
|
||||
let rank = *UTF_7_RANK.get(ch as usize)?;
|
||||
|
||||
if rank == 0xff {
|
||||
return None;
|
||||
}
|
||||
|
||||
v = (v << 6) | rank as u32;
|
||||
bits += 6;
|
||||
|
||||
if bits >= 16 {
|
||||
bytes.push(((v >> (bits - 16)) & 0xffff) as u16);
|
||||
bits -= 16;
|
||||
}
|
||||
}
|
||||
} else if ch == '&' {
|
||||
match text.peek() {
|
||||
Some('-') => {
|
||||
bytes.push(b'&' as u16);
|
||||
text.next();
|
||||
}
|
||||
Some(_) => {
|
||||
shifted = true;
|
||||
}
|
||||
None => {
|
||||
bytes.push(ch as u16);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
bytes.push(ch as u16);
|
||||
}
|
||||
}
|
||||
|
||||
String::from_utf16(&bytes).ok()
|
||||
}
|
||||
|
||||
pub fn utf7_encode(text: &str) -> String {
|
||||
let mut result = String::with_capacity(text.len());
|
||||
let mut shifted = false;
|
||||
let mut bits = 0;
|
||||
let mut u: u32 = 0;
|
||||
|
||||
for ch in text.encode_utf16() {
|
||||
if (0x20..0x7f).contains(&ch) {
|
||||
if shifted {
|
||||
if bits > 0 {
|
||||
result.push(char::from(UTF_7_MAP[((u << (6 - bits)) & 0x3f) as usize]));
|
||||
}
|
||||
result.push('-');
|
||||
shifted = false;
|
||||
bits = 0;
|
||||
}
|
||||
|
||||
if ch == 0x26 {
|
||||
result.push_str("&-");
|
||||
} else {
|
||||
result.push((ch as u8) as char);
|
||||
}
|
||||
} else {
|
||||
if !shifted {
|
||||
result.push('&');
|
||||
shifted = true;
|
||||
}
|
||||
|
||||
u = (u << 16) | ch as u32;
|
||||
bits += 16;
|
||||
|
||||
while bits >= 6 {
|
||||
result.push(char::from(UTF_7_MAP[((u >> (bits - 6)) & 0x3f) as usize]));
|
||||
bits -= 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shifted {
|
||||
if bits > 0 {
|
||||
result.push(char::from(UTF_7_MAP[((u << (6 - bits)) & 0x3f) as usize]));
|
||||
}
|
||||
result.push('-');
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn utf7_maybe_decode(text: String, is_utf8: bool) -> String {
|
||||
if is_utf8 {
|
||||
text
|
||||
} else {
|
||||
utf7_decode(&text).unwrap_or(text)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn utf7_decode() {
|
||||
for (input, expected_result) in [
|
||||
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
|
||||
("&U,BTF2XlZyyKng-", "台北日本語"),
|
||||
("Hello, World&ACE-", "Hello, World!"),
|
||||
("Hi Mom -&Jjo--!", "Hi Mom -☺-!"),
|
||||
("&ZeVnLIqe-", "日本語"),
|
||||
("Item 3 is &AKM-1.", "Item 3 is £1."),
|
||||
("Plus minus &- -&- &--", "Plus minus & -& &-"),
|
||||
(
|
||||
"&APw-ber ihre mi&AN8-liche Lage&ADs- &ACI-wir",
|
||||
"über ihre mißliche Lage; \"wir",
|
||||
),
|
||||
(
|
||||
concat!(
|
||||
"&ACI-The sayings of Confucius,&ACI- James R. Ware, trans. &U,BTFw-:\n",
|
||||
"&ZYeB9FH6ckh5Pg-, 1980.\n",
|
||||
"&Vttm+E6UfZM-, &W4tRQ066bOg-, &UxdOrA-: &Ti1XC2b4Xpc-, 1990."
|
||||
),
|
||||
concat!(
|
||||
"\"The sayings of Confucius,\" James R. Ware, trans. 台北:\n",
|
||||
"文致出版社, 1980.\n",
|
||||
"四書五經, 宋元人注, 北京: 中國書店, 1990."
|
||||
),
|
||||
),
|
||||
("Test-ąęć-Test", "Test-ąęć-Test"),
|
||||
(r#"&A8g- "&A9QD1APUA9gD3APcA-+""#, "ψ \"ϔϔϔϘϜϜ+\""),
|
||||
] {
|
||||
assert_eq!(
|
||||
super::utf7_decode(input).expect(input),
|
||||
expected_result,
|
||||
"while decoding {:?}",
|
||||
input
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf7_encode() {
|
||||
for (expected_result, input) in [
|
||||
("~peter/mail/&U,BTFw-/&ZeVnLIqe-", "~peter/mail/台北/日本語"),
|
||||
("&U,BTF2XlZyyKng-", "台北日本語"),
|
||||
("Hi Mom -&Jjo--!", "Hi Mom -☺-!"),
|
||||
("&ZeVnLIqe-", "日本語"),
|
||||
("Item 3 is &AKM-1.", "Item 3 is £1."),
|
||||
("Plus minus &- -&- &--", "Plus minus & -& &-"),
|
||||
("&VMhUyNg93gQ-", "哈哈😄"),
|
||||
] {
|
||||
assert_eq!(
|
||||
super::utf7_encode(input),
|
||||
expected_result,
|
||||
"while encoding {:?}",
|
||||
expected_result
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user