#[derive(Debug, Clone)]
pub enum Flag {
Seen,
Answered,
Flagged,
Deleted,
Draft,
Recent,
Wildcard,
Custom(String),
}
impl Flag {
pub fn as_imap_str(&self) -> &str {
match self {
Self::Seen => "\\Seen",
Self::Answered => "\\Answered",
Self::Flagged => "\\Flagged",
Self::Deleted => "\\Deleted",
Self::Draft => "\\Draft",
Self::Recent => "\\Recent",
Self::Wildcard => "\\*",
Self::Custom(s) => s,
}
}
pub fn from_imap_str(s: &str) -> Self {
let lower = s.to_ascii_lowercase();
match lower.as_str() {
"\\seen" => Self::Seen,
"\\answered" => Self::Answered,
"\\flagged" => Self::Flagged,
"\\deleted" => Self::Deleted,
"\\draft" => Self::Draft,
"\\recent" => Self::Recent,
"\\*" => Self::Wildcard,
_ => Self::Custom(s.to_owned()),
}
}
}
impl std::fmt::Display for Flag {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_imap_str())
}
}
impl PartialEq for Flag {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Seen, Self::Seen)
| (Self::Answered, Self::Answered)
| (Self::Flagged, Self::Flagged)
| (Self::Deleted, Self::Deleted)
| (Self::Draft, Self::Draft)
| (Self::Recent, Self::Recent)
| (Self::Wildcard, Self::Wildcard) => 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 Flag {}
impl std::hash::Hash for Flag {
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);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_flags_round_trip() {
let flags = [
Flag::Seen,
Flag::Answered,
Flag::Flagged,
Flag::Deleted,
Flag::Draft,
Flag::Recent,
];
for flag in &flags {
let s = flag.as_imap_str();
let parsed = Flag::from_imap_str(s);
assert_eq!(*flag, parsed);
}
}
#[test]
fn case_insensitive_parsing() {
assert_eq!(Flag::from_imap_str("\\SEEN"), Flag::Seen);
assert_eq!(Flag::from_imap_str("\\seen"), Flag::Seen);
assert_eq!(Flag::from_imap_str("\\Seen"), Flag::Seen);
}
#[test]
fn custom_flag_preserved() {
let flag = Flag::from_imap_str("$Important");
assert_eq!(flag, Flag::Custom("$Important".into()));
}
#[test]
fn spec_audit_l1_permanent_flag_wildcard_has_dedicated_variant() {
let flag = Flag::from_imap_str("\\*");
assert!(
!matches!(flag, Flag::Custom(_)),
"\\* should have a dedicated Flag variant, not Flag::Custom; got {flag:?}"
);
assert_eq!(flag, Flag::Wildcard);
}
#[test]
fn wildcard_round_trip() {
let flag = Flag::Wildcard;
let wire = flag.as_imap_str();
assert_eq!(wire, "\\*");
let parsed = Flag::from_imap_str(wire);
assert_eq!(parsed, Flag::Wildcard);
}
#[test]
fn custom_flag_equality_is_case_insensitive() {
assert_eq!(
Flag::Custom("$Important".into()),
Flag::Custom("$important".into()),
"Custom flags must compare case-insensitively per RFC 3501 Section 2.3.2"
);
assert_eq!(
Flag::Custom("$JUNK".into()),
Flag::Custom("$Junk".into()),
"Custom flags must compare case-insensitively per RFC 3501 Section 2.3.2"
);
}
#[test]
fn custom_flag_hash_is_case_insensitive() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Flag::Custom("$Important".into()));
set.insert(Flag::Custom("$important".into()));
assert_eq!(
set.len(),
1,
"Case-insensitively equal Custom flags must have the same Hash per RFC 3501 Section 2.3.2"
);
}
#[test]
fn custom_seen_equals_seen_variant() {
assert_eq!(
Flag::Custom("\\Seen".into()),
Flag::Seen,
"Custom(\"\\\\Seen\") must equal Flag::Seen per RFC 3501 Section 2.3.2"
);
}
#[test]
fn custom_system_flag_cross_representation_hash() {
use std::collections::HashSet;
let mut set = HashSet::new();
set.insert(Flag::Seen);
set.insert(Flag::Custom("\\Seen".into()));
assert_eq!(
set.len(),
1,
"Custom(\"\\\\Seen\") and Flag::Seen must hash the same per RFC 3501 Section 2.3.2"
);
}
#[test]
fn custom_wildcard_equals_wildcard_variant() {
assert_eq!(
Flag::Custom("\\*".into()),
Flag::Wildcard,
"Custom(\"\\\\*\") must equal Flag::Wildcard per RFC 3501 Section 7.1"
);
}
}