use super::validated::MailboxName;
use super::{FetchResponse, Flag, MailboxInfo, StatusItem};
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Response {
Greeting(GreetingResponse),
Tagged(TaggedResponse),
Untagged(Box<UntaggedResponse>),
Continuation(ContinuationRequest),
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GreetingResponse {
pub status: GreetingStatus,
pub code: Option<ResponseCode>,
pub text: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum GreetingStatus {
#[default]
Ok,
PreAuth,
Bye,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TaggedResponse {
pub tag: String,
pub status: StatusKind,
pub code: Option<ResponseCode>,
pub text: String,
}
impl TaggedResponse {
pub(crate) fn require_ok(self) -> Result<Self, crate::error::Error> {
match self.status {
StatusKind::Ok => Ok(self),
StatusKind::No => Err(crate::error::Error::no_with_code(self.text, self.code)),
StatusKind::Bad => Err(crate::error::Error::bad_with_code(self.text, self.code)),
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum StatusKind {
#[default]
Ok,
No,
Bad,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum UntaggedStatus {
#[default]
Ok,
No,
Bad,
Bye,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum UntaggedResponse {
Status {
status: UntaggedStatus,
code: Option<ResponseCode>,
text: String,
},
Exists(u32),
Recent(u32),
Expunge(u32),
Fetch(Box<FetchResponse>),
List(MailboxInfo),
Lsub(MailboxInfo),
Flags(Vec<Flag>),
Search {
uids: Vec<u32>,
mod_seq: Option<u64>,
},
Esearch(EsearchResponse),
MailboxStatus {
mailbox: MailboxName,
items: Vec<StatusItem>,
},
Capability(Vec<Capability>),
Enabled(Vec<String>),
Vanished { earlier: bool, uids: Vec<UidRange> },
Id(Vec<(String, Option<String>)>),
Namespace {
personal: Vec<NamespaceDescriptor>,
other: Vec<NamespaceDescriptor>,
shared: Vec<NamespaceDescriptor>,
},
Quota {
root: String,
resources: Vec<QuotaResource>,
},
QuotaRoot {
mailbox: MailboxName,
roots: Vec<String>,
},
Acl {
mailbox: MailboxName,
entries: Vec<AclEntry>,
},
MyRights {
mailbox: MailboxName,
rights: String,
},
ListRights {
mailbox: MailboxName,
identifier: String,
required: String,
optional: Vec<String>,
},
Metadata {
mailbox: MailboxName,
entries: Vec<MetadataEntry>,
},
Thread(Vec<ThreadNode>),
Sort {
nums: Vec<u32>,
mod_seq: Option<u64>,
},
Unknown(String),
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ContinuationRequest {
pub code: Option<ResponseCode>,
pub data: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ResponseCode {
Alert,
BadCharset(Vec<String>),
Capability(Vec<Capability>),
Parse,
PermanentFlags(Vec<Flag>),
ReadOnly,
ReadWrite,
TryCreate,
UidNext(u32),
UidValidity(u32),
Unseen(u32),
AppendUid {
uid_validity: u32,
uids: Vec<UidRange>,
},
CopyUid {
uid_validity: u32,
source_uids: Vec<UidRange>,
dest_uids: Vec<UidRange>,
},
HighestModSeq(u64),
Modified(Vec<UidRange>),
NoModSeq,
Closed,
MailboxId(String),
Unavailable,
AuthenticationFailed,
AuthorizationFailed,
Expired,
PrivacyRequired,
ContactAdmin,
NoPerm,
InUse,
ExpungeIssued,
Corruption,
ServerBug,
ClientBug,
Cannot,
Limit,
OverQuota,
AlreadyExists,
NonExistent,
NewName(Option<String>),
Referral(Option<String>),
UrlMech(Option<String>),
BadUrl(Option<String>),
BadComparator(Option<String>),
Annotate(Option<String>),
Annotations(Option<String>),
TempFail(Option<String>),
MaxConvertMessages(Option<String>),
MaxConvertParts(Option<String>),
NoUpdate(Option<String>),
NotificationOverflow(Option<String>),
BadEvent(Option<String>),
UndefinedFilter(Option<String>),
UidNotSticky,
NotSaved,
HasChildren,
UnknownCte,
TooBig,
CompressionActive,
UseAttr,
MetadataLongEntries(u64),
MetadataMaxSize(u64),
MetadataTooMany,
MetadataNoPrivate,
Other { name: String, value: Option<String> },
}
#[non_exhaustive]
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Capability {
Imap4Rev1,
Imap4Rev2,
Acl,
AppendLimit(Option<u64>),
Binary,
Children,
CompressDeflate,
Condstore,
CreateSpecialUse,
Enable,
Esearch,
Id,
Idle,
ListExtended,
ListStatus,
LiteralPlus,
LoginDisabled,
LiteralMinus,
Metadata,
MetadataServer,
Move,
MultiAppend,
Namespace,
Notify,
ObjectId,
QResync,
Quota,
QuotaResource(String),
QuotaSet,
Rights(String),
Preview,
SaslIr,
SaveDate,
SearchRes,
Sort,
SortDisplay(String),
StartTls,
SpecialUse,
Thread(String),
StatusSize,
UidPlus,
Unauthenticate,
Unselect,
Utf8Accept,
Utf8Only,
Within,
Auth(String),
Other(String),
}
impl Capability {
pub fn as_imap_str(&self) -> String {
match self {
Self::Imap4Rev1 => "IMAP4rev1".into(),
Self::Imap4Rev2 => "IMAP4rev2".into(),
Self::Acl => "ACL".into(),
Self::AppendLimit(Some(n)) => format!("APPENDLIMIT={n}"),
Self::AppendLimit(None) => "APPENDLIMIT".into(),
Self::Binary => "BINARY".into(),
Self::Children => "CHILDREN".into(),
Self::CompressDeflate => "COMPRESS=DEFLATE".into(),
Self::Condstore => "CONDSTORE".into(),
Self::CreateSpecialUse => "CREATE-SPECIAL-USE".into(),
Self::Enable => "ENABLE".into(),
Self::Esearch => "ESEARCH".into(),
Self::Id => "ID".into(),
Self::Idle => "IDLE".into(),
Self::ListExtended => "LIST-EXTENDED".into(),
Self::ListStatus => "LIST-STATUS".into(),
Self::LiteralPlus => "LITERAL+".into(),
Self::LoginDisabled => "LOGINDISABLED".into(),
Self::LiteralMinus => "LITERAL-".into(),
Self::Metadata => "METADATA".into(),
Self::MetadataServer => "METADATA-SERVER".into(),
Self::Move => "MOVE".into(),
Self::MultiAppend => "MULTIAPPEND".into(),
Self::Namespace => "NAMESPACE".into(),
Self::Notify => "NOTIFY".into(),
Self::ObjectId => "OBJECTID".into(),
Self::Preview => "PREVIEW".into(),
Self::QResync => "QRESYNC".into(),
Self::Quota => "QUOTA".into(),
Self::QuotaResource(s) => format!("QUOTA=RES-{s}"),
Self::QuotaSet => "QUOTASET".into(),
Self::Rights(s) => format!("RIGHTS={s}"),
Self::SaslIr => "SASL-IR".into(),
Self::SaveDate => "SAVEDATE".into(),
Self::SearchRes => "SEARCHRES".into(),
Self::Sort => "SORT".into(),
Self::SortDisplay(s) => format!("SORT={s}"),
Self::StartTls => "STARTTLS".into(),
Self::SpecialUse => "SPECIAL-USE".into(),
Self::Thread(s) => format!("THREAD={s}"),
Self::StatusSize => "STATUS=SIZE".into(),
Self::Unauthenticate => "UNAUTHENTICATE".into(),
Self::UidPlus => "UIDPLUS".into(),
Self::Unselect => "UNSELECT".into(),
Self::Within => "WITHIN".into(),
Self::Utf8Accept => "UTF8=ACCEPT".into(),
Self::Utf8Only => "UTF8=ONLY".into(),
Self::Auth(s) => format!("AUTH={s}"),
Self::Other(s) => s.clone(),
}
}
#[allow(clippy::too_many_lines)]
pub fn from_imap_str(s: &str) -> Self {
let upper = s.to_ascii_uppercase();
match upper.as_str() {
"IMAP4REV1" => Self::Imap4Rev1,
"IMAP4REV2" => Self::Imap4Rev2,
"ACL" => Self::Acl,
"BINARY" => Self::Binary,
"CHILDREN" => Self::Children,
"COMPRESS=DEFLATE" => Self::CompressDeflate,
"CONDSTORE" => Self::Condstore,
"CREATE-SPECIAL-USE" => Self::CreateSpecialUse,
"ENABLE" => Self::Enable,
"ESEARCH" => Self::Esearch,
"ID" => Self::Id,
"IDLE" => Self::Idle,
"LIST-EXTENDED" => Self::ListExtended,
"LIST-STATUS" => Self::ListStatus,
"LITERAL+" => Self::LiteralPlus,
"LITERAL-" => Self::LiteralMinus,
"LOGINDISABLED" => Self::LoginDisabled,
"METADATA" => Self::Metadata,
"METADATA-SERVER" => Self::MetadataServer,
"MOVE" => Self::Move,
"MULTIAPPEND" => Self::MultiAppend,
"NAMESPACE" => Self::Namespace,
"NOTIFY" => Self::Notify,
"OBJECTID" => Self::ObjectId,
"PREVIEW" => Self::Preview,
"QRESYNC" => Self::QResync,
"QUOTA" => Self::Quota,
"QUOTASET" => Self::QuotaSet,
"SASL-IR" => Self::SaslIr,
"SAVEDATE" => Self::SaveDate,
"SEARCHRES" => Self::SearchRes,
"SORT" => Self::Sort,
"SPECIAL-USE" => Self::SpecialUse,
"STARTTLS" => Self::StartTls,
"STATUS=SIZE" => Self::StatusSize,
"UNAUTHENTICATE" => Self::Unauthenticate,
"UIDPLUS" => Self::UidPlus,
"UNSELECT" => Self::Unselect,
"WITHIN" => Self::Within,
"UTF8=ACCEPT" => Self::Utf8Accept,
"UTF8=ONLY" => Self::Utf8Only,
_ => {
if let Some(mechanism) = upper.strip_prefix("AUTH=") {
if mechanism.is_empty() {
Self::Other(s.to_owned())
} else {
Self::Auth(mechanism.to_owned())
}
} else if upper == "SORT=DISPLAY" {
Self::SortDisplay("DISPLAY".to_owned())
} else if let Some(resource) = upper.strip_prefix("QUOTA=RES-") {
if resource.is_empty() {
Self::Other(s.to_owned())
} else {
Self::QuotaResource(s["QUOTA=RES-".len()..].to_string())
}
} else if let Some(algo) = upper.strip_prefix("THREAD=") {
if algo.is_empty() {
Self::Other(s.to_owned())
} else {
Self::Thread(algo.to_owned())
}
} else if let Some(rights) = upper.strip_prefix("RIGHTS=") {
if rights.is_empty() {
Self::Other(s.to_owned())
} else {
Self::Rights(s["RIGHTS=".len()..].to_string())
}
} else if let Some(rest) = upper.strip_prefix("APPENDLIMIT") {
if rest.is_empty() {
Self::AppendLimit(None)
} else if let Some(val_str) = rest.strip_prefix('=') {
if !val_str.is_empty() && val_str.bytes().all(|b| b.is_ascii_digit()) {
if let Ok(n) = val_str.parse::<u64>() {
Self::AppendLimit(Some(n))
} else {
Self::Other(s.to_owned())
}
} else {
Self::Other(s.to_owned())
}
} else {
Self::Other(s.to_owned())
}
} else {
Self::Other(s.to_owned())
}
}
}
}
}
impl From<String> for Capability {
fn from(s: String) -> Self {
Self::from_imap_str(&s)
}
}
impl From<&str> for Capability {
fn from(s: &str) -> Self {
Self::from_imap_str(s)
}
}
impl PartialEq for Capability {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Imap4Rev1, Self::Imap4Rev1)
| (Self::Imap4Rev2, Self::Imap4Rev2)
| (Self::Acl, Self::Acl)
| (Self::Binary, Self::Binary)
| (Self::Children, Self::Children)
| (Self::CompressDeflate, Self::CompressDeflate)
| (Self::Condstore, Self::Condstore)
| (Self::CreateSpecialUse, Self::CreateSpecialUse)
| (Self::Enable, Self::Enable)
| (Self::Esearch, Self::Esearch)
| (Self::Id, Self::Id)
| (Self::Idle, Self::Idle)
| (Self::ListExtended, Self::ListExtended)
| (Self::ListStatus, Self::ListStatus)
| (Self::LiteralPlus, Self::LiteralPlus)
| (Self::LoginDisabled, Self::LoginDisabled)
| (Self::LiteralMinus, Self::LiteralMinus)
| (Self::Metadata, Self::Metadata)
| (Self::MetadataServer, Self::MetadataServer)
| (Self::Move, Self::Move)
| (Self::MultiAppend, Self::MultiAppend)
| (Self::Namespace, Self::Namespace)
| (Self::Notify, Self::Notify)
| (Self::ObjectId, Self::ObjectId)
| (Self::Preview, Self::Preview)
| (Self::QResync, Self::QResync)
| (Self::Quota, Self::Quota)
| (Self::QuotaSet, Self::QuotaSet)
| (Self::SaslIr, Self::SaslIr)
| (Self::SaveDate, Self::SaveDate)
| (Self::SearchRes, Self::SearchRes)
| (Self::Sort, Self::Sort)
| (Self::StartTls, Self::StartTls)
| (Self::SpecialUse, Self::SpecialUse)
| (Self::StatusSize, Self::StatusSize)
| (Self::Unauthenticate, Self::Unauthenticate)
| (Self::UidPlus, Self::UidPlus)
| (Self::Unselect, Self::Unselect)
| (Self::Within, Self::Within)
| (Self::Utf8Accept, Self::Utf8Accept)
| (Self::Utf8Only, Self::Utf8Only) => true,
(Self::AppendLimit(a), Self::AppendLimit(b)) => a == b,
(Self::Auth(a), Self::Auth(b))
| (Self::Thread(a), Self::Thread(b))
| (Self::SortDisplay(a), Self::SortDisplay(b))
| (Self::QuotaResource(a), Self::QuotaResource(b))
| (Self::Rights(a), Self::Rights(b))
| (Self::Other(a), Self::Other(b)) => a.eq_ignore_ascii_case(b),
(Self::Other(s), known) | (known, Self::Other(s)) => {
s.eq_ignore_ascii_case(&known.as_imap_str())
}
_ => false,
}
}
}
impl Eq for Capability {}
impl std::hash::Hash for Capability {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
for byte in self.as_imap_str().as_bytes() {
byte.to_ascii_lowercase().hash(state);
}
}
}
impl std::fmt::Display for Capability {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(&self.as_imap_str(), f)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NamespaceDescriptor {
pub prefix: String,
pub delimiter: Option<char>,
pub extensions: Vec<(String, Vec<String>)>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NamespaceResponse {
pub personal: Vec<NamespaceDescriptor>,
pub other: Vec<NamespaceDescriptor>,
pub shared: Vec<NamespaceDescriptor>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct QuotaRootResponse {
pub roots: Vec<String>,
pub resources: Vec<(String, Vec<QuotaResource>)>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ListRightsResponse {
pub required: String,
pub optional: Vec<String>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EsearchResponse {
pub tag: Option<String>,
pub uid: bool,
pub min: Option<u32>,
pub max: Option<u32>,
pub count: Option<u32>,
pub all: Vec<UidRange>,
pub mod_seq: Option<u64>,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct UidRange {
pub start: u32,
pub end: Option<u32>,
}
impl UidRange {
pub const fn single(uid: u32) -> Self {
debug_assert!(
uid != 0,
"UID must be non-zero (RFC 3501 Section 9: uniqueid = nz-number)"
);
Self {
start: uid,
end: None,
}
}
pub const fn range(start: u32, end: u32) -> Self {
debug_assert!(
start != 0,
"UID start must be non-zero (RFC 3501 Section 9: uniqueid = nz-number)"
);
debug_assert!(
end != 0,
"UID end must be non-zero (RFC 3501 Section 9: uniqueid = nz-number)"
);
Self {
start,
end: Some(end),
}
}
pub const fn try_single(uid: u32) -> Option<Self> {
if uid == 0 {
None
} else {
Some(Self {
start: uid,
end: None,
})
}
}
pub const fn try_range(start: u32, end: u32) -> Option<Self> {
if start == 0 || end == 0 {
None
} else {
Some(Self {
start,
end: Some(end),
})
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ExpungeResult {
Expunged(Vec<u32>),
Vanished(Vec<UidRange>),
}
impl Default for ExpungeResult {
fn default() -> Self {
Self::Expunged(Vec::new())
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MoveResult {
pub code: Option<ResponseCode>,
pub expunged: ExpungeResult,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct CopyResult {
pub code: Option<ResponseCode>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct QresyncParams {
pub uid_validity: u32,
pub mod_seq: u64,
pub known_uids: Option<String>,
pub seq_match_data: Option<(String, String)>,
}
impl QresyncParams {
pub fn new(uid_validity: u32, mod_seq: u64) -> Self {
Self {
uid_validity,
mod_seq,
known_uids: None,
seq_match_data: None,
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SelectOptions {
pub condstore: bool,
pub qresync: Option<QresyncParams>,
}
impl SelectOptions {
pub fn condstore() -> Self {
Self {
condstore: true,
..Self::default()
}
}
pub fn qresync(params: QresyncParams) -> Self {
Self {
qresync: Some(params),
..Self::default()
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct QuotaResource {
pub name: String,
pub usage: u64,
pub limit: u64,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AclEntry {
pub identifier: String,
pub rights: String,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MetadataEntry {
pub name: String,
pub value: Option<Vec<u8>>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MetadataResult {
pub entries: Vec<MetadataEntry>,
pub notify_ambiguity: bool,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ThreadNode {
pub id: Option<u32>,
pub children: Vec<Self>,
}
#[cfg(test)]
#[path = "response_tests.rs"]
mod tests;