Import upstream v0.16.22, stripped

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

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+286
View File
@@ -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"
);
}
}
+21
View File
@@ -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,
}
+14
View File
@@ -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>,
}
+11
View File
@@ -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,
}
+35
View File
@@ -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()
}
}
}
+111
View File
@@ -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
+414
View File
@@ -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);
}
}
+12
View File
@@ -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,
}
+879
View File
@@ -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
}
}
+138
View File
@@ -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);
}
}
}
+12
View File
@@ -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,
}
+267
View File
@@ -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);
}
}
}
+251
View File
@@ -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);
}
}
}
+125
View File
@@ -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")
);
}
}
+40
View File
@@ -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,
}
+65
View File
@@ -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"
);
}
}