use super::fetch::FetchResponse;
use super::flag::Flag;
use super::response::UidRange;
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct MailboxInfo {
pub name: String,
pub delimiter: Option<char>,
pub attributes: Vec<MailboxAttribute>,
pub old_name: Option<String>,
pub child_info: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum MailboxAttribute {
NoInferiors,
NoSelect,
NonExistent,
HasChildren,
HasNoChildren,
Marked,
Unmarked,
Subscribed,
Remote,
All,
Archive,
Drafts,
Flagged,
Junk,
Sent,
Trash,
Important,
Memos,
Scheduled,
Snoozed,
Custom(String),
}
impl MailboxAttribute {
pub fn as_imap_str(&self) -> &str {
match self {
Self::NoInferiors => "\\Noinferiors",
Self::NoSelect => "\\Noselect",
Self::NonExistent => "\\NonExistent",
Self::HasChildren => "\\HasChildren",
Self::HasNoChildren => "\\HasNoChildren",
Self::Marked => "\\Marked",
Self::Unmarked => "\\Unmarked",
Self::Subscribed => "\\Subscribed",
Self::Remote => "\\Remote",
Self::All => "\\All",
Self::Archive => "\\Archive",
Self::Drafts => "\\Drafts",
Self::Flagged => "\\Flagged",
Self::Junk => "\\Junk",
Self::Sent => "\\Sent",
Self::Trash => "\\Trash",
Self::Important => "\\Important",
Self::Memos => "\\Memos",
Self::Scheduled => "\\Scheduled",
Self::Snoozed => "\\Snoozed",
Self::Custom(s) => s,
}
}
}
impl PartialEq for MailboxAttribute {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::NoInferiors, Self::NoInferiors)
| (Self::NoSelect, Self::NoSelect)
| (Self::NonExistent, Self::NonExistent)
| (Self::HasChildren, Self::HasChildren)
| (Self::HasNoChildren, Self::HasNoChildren)
| (Self::Marked, Self::Marked)
| (Self::Unmarked, Self::Unmarked)
| (Self::Subscribed, Self::Subscribed)
| (Self::Remote, Self::Remote)
| (Self::All, Self::All)
| (Self::Archive, Self::Archive)
| (Self::Drafts, Self::Drafts)
| (Self::Flagged, Self::Flagged)
| (Self::Junk, Self::Junk)
| (Self::Sent, Self::Sent)
| (Self::Trash, Self::Trash)
| (Self::Important, Self::Important)
| (Self::Memos, Self::Memos)
| (Self::Scheduled, Self::Scheduled)
| (Self::Snoozed, Self::Snoozed) => true,
(Self::Custom(a), Self::Custom(b)) => a.eq_ignore_ascii_case(b),
(Self::Custom(s), known) | (known, Self::Custom(s)) => {
s.eq_ignore_ascii_case(known.as_imap_str())
}
_ => false,
}
}
}
impl Eq for MailboxAttribute {}
impl std::hash::Hash for MailboxAttribute {
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);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SpecialUse {
Inbox,
All,
Archive,
Drafts,
Sent,
Junk,
Trash,
Flagged,
Important,
}
impl MailboxInfo {
pub fn special_use(&self) -> Option<SpecialUse> {
for attr in &self.attributes {
match attr {
MailboxAttribute::All => return Some(SpecialUse::All),
MailboxAttribute::Archive => return Some(SpecialUse::Archive),
MailboxAttribute::Drafts => return Some(SpecialUse::Drafts),
MailboxAttribute::Sent => return Some(SpecialUse::Sent),
MailboxAttribute::Junk => return Some(SpecialUse::Junk),
MailboxAttribute::Trash => return Some(SpecialUse::Trash),
MailboxAttribute::Flagged => return Some(SpecialUse::Flagged),
MailboxAttribute::Important => return Some(SpecialUse::Important),
_ => {}
}
}
let lower = self.name.to_ascii_lowercase();
let leaf = match self.delimiter {
Some(delim) => match lower.rsplit(delim).next() {
Some(s) => s,
None => &lower,
},
None => &lower,
};
match leaf {
"inbox" => Some(SpecialUse::Inbox),
"sent" | "sent items" | "sent messages" => Some(SpecialUse::Sent),
"drafts" | "draft" => Some(SpecialUse::Drafts),
"trash" | "deleted" | "deleted items" | "deleted messages" => Some(SpecialUse::Trash),
"spam" | "junk" | "junk e-mail" | "bulk mail" => Some(SpecialUse::Junk),
"archive" | "archives" => Some(SpecialUse::Archive),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SelectedMailbox {
pub exists: u32,
pub recent: u32,
pub uid_validity: Option<u32>,
pub uid_next: Option<u32>,
pub flags: Vec<Flag>,
pub permanent_flags: Vec<Flag>,
pub highest_mod_seq: Option<u64>,
pub unseen: Option<u32>,
pub mailbox_id: Option<String>,
pub read_only: bool,
pub vanished: Vec<UidRange>,
pub changed_messages: Vec<FetchResponse>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StatusItem {
Messages(u32),
Recent(u32),
Unseen(u32),
UidNext(u32),
UidValidity(u32),
Deleted(u32),
HighestModSeq(u64),
Size(u64),
MailboxId(String),
AppendLimit(Option<u64>),
DeletedStorage(u64),
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn special_use_from_attribute() {
let info = MailboxInfo {
name: "MyArchive".into(),
delimiter: Some('/'),
attributes: vec![MailboxAttribute::Archive],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Archive));
}
#[test]
fn special_use_name_fallback() {
let info = MailboxInfo {
name: "Spam".into(),
delimiter: Some('/'),
attributes: vec![],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Junk));
}
#[test]
fn special_use_case_insensitive() {
let info = MailboxInfo {
name: "SENT".into(),
delimiter: Some('/'),
attributes: vec![],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Sent));
}
#[test]
fn special_use_nested_name() {
let info = MailboxInfo {
name: "INBOX/Trash".into(),
delimiter: Some('/'),
attributes: vec![],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Trash));
}
#[test]
fn special_use_attribute_overrides_name() {
let info = MailboxInfo {
name: "Trash".into(),
delimiter: Some('/'),
attributes: vec![MailboxAttribute::Sent],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Sent));
}
#[test]
fn special_use_dot_delimiter() {
let info = MailboxInfo {
name: "INBOX.Trash".into(),
delimiter: Some('.'),
attributes: vec![],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Trash));
}
#[test]
fn special_use_no_delimiter() {
let info = MailboxInfo {
name: "Sent".into(),
delimiter: None,
attributes: vec![],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Sent));
}
#[test]
fn special_use_unknown() {
let info = MailboxInfo {
name: "Work".into(),
delimiter: Some('/'),
attributes: vec![],
..Default::default()
};
assert_eq!(info.special_use(), None);
}
#[test]
fn special_use_from_drafts_attribute() {
let info = MailboxInfo {
name: "MyDrafts".into(),
delimiter: Some('/'),
attributes: vec![MailboxAttribute::Drafts],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Drafts));
}
#[test]
fn special_use_from_junk_attribute() {
let info = MailboxInfo {
name: "SpamFolder".into(),
delimiter: Some('/'),
attributes: vec![MailboxAttribute::Junk],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Junk));
}
#[test]
fn special_use_from_trash_attribute() {
let info = MailboxInfo {
name: "Bin".into(),
delimiter: Some('/'),
attributes: vec![MailboxAttribute::Trash],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Trash));
}
#[test]
fn special_use_from_flagged_attribute() {
let info = MailboxInfo {
name: "Stars".into(),
delimiter: Some('/'),
attributes: vec![MailboxAttribute::Flagged],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Flagged));
}
#[test]
fn special_use_from_important_attribute() {
let info = MailboxInfo {
name: "Priority".into(),
delimiter: Some('/'),
attributes: vec![MailboxAttribute::Important],
..Default::default()
};
assert_eq!(info.special_use(), Some(SpecialUse::Important));
}
#[test]
fn special_use_name_fallback_returns_none_for_unknown_name() {
let info = MailboxInfo {
name: "Projects/2024/reports".into(),
delimiter: Some('/'),
attributes: vec![MailboxAttribute::HasNoChildren],
..Default::default()
};
assert_eq!(info.special_use(), None);
}
#[test]
fn status_item_mailboxid() {
let item = StatusItem::MailboxId("F2212ea87-6097-4256".into());
assert_eq!(item, StatusItem::MailboxId("F2212ea87-6097-4256".into()));
}
#[test]
fn as_imap_str_base_attributes() {
assert_eq!(MailboxAttribute::NoInferiors.as_imap_str(), "\\Noinferiors");
assert_eq!(MailboxAttribute::NoSelect.as_imap_str(), "\\Noselect");
assert_eq!(MailboxAttribute::NonExistent.as_imap_str(), "\\NonExistent");
assert_eq!(MailboxAttribute::HasChildren.as_imap_str(), "\\HasChildren");
assert_eq!(
MailboxAttribute::HasNoChildren.as_imap_str(),
"\\HasNoChildren"
);
assert_eq!(MailboxAttribute::Marked.as_imap_str(), "\\Marked");
assert_eq!(MailboxAttribute::Unmarked.as_imap_str(), "\\Unmarked");
assert_eq!(MailboxAttribute::Subscribed.as_imap_str(), "\\Subscribed");
assert_eq!(MailboxAttribute::Remote.as_imap_str(), "\\Remote");
}
#[test]
fn as_imap_str_special_use_attributes() {
assert_eq!(MailboxAttribute::All.as_imap_str(), "\\All");
assert_eq!(MailboxAttribute::Archive.as_imap_str(), "\\Archive");
assert_eq!(MailboxAttribute::Drafts.as_imap_str(), "\\Drafts");
assert_eq!(MailboxAttribute::Flagged.as_imap_str(), "\\Flagged");
assert_eq!(MailboxAttribute::Junk.as_imap_str(), "\\Junk");
assert_eq!(MailboxAttribute::Sent.as_imap_str(), "\\Sent");
assert_eq!(MailboxAttribute::Trash.as_imap_str(), "\\Trash");
assert_eq!(MailboxAttribute::Important.as_imap_str(), "\\Important");
}
#[test]
fn as_imap_str_non_standard_attributes() {
assert_eq!(MailboxAttribute::Memos.as_imap_str(), "\\Memos");
assert_eq!(MailboxAttribute::Scheduled.as_imap_str(), "\\Scheduled");
assert_eq!(MailboxAttribute::Snoozed.as_imap_str(), "\\Snoozed");
}
#[test]
fn as_imap_str_custom() {
let attr = MailboxAttribute::Custom("\\MyCustom".into());
assert_eq!(attr.as_imap_str(), "\\MyCustom");
}
#[test]
fn custom_attribute_equality_is_case_insensitive() {
assert_eq!(
MailboxAttribute::Custom("\\MyCustom".into()),
MailboxAttribute::Custom("\\mycustom".into()),
"Custom mailbox attributes must compare case-insensitively per RFC 3501 Section 7.2.2"
);
}
#[test]
fn custom_attribute_hash_is_case_insensitive() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(MailboxAttribute::Custom("\\MyCustom".into()));
set.insert(MailboxAttribute::Custom("\\mycustom".into()));
assert_eq!(
set.len(),
1,
"Case-insensitively equal Custom attributes must have the same Hash per RFC 3501 Section 7.2.2"
);
}
#[test]
fn custom_noselect_equals_noselect_variant() {
assert_eq!(
MailboxAttribute::Custom("\\Noselect".into()),
MailboxAttribute::NoSelect,
"Custom(\"\\\\Noselect\") must equal MailboxAttribute::NoSelect \
per RFC 3501 Section 7.2.2"
);
}
#[test]
fn custom_haschildren_equals_haschildren_variant() {
assert_eq!(
MailboxAttribute::Custom("\\HasChildren".into()),
MailboxAttribute::HasChildren,
"Custom(\"\\\\HasChildren\") must equal MailboxAttribute::HasChildren \
per RFC 3501 Section 7.2.2"
);
}
#[test]
fn custom_sent_equals_sent_variant() {
assert_eq!(
MailboxAttribute::Custom("\\Sent".into()),
MailboxAttribute::Sent,
"Custom(\"\\\\Sent\") must equal MailboxAttribute::Sent \
per RFC 6154 Section 2"
);
}
#[test]
fn custom_attribute_cross_representation_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(MailboxAttribute::NoSelect);
set.insert(MailboxAttribute::Custom("\\Noselect".into()));
assert_eq!(
set.len(),
1,
"Custom(\"\\\\Noselect\") and NoSelect must hash the same \
per RFC 3501 Section 7.2.2"
);
}
#[test]
fn spec_audit_l13_appendlimit_status_attribute() {
let input = b"* STATUS \"INBOX\" (MESSAGES 10 APPENDLIMIT 1048576)\r\n";
let (_, resp) =
crate::codec::decode::parse_response(input).expect("should parse APPENDLIMIT");
match resp {
crate::types::response::Response::Untagged(inner) => match *inner {
crate::types::response::UntaggedResponse::MailboxStatus { ref items, .. } => {
let has_appendlimit = items
.iter()
.any(|item| matches!(item, StatusItem::AppendLimit(Some(1_048_576))));
assert!(
has_appendlimit,
"STATUS response should contain AppendLimit(Some(1048576)); got {items:?}"
);
}
other => panic!("expected MailboxStatus, got {other:?}"),
},
other => panic!("expected Untagged, got {other:?}"),
}
}
}