1use core::fmt;
4use core::fmt::Write as _;
5use core::str::FromStr;
6
7use alloc::{
8 collections::{BTreeMap, BTreeSet},
9 string::String,
10 vec::Vec,
11};
12
13use log::trace;
14
15use crate::path::FsPath;
16
17#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
19pub struct MaildirFlags {
20 flags: BTreeSet<MaildirFlag>,
21 extra_letters: BTreeSet<char>,
24}
25
26impl From<&FsPath> for MaildirFlags {
27 fn from(path: &FsPath) -> Self {
28 let Some(file_name) = path.file_name() else {
29 return Default::default();
30 };
31
32 let Some((_, flags)) = file_name.rsplit_once(',') else {
33 return Default::default();
34 };
35
36 MaildirFlags::from_iter(flags.chars().filter_map(MaildirFlag::from_char))
37 }
38}
39
40impl fmt::Display for MaildirFlags {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 for flag in &self.flags {
45 write!(f, "{flag}")?;
46 }
47 for letter in &self.extra_letters {
48 f.write_char(*letter)?;
49 }
50 Ok(())
51 }
52}
53
54impl MaildirFlags {
55 pub fn is_empty(&self) -> bool {
56 self.flags.is_empty() && self.extra_letters.is_empty()
57 }
58
59 pub fn len(&self) -> usize {
60 self.flags.len() + self.extra_letters.len()
61 }
62
63 pub fn contains(&self, flag: &MaildirFlag) -> bool {
64 self.flags.contains(flag)
65 }
66
67 pub fn extend(&mut self, flags: MaildirFlags) {
68 self.flags.extend(flags.flags);
69 self.extra_letters.extend(flags.extra_letters);
70 }
71
72 pub fn difference(&mut self, flags: &MaildirFlags) {
73 self.flags = self.flags.difference(&flags.flags).cloned().collect();
74 self.extra_letters = self
75 .extra_letters
76 .difference(&flags.extra_letters)
77 .copied()
78 .collect();
79 }
80
81 pub fn iter(&self) -> impl Iterator<Item = &MaildirFlag> {
82 self.flags.iter()
83 }
84
85 pub fn insert(&mut self, flag: MaildirFlag) -> bool {
86 self.flags.insert(flag)
87 }
88
89 pub fn with_dovecot(path: &FsPath, table: &BTreeMap<char, String>) -> Self {
92 let Some(file_name) = path.file_name() else {
93 return Default::default();
94 };
95
96 let Some((_, letters)) = file_name.rsplit_once(',') else {
97 return Default::default();
98 };
99
100 let mut flags = BTreeSet::new();
101 for c in letters.chars() {
102 if let Some(flag) = MaildirFlag::from_char(c) {
103 flags.insert(flag);
104 } else if c.is_ascii_lowercase() {
105 if let Some(name) = table.get(&c) {
106 flags.insert(MaildirFlag::Keyword(name.clone()));
107 }
108 }
109 }
110 MaildirFlags {
111 flags,
112 extra_letters: BTreeSet::new(),
113 }
114 }
115
116 pub fn extend_keywords<I, S>(&mut self, keywords: I)
118 where
119 I: IntoIterator<Item = S>,
120 S: Into<String>,
121 {
122 for k in keywords {
123 self.flags.insert(MaildirFlag::Keyword(k.into()));
124 }
125 }
126
127 pub fn extend_letters<I>(&mut self, letters: I)
130 where
131 I: IntoIterator<Item = char>,
132 {
133 self.extra_letters.extend(letters);
134 }
135
136 pub fn drain_keywords(&mut self) -> Vec<String> {
139 let keywords: BTreeSet<MaildirFlag> = self
140 .flags
141 .iter()
142 .filter(|f| matches!(f, MaildirFlag::Keyword(_)))
143 .cloned()
144 .collect();
145
146 for f in &keywords {
147 self.flags.remove(f);
148 }
149
150 keywords
151 .into_iter()
152 .filter_map(|f| match f {
153 MaildirFlag::Keyword(s) => Some(s),
154 _ => None,
155 })
156 .collect()
157 }
158}
159
160impl FromIterator<MaildirFlag> for MaildirFlags {
161 fn from_iter<I: IntoIterator<Item = MaildirFlag>>(iter: I) -> Self {
162 MaildirFlags {
163 flags: iter.into_iter().collect(),
164 extra_letters: BTreeSet::new(),
165 }
166 }
167}
168
169#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
171pub enum MaildirFlag {
172 Passed,
173 Replied,
174 Seen,
175 Trashed,
176 Draft,
177 Flagged,
178 Keyword(String),
181}
182
183impl MaildirFlag {
184 pub fn from_char(c: char) -> Option<MaildirFlag> {
185 match c {
186 'P' => Some(MaildirFlag::Passed),
187 'R' => Some(MaildirFlag::Replied),
188 'S' => Some(MaildirFlag::Seen),
189 'T' => Some(MaildirFlag::Trashed),
190 'D' => Some(MaildirFlag::Draft),
191 'F' => Some(MaildirFlag::Flagged),
192 c => {
193 trace!("invalid maildir flag `{c}`, ignoring");
194 None
195 }
196 }
197 }
198
199 pub fn keyword(s: impl Into<String>) -> Self {
200 Self::Keyword(s.into())
201 }
202
203 pub fn as_keyword(&self) -> Option<&str> {
204 match self {
205 Self::Keyword(s) => Some(s.as_str()),
206 _ => None,
207 }
208 }
209}
210
211impl fmt::Display for MaildirFlag {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 match self {
214 Self::Passed => write!(f, "P"),
215 Self::Replied => write!(f, "R"),
216 Self::Seen => write!(f, "S"),
217 Self::Trashed => write!(f, "T"),
218 Self::Draft => write!(f, "D"),
219 Self::Flagged => write!(f, "F"),
220 Self::Keyword(_) => Ok(()),
223 }
224 }
225}
226
227#[derive(Clone, Copy, Debug, Eq, PartialEq)]
232pub enum KeywordHeader {
233 XKeywords,
234 XLabel,
235}
236
237impl KeywordHeader {
238 pub fn header_name(&self) -> &'static str {
239 match self {
240 Self::XKeywords => "X-Keywords",
241 Self::XLabel => "X-Label",
242 }
243 }
244
245 pub fn separator(&self) -> char {
246 match self {
247 Self::XKeywords => ',',
248 Self::XLabel => ' ',
249 }
250 }
251}
252
253impl FromStr for KeywordHeader {
254 type Err = &'static str;
255
256 fn from_str(s: &str) -> Result<Self, Self::Err> {
257 match s.to_ascii_lowercase().as_str() {
258 "x-keywords" | "xkeywords" | "x_keywords" => Ok(Self::XKeywords),
259 "x-label" | "xlabel" | "x_label" => Ok(Self::XLabel),
260 _ => Err("expected `x-keywords` or `x-label`"),
261 }
262 }
263}
264
265impl fmt::Display for KeywordHeader {
266 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
267 f.write_str(self.header_name())
268 }
269}