1use core::{cmp::Ordering, hash::Hash};
24
25use alloc::string::{String, ToString};
26
27#[derive(Clone, Debug)]
35#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
36pub struct Flag {
37 raw: String,
38 iana: Option<IanaFlag>,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[cfg_attr(feature = "serde", serde(rename_all = "kebab-case"))]
50pub enum IanaFlag {
51 Seen,
52 Answered,
53 Flagged,
54 Draft,
55 Deleted,
60 Forwarded,
61 Junk,
62 NotJunk,
63 Phishing,
64 Important,
65 MdnSent,
66}
67
68impl Flag {
69 pub fn from_raw(raw: impl Into<String>) -> Self {
73 let raw = raw.into();
74 let iana = classify_iana(&raw);
75 Self { raw, iana }
76 }
77
78 pub fn from_iana(iana: IanaFlag) -> Self {
83 Self {
84 raw: canonical_raw(iana).to_string(),
85 iana: Some(iana),
86 }
87 }
88
89 pub fn raw(&self) -> &str {
92 &self.raw
93 }
94
95 pub fn iana(&self) -> Option<IanaFlag> {
98 self.iana
99 }
100
101 pub fn is_seen(&self) -> bool {
102 matches!(self.iana, Some(IanaFlag::Seen))
103 }
104
105 pub fn is_answered(&self) -> bool {
106 matches!(self.iana, Some(IanaFlag::Answered))
107 }
108
109 pub fn is_flagged(&self) -> bool {
110 matches!(self.iana, Some(IanaFlag::Flagged))
111 }
112
113 pub fn is_draft(&self) -> bool {
114 matches!(self.iana, Some(IanaFlag::Draft))
115 }
116
117 pub fn is_deleted(&self) -> bool {
118 matches!(self.iana, Some(IanaFlag::Deleted))
119 }
120
121 pub fn is_forwarded(&self) -> bool {
122 matches!(self.iana, Some(IanaFlag::Forwarded))
123 }
124
125 pub fn is_junk(&self) -> bool {
126 matches!(self.iana, Some(IanaFlag::Junk))
127 }
128
129 pub fn is_notjunk(&self) -> bool {
130 matches!(self.iana, Some(IanaFlag::NotJunk))
131 }
132
133 pub fn is_phishing(&self) -> bool {
134 matches!(self.iana, Some(IanaFlag::Phishing))
135 }
136
137 pub fn is_important(&self) -> bool {
138 matches!(self.iana, Some(IanaFlag::Important))
139 }
140
141 pub fn is_mdnsent(&self) -> bool {
142 matches!(self.iana, Some(IanaFlag::MdnSent))
143 }
144}
145
146impl PartialEq for Flag {
147 fn eq(&self, other: &Self) -> bool {
148 match (self.iana, other.iana) {
149 (Some(a), Some(b)) => a == b,
150 (None, None) => self.raw.eq_ignore_ascii_case(&other.raw),
151 _ => false,
152 }
153 }
154}
155
156impl Eq for Flag {}
157
158impl Ord for Flag {
159 fn cmp(&self, other: &Self) -> Ordering {
165 match (self.iana, other.iana) {
166 (Some(a), Some(b)) => a.cmp(&b),
167 (Some(_), None) => Ordering::Less,
168 (None, Some(_)) => Ordering::Greater,
169 (None, None) => self
170 .raw
171 .to_ascii_lowercase()
172 .cmp(&other.raw.to_ascii_lowercase()),
173 }
174 }
175}
176
177impl PartialOrd for Flag {
178 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
179 Some(self.cmp(other))
180 }
181}
182
183impl Hash for Flag {
184 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
187 match self.iana {
188 Some(iana) => {
189 0u8.hash(state);
190 iana.hash(state);
191 }
192 None => {
193 1u8.hash(state);
194 for b in self.raw.as_bytes() {
195 b.to_ascii_lowercase().hash(state);
196 }
197 }
198 }
199 }
200}
201
202pub fn classify_iana(raw: &str) -> Option<IanaFlag> {
208 let stripped = raw
209 .strip_prefix('\\')
210 .or_else(|| raw.strip_prefix('$'))
211 .unwrap_or(raw);
212
213 match stripped.to_ascii_lowercase().as_str() {
214 "seen" => Some(IanaFlag::Seen),
215 "answered" => Some(IanaFlag::Answered),
216 "flagged" => Some(IanaFlag::Flagged),
217 "draft" => Some(IanaFlag::Draft),
218 "deleted" => Some(IanaFlag::Deleted),
219 "forwarded" => Some(IanaFlag::Forwarded),
220 "junk" => Some(IanaFlag::Junk),
221 "notjunk" => Some(IanaFlag::NotJunk),
222 "phishing" => Some(IanaFlag::Phishing),
223 "important" => Some(IanaFlag::Important),
224 "mdnsent" => Some(IanaFlag::MdnSent),
225 _ => None,
226 }
227}
228
229fn canonical_raw(iana: IanaFlag) -> &'static str {
233 match iana {
234 IanaFlag::Seen => "\\Seen",
235 IanaFlag::Answered => "\\Answered",
236 IanaFlag::Flagged => "\\Flagged",
237 IanaFlag::Draft => "\\Draft",
238 IanaFlag::Deleted => "\\Deleted",
239 IanaFlag::Forwarded => "$Forwarded",
240 IanaFlag::Junk => "$Junk",
241 IanaFlag::NotJunk => "$NotJunk",
242 IanaFlag::Phishing => "$Phishing",
243 IanaFlag::Important => "$Important",
244 IanaFlag::MdnSent => "$MDNSent",
245 }
246}
247
248#[derive(Clone, Copy, Debug)]
255pub enum FlagOp {
256 Add,
257 Set,
258 Remove,
259}
260
261#[cfg(test)]
262mod tests {
263 use alloc::collections::BTreeSet;
264
265 use super::*;
266
267 #[test]
268 fn classify_strips_prefix_and_is_case_insensitive() {
269 assert_eq!(classify_iana("\\Seen"), Some(IanaFlag::Seen));
270 assert_eq!(classify_iana("$seen"), Some(IanaFlag::Seen));
271 assert_eq!(classify_iana("SEEN"), Some(IanaFlag::Seen));
272 assert_eq!(classify_iana("$MDNSent"), Some(IanaFlag::MdnSent));
273 assert_eq!(classify_iana("foo"), None);
274 }
275
276 #[test]
277 fn from_raw_populates_iana_when_recognised() {
278 let f = Flag::from_raw("\\Seen");
279 assert_eq!(f.raw(), "\\Seen");
280 assert_eq!(f.iana(), Some(IanaFlag::Seen));
281
282 let f = Flag::from_raw("custom-label");
283 assert_eq!(f.raw(), "custom-label");
284 assert_eq!(f.iana(), None);
285 }
286
287 #[test]
288 fn iana_uses_canonical_spelling() {
289 assert_eq!(Flag::from_iana(IanaFlag::Seen).raw(), "\\Seen");
290 assert_eq!(Flag::from_iana(IanaFlag::Forwarded).raw(), "$Forwarded");
291 assert_eq!(Flag::from_iana(IanaFlag::MdnSent).raw(), "$MDNSent");
292 }
293
294 #[test]
295 fn equality_collapses_wire_variants() {
296 assert_eq!(Flag::from_raw("\\Seen"), Flag::from_raw("$seen"));
297 assert_eq!(Flag::from_raw("\\Seen"), Flag::from_iana(IanaFlag::Seen));
298 assert_eq!(Flag::from_raw("FOO"), Flag::from_raw("foo"));
299 assert_ne!(Flag::from_raw("foo"), Flag::from_iana(IanaFlag::Seen));
300 assert_ne!(Flag::from_raw("foo"), Flag::from_raw("bar"));
301 }
302
303 #[test]
304 fn btreeset_dedupes_across_spellings() {
305 let mut set: BTreeSet<Flag> = BTreeSet::new();
306 set.insert(Flag::from_raw("\\Seen"));
307 set.insert(Flag::from_raw("$seen"));
308 set.insert(Flag::from_iana(IanaFlag::Seen));
309 set.insert(Flag::from_raw("custom"));
310 set.insert(Flag::from_raw("CUSTOM"));
311 assert_eq!(set.len(), 2);
312 }
313
314 #[test]
315 fn predicates_match_iana_only() {
316 assert!(Flag::from_iana(IanaFlag::Seen).is_seen());
317 assert!(!Flag::from_raw("seen-ish").is_seen());
318 assert!(Flag::from_iana(IanaFlag::Deleted).is_deleted());
319 assert!(Flag::from_iana(IanaFlag::Forwarded).is_forwarded());
320 }
321}