1use std::collections::BTreeSet;
2use std::ffi::OsString;
3use std::fmt::{self, Display};
4use std::fs::File;
5use std::io::{self, Write};
6use std::ops::{Deref, DerefMut};
7use std::path::{Path, PathBuf};
8
9#[derive(thiserror::Error, Debug)]
11pub enum Error {
12 #[error("invalid flag name: {0}")]
13 InvalidFlagName(String),
14 #[error("I/O error: {0}")]
15 IOError(#[from] std::io::Error),
16}
17
18#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
26pub enum Flag {
27 Seen,
28 Answered,
29 Forwarded,
30 Flagged,
31 Deleted,
32 Draft,
33 Important,
34 MDNSent,
35 Junk,
36 NotJunk,
37 Phishing,
38 Custom(String),
39}
40
41impl TryFrom<&str> for Flag {
42 type Error = Error;
43
44 fn try_from(flag: &str) -> Result<Self, Error> {
45 Ok(match flag {
46 "$seen" => Flag::Seen,
47 "$answered" => Flag::Answered,
48 "$Forwarded" => Flag::Forwarded,
49 "$flagged" => Flag::Flagged,
50 "$Deleted" => Flag::Deleted,
51 "$draft" => Flag::Draft,
52 "$Important" => Flag::Important,
53 "$MDNSent" => Flag::MDNSent,
54 "$Junk" => Flag::Junk,
55 "$NotJunk" => Flag::NotJunk,
56 "$Phishing" => Flag::Phishing,
57 _ => {
58 if flag.is_empty() || flag.contains('\n') {
59 return Err(Error::InvalidFlagName(flag.to_string()));
60 }
61 Flag::Custom(flag.to_string())
62 }
63 })
64 }
65}
66
67impl Display for Flag {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 let s = match self {
70 Flag::Seen => "$seen",
71 Flag::Answered => "$answered",
72 Flag::Forwarded => "$Forwarded",
73 Flag::Flagged => "$flagged",
74 Flag::Deleted => "$Deleted",
75 Flag::Draft => "$draft",
76 Flag::Important => "$Important",
77 Flag::MDNSent => "$MDNSent",
78 Flag::Junk => "$Junk",
79 Flag::NotJunk => "$NotJunk",
80 Flag::Phishing => "$Phishing",
81 Flag::Custom(c) => c,
82 };
83 write!(f, "{}", s)
84 }
85}
86
87#[derive(Debug, Eq, PartialEq, PartialOrd)]
89pub struct Flags(BTreeSet<Flag>);
90
91impl Flags {
92 pub fn new() -> Self {
93 Flags(BTreeSet::new())
94 }
95
96 pub fn parse_string(s: &str) -> Result<Self, Error> {
100 if s.is_empty() {
101 return Ok(Flags::new());
102 }
103 s.trim_end().split('\n').map(Flag::try_from).collect()
104 }
105
106 pub fn parse_file(f: File) -> Result<Self, Error> {
112 let s = io::read_to_string(&f)?;
115 Flags::parse_string(&s)
116 }
117
118 pub fn write_string(&self) -> String {
126 let mut result = String::new();
127 for flag in &self.0 {
128 result.push_str(&format!("{}\n", flag));
129 }
130 result
131 }
132
133 #[allow(clippy::write_with_newline)]
139 pub fn write_file(&self, f: &mut impl Write) -> io::Result<()> {
140 for flag in &self.0 {
141 write!(f, "{}\n", flag)?;
142 }
143 f.flush()
144 }
145}
146
147impl Default for Flags {
148 fn default() -> Self {
149 Self::new()
150 }
151}
152
153impl Deref for Flags {
154 type Target = BTreeSet<Flag>;
155 fn deref(&self) -> &Self::Target {
156 &self.0
157 }
158}
159
160impl DerefMut for Flags {
161 fn deref_mut(&mut self) -> &mut Self::Target {
162 &mut self.0
163 }
164}
165
166impl<const N: usize> std::convert::From<[Flag; N]> for Flags {
167 fn from(arr: [Flag; N]) -> Self {
168 Flags(BTreeSet::from(arr))
169 }
170}
171
172impl std::convert::From<&BTreeSet<Flag>> for Flags {
173 fn from(set: &BTreeSet<Flag>) -> Self {
174 let mut c = BTreeSet::new();
175 for i in set {
176 c.insert(i.clone());
177 }
178 Flags(c)
179 }
180}
181
182impl FromIterator<Flag> for Flags {
183 fn from_iter<T: IntoIterator<Item = Flag>>(iter: T) -> Self {
184 let mut set = BTreeSet::new();
185 for i in iter {
186 set.insert(i);
187 }
188 Flags(set)
189 }
190}
191
192impl IntoIterator for Flags {
193 type Item = Flag;
194 type IntoIter = <BTreeSet<Flag> as IntoIterator>::IntoIter;
195 fn into_iter(self) -> Self::IntoIter {
196 self.0.into_iter()
197 }
198}
199
200impl fmt::Display for Flags {
201 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202 let mut first = true;
203
204 write!(f, "(")?;
205 for flag in &self.0 {
206 if !first {
207 write!(f, ", {}", flag)?;
208 } else {
209 write!(f, "{}", flag)?;
210 first = false;
211 }
212 }
213 write!(f, ")")?;
214 Ok(())
215 }
216}
217
218pub(crate) fn flags_path_for(dir: impl AsRef<Path>, id: &str) -> PathBuf {
219 PathBuf::from_iter([
220 dir.as_ref().as_os_str(),
221 &OsString::from(".meta"),
222 &OsString::from(format!("{}.flags", id)),
223 ])
224}
225
226#[cfg(test)]
227mod tests {
228 use tempfile::NamedTempFile;
229
230 use super::*;
231
232 #[test]
233 fn test_flag() {
234 assert_eq!(Flag::try_from("$seen").unwrap(), Flag::Seen);
235 assert_eq!(Flag::try_from("$Forwarded").unwrap(), Flag::Forwarded);
236 assert_eq!(
237 Flag::try_from("myflag").unwrap(),
238 Flag::Custom(String::from("myflag"))
239 );
240 assert!(Flag::try_from("my\nflag").is_err());
241 assert!(Flag::try_from("").is_err());
242 }
243
244 #[test]
245 fn test_flags() {
246 let set = Flags::from([Flag::Answered]);
247 assert_eq!(set.to_string(), String::from("($answered)"));
248
249 let set = Flags::from([Flag::Forwarded, Flag::Answered]);
250 assert_eq!(set.to_string(), String::from("($answered, $Forwarded)"));
251
252 let set = Flags::from([
253 Flag::Custom(String::from("myflag")),
254 Flag::Forwarded,
255 Flag::Answered,
256 ]);
257 assert_eq!(
258 set.to_string(),
259 String::from("($answered, $Forwarded, myflag)")
260 );
261 }
262
263 #[test]
264 fn test_serialize_string() {
265 let set = Flags::from([
266 Flag::Custom(String::from("myflag")),
267 Flag::Forwarded,
268 Flag::Answered,
269 ]);
270 assert_eq!(
271 set.to_string(),
272 String::from("($answered, $Forwarded, myflag)")
273 );
274
275 let ser = set.write_string();
276 let set2 = Flags::parse_string(&ser).unwrap();
277 assert_eq!(set, set2);
278 }
279
280 #[test]
281 fn test_serialize_file() {
282 let set = Flags::from([
283 Flag::Custom(String::from("myflag")),
284 Flag::Forwarded,
285 Flag::Answered,
286 ]);
287 assert_eq!(
288 set.to_string(),
289 String::from("($answered, $Forwarded, myflag)")
290 );
291
292 let mut f = NamedTempFile::new().unwrap();
293
294 set.write_file(&mut f).unwrap();
295
296 let r = File::open(f.path()).unwrap();
297
298 let set2 = Flags::parse_file(r).unwrap();
299 assert_eq!(set, set2);
300 }
301}