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> {
97 if s.is_empty() {
98 return Ok(Flags::new());
99 }
100 s.trim_end().split('\n').map(Flag::try_from).collect()
101 }
102
103 pub fn parse_file(f: File) -> Result<Self, Error> {
104 let s = io::read_to_string(&f)?;
107 Flags::parse_string(&s)
108 }
109
110 pub fn write_string(&self) -> String {
111 let mut result = String::new();
112 for flag in &self.0 {
113 result.push_str(&format!("{}\n", flag));
114 }
115 result
116 }
117
118 #[allow(clippy::write_with_newline)]
119 pub fn write_file(&self, f: &mut impl Write) -> io::Result<()> {
120 for flag in &self.0 {
121 write!(f, "{}\n", flag)?;
122 }
123 f.flush()
124 }
125}
126
127impl Default for Flags {
128 fn default() -> Self {
129 Self::new()
130 }
131}
132
133impl Deref for Flags {
134 type Target = BTreeSet<Flag>;
135 fn deref(&self) -> &Self::Target {
136 &self.0
137 }
138}
139
140impl DerefMut for Flags {
141 fn deref_mut(&mut self) -> &mut Self::Target {
142 &mut self.0
143 }
144}
145
146impl<const N: usize> std::convert::From<[Flag; N]> for Flags {
147 fn from(arr: [Flag; N]) -> Self {
148 Flags(BTreeSet::from(arr))
149 }
150}
151
152impl std::convert::From<&BTreeSet<Flag>> for Flags {
153 fn from(set: &BTreeSet<Flag>) -> Self {
154 let mut c = BTreeSet::new();
155 for i in set {
156 c.insert(i.clone());
157 }
158 Flags(c)
159 }
160}
161
162impl FromIterator<Flag> for Flags {
163 fn from_iter<T: IntoIterator<Item = Flag>>(iter: T) -> Self {
164 let mut set = BTreeSet::new();
165 for i in iter {
166 set.insert(i);
167 }
168 Flags(set)
169 }
170}
171
172impl IntoIterator for Flags {
173 type Item = Flag;
174 type IntoIter = <BTreeSet<Flag> as IntoIterator>::IntoIter;
175 fn into_iter(self) -> Self::IntoIter {
176 self.0.into_iter()
177 }
178}
179
180impl fmt::Display for Flags {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 let mut first = true;
183
184 write!(f, "(")?;
185 for flag in &self.0 {
186 if !first {
187 write!(f, ", {}", flag)?;
188 } else {
189 write!(f, "{}", flag)?;
190 first = false;
191 }
192 }
193 write!(f, ")")?;
194 Ok(())
195 }
196}
197
198pub(crate) fn flags_path_for(dir: impl AsRef<Path>, id: &str) -> PathBuf {
199 PathBuf::from_iter([
200 dir.as_ref().as_os_str(),
201 &OsString::from(".meta"),
202 &OsString::from(format!("{}.flags", id)),
203 ])
204}
205
206#[cfg(test)]
207mod tests {
208 use tempfile::NamedTempFile;
209
210 use super::*;
211
212 #[test]
213 fn test_flag() {
214 assert_eq!(Flag::try_from("$seen").unwrap(), Flag::Seen);
215 assert_eq!(Flag::try_from("$Forwarded").unwrap(), Flag::Forwarded);
216 assert_eq!(
217 Flag::try_from("myflag").unwrap(),
218 Flag::Custom(String::from("myflag"))
219 );
220 assert!(Flag::try_from("my\nflag").is_err());
221 assert!(Flag::try_from("").is_err());
222 }
223
224 #[test]
225 fn test_flags() {
226 let set = Flags::from([Flag::Answered]);
227 assert_eq!(set.to_string(), String::from("($answered)"));
228
229 let set = Flags::from([Flag::Forwarded, Flag::Answered]);
230 assert_eq!(set.to_string(), String::from("($answered, $Forwarded)"));
231
232 let set = Flags::from([
233 Flag::Custom(String::from("myflag")),
234 Flag::Forwarded,
235 Flag::Answered,
236 ]);
237 assert_eq!(
238 set.to_string(),
239 String::from("($answered, $Forwarded, myflag)")
240 );
241 }
242
243 #[test]
244 fn test_serialize_string() {
245 let set = Flags::from([
246 Flag::Custom(String::from("myflag")),
247 Flag::Forwarded,
248 Flag::Answered,
249 ]);
250 assert_eq!(
251 set.to_string(),
252 String::from("($answered, $Forwarded, myflag)")
253 );
254
255 let ser = set.write_string();
256 let set2 = Flags::parse_string(&ser).unwrap();
257 assert_eq!(set, set2);
258 }
259
260 #[test]
261 fn test_serialize_file() {
262 let set = Flags::from([
263 Flag::Custom(String::from("myflag")),
264 Flag::Forwarded,
265 Flag::Answered,
266 ]);
267 assert_eq!(
268 set.to_string(),
269 String::from("($answered, $Forwarded, myflag)")
270 );
271
272 let mut f = NamedTempFile::new().unwrap();
273
274 set.write_file(&mut f).unwrap();
275
276 let r = File::open(f.path()).unwrap();
277
278 let set2 = Flags::parse_file(r).unwrap();
279 assert_eq!(set, set2);
280 }
281}