use core::{cmp::Ordering, hash::Hash};
use alloc::string::{String, ToString};
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Flag {
raw: String,
iana: Option<IanaFlag>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
pub enum IanaFlag {
Seen,
Answered,
Flagged,
Draft,
Deleted,
Forwarded,
Junk,
NotJunk,
Phishing,
Important,
MdnSent,
}
impl Flag {
pub fn from_raw(raw: impl Into<String>) -> Self {
let raw = raw.into();
let iana = classify_iana(&raw);
Self { raw, iana }
}
pub fn from_iana(iana: IanaFlag) -> Self {
Self {
raw: canonical_raw(iana).to_string(),
iana: Some(iana),
}
}
pub fn raw(&self) -> &str {
&self.raw
}
pub fn iana(&self) -> Option<IanaFlag> {
self.iana
}
pub fn is_seen(&self) -> bool {
matches!(self.iana, Some(IanaFlag::Seen))
}
pub fn is_answered(&self) -> bool {
matches!(self.iana, Some(IanaFlag::Answered))
}
pub fn is_flagged(&self) -> bool {
matches!(self.iana, Some(IanaFlag::Flagged))
}
pub fn is_draft(&self) -> bool {
matches!(self.iana, Some(IanaFlag::Draft))
}
pub fn is_deleted(&self) -> bool {
matches!(self.iana, Some(IanaFlag::Deleted))
}
pub fn is_forwarded(&self) -> bool {
matches!(self.iana, Some(IanaFlag::Forwarded))
}
pub fn is_junk(&self) -> bool {
matches!(self.iana, Some(IanaFlag::Junk))
}
pub fn is_notjunk(&self) -> bool {
matches!(self.iana, Some(IanaFlag::NotJunk))
}
pub fn is_phishing(&self) -> bool {
matches!(self.iana, Some(IanaFlag::Phishing))
}
pub fn is_important(&self) -> bool {
matches!(self.iana, Some(IanaFlag::Important))
}
pub fn is_mdnsent(&self) -> bool {
matches!(self.iana, Some(IanaFlag::MdnSent))
}
}
impl PartialEq for Flag {
fn eq(&self, other: &Self) -> bool {
match (self.iana, other.iana) {
(Some(a), Some(b)) => a == b,
(None, None) => self.raw.eq_ignore_ascii_case(&other.raw),
_ => false,
}
}
}
impl Eq for Flag {}
impl Ord for Flag {
fn cmp(&self, other: &Self) -> Ordering {
match (self.iana, other.iana) {
(Some(a), Some(b)) => a.cmp(&b),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => self
.raw
.to_ascii_lowercase()
.cmp(&other.raw.to_ascii_lowercase()),
}
}
}
impl PartialOrd for Flag {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Hash for Flag {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
match self.iana {
Some(iana) => {
0u8.hash(state);
iana.hash(state);
}
None => {
1u8.hash(state);
for b in self.raw.as_bytes() {
b.to_ascii_lowercase().hash(state);
}
}
}
}
}
pub fn classify_iana(raw: &str) -> Option<IanaFlag> {
let stripped = raw
.strip_prefix('\\')
.or_else(|| raw.strip_prefix('$'))
.unwrap_or(raw);
match stripped.to_ascii_lowercase().as_str() {
"seen" => Some(IanaFlag::Seen),
"answered" => Some(IanaFlag::Answered),
"flagged" => Some(IanaFlag::Flagged),
"draft" => Some(IanaFlag::Draft),
"deleted" => Some(IanaFlag::Deleted),
"forwarded" => Some(IanaFlag::Forwarded),
"junk" => Some(IanaFlag::Junk),
"notjunk" => Some(IanaFlag::NotJunk),
"phishing" => Some(IanaFlag::Phishing),
"important" => Some(IanaFlag::Important),
"mdnsent" => Some(IanaFlag::MdnSent),
_ => None,
}
}
fn canonical_raw(iana: IanaFlag) -> &'static str {
match iana {
IanaFlag::Seen => "\\Seen",
IanaFlag::Answered => "\\Answered",
IanaFlag::Flagged => "\\Flagged",
IanaFlag::Draft => "\\Draft",
IanaFlag::Deleted => "\\Deleted",
IanaFlag::Forwarded => "$Forwarded",
IanaFlag::Junk => "$Junk",
IanaFlag::NotJunk => "$NotJunk",
IanaFlag::Phishing => "$Phishing",
IanaFlag::Important => "$Important",
IanaFlag::MdnSent => "$MDNSent",
}
}
#[derive(Clone, Copy, Debug)]
pub enum FlagOp {
Add,
Set,
Remove,
}
#[cfg(test)]
mod tests {
use alloc::collections::BTreeSet;
use super::*;
#[test]
fn classify_strips_prefix_and_is_case_insensitive() {
assert_eq!(classify_iana("\\Seen"), Some(IanaFlag::Seen));
assert_eq!(classify_iana("$seen"), Some(IanaFlag::Seen));
assert_eq!(classify_iana("SEEN"), Some(IanaFlag::Seen));
assert_eq!(classify_iana("$MDNSent"), Some(IanaFlag::MdnSent));
assert_eq!(classify_iana("foo"), None);
}
#[test]
fn from_raw_populates_iana_when_recognised() {
let f = Flag::from_raw("\\Seen");
assert_eq!(f.raw(), "\\Seen");
assert_eq!(f.iana(), Some(IanaFlag::Seen));
let f = Flag::from_raw("custom-label");
assert_eq!(f.raw(), "custom-label");
assert_eq!(f.iana(), None);
}
#[test]
fn iana_uses_canonical_spelling() {
assert_eq!(Flag::from_iana(IanaFlag::Seen).raw(), "\\Seen");
assert_eq!(Flag::from_iana(IanaFlag::Forwarded).raw(), "$Forwarded");
assert_eq!(Flag::from_iana(IanaFlag::MdnSent).raw(), "$MDNSent");
}
#[test]
fn equality_collapses_wire_variants() {
assert_eq!(Flag::from_raw("\\Seen"), Flag::from_raw("$seen"));
assert_eq!(Flag::from_raw("\\Seen"), Flag::from_iana(IanaFlag::Seen));
assert_eq!(Flag::from_raw("FOO"), Flag::from_raw("foo"));
assert_ne!(Flag::from_raw("foo"), Flag::from_iana(IanaFlag::Seen));
assert_ne!(Flag::from_raw("foo"), Flag::from_raw("bar"));
}
#[test]
fn btreeset_dedupes_across_spellings() {
let mut set: BTreeSet<Flag> = BTreeSet::new();
set.insert(Flag::from_raw("\\Seen"));
set.insert(Flag::from_raw("$seen"));
set.insert(Flag::from_iana(IanaFlag::Seen));
set.insert(Flag::from_raw("custom"));
set.insert(Flag::from_raw("CUSTOM"));
assert_eq!(set.len(), 2);
}
#[test]
fn predicates_match_iana_only() {
assert!(Flag::from_iana(IanaFlag::Seen).is_seen());
assert!(!Flag::from_raw("seen-ish").is_seen());
assert!(Flag::from_iana(IanaFlag::Deleted).is_deleted());
assert!(Flag::from_iana(IanaFlag::Forwarded).is_forwarded());
}
}