1use std::path::PathBuf;
6
7mod flags;
8mod m2dir;
9mod m2store;
10mod message;
11mod util;
12
13pub use flags::{Error as FlagsError, Flag, Flags};
14pub use m2dir::M2dir;
15pub use m2store::{Folder, Folders, M2store};
16pub use message::{Message, Messages};
17
18#[derive(thiserror::Error, Debug)]
20pub enum Error {
21 #[error("error storing message: too many duplicate messages")]
24 TooManyDuplicates,
25 #[error("error handling flags in {0}: {1}")]
26 Flags(PathBuf, #[source] FlagsError),
27 #[error("invalid folder name: {0}")]
28 InvalidFolderName(String),
29 #[error("no such file or directory")]
30 FolderNotFound,
31 #[error("invalid file name: {0}")]
32 InvalidFileName(PathBuf),
33 #[error("IO error: {0}")]
34 IO(#[from] std::io::Error),
35 #[error("error writing flags to {0}: {1}")]
36 WriteFlags(PathBuf, #[source] std::io::Error),
37 #[error("message not found")]
38 MessageNotFound,
39 #[error("failed to parse message")]
40 Parse(),
41 #[error("IO error walking directory: {0}")]
42 WalkDir(#[from] walkdir::Error),
43}
44
45#[cfg(test)]
46mod tests {
47 use std::{
48 fs::OpenOptions,
49 io::Write,
50 path::Path,
51 time::{Duration, SystemTime},
52 };
53
54 use lettre::message::header::Date;
55 use tempfile::{tempdir, TempDir};
56
57 use flags::Flag;
58
59 use super::*;
60
61 const TEST_MAIL_BODY: &[u8] = b"Date: Sun, 29 Nov 2020 03:49:23 +0000
62From: \"Conrad Hoffmann\" <ch@bitfehler.net>
63To: \"Testy McTestface\" <test@bitfehler.net>
64Subject: Test email that is about to be delivered
65Content-Transfer-Encoding: 7bit
66
67With a test body";
68
69 fn gen_message(from: &str, to: &str, subject: &str, body: &str) -> lettre::Message {
70 let date = Date::from(SystemTime::UNIX_EPOCH + Duration::from_secs(1606621763));
71 lettre::message::Message::builder()
72 .header(date)
73 .from(from.parse().unwrap())
74 .to(to.parse().unwrap())
75 .subject(subject)
76 .body(body.to_string())
77 .unwrap()
78 .into()
79 }
80
81 pub(crate) fn generate_test_data(target: impl AsRef<Path>) {
82 let store = M2store::create(&target).unwrap();
83
84 let inbox = store.create_folder("INBOX").unwrap();
86
87 let mgs = gen_message(
88 "Conrad Hoffmann <ch@bitfehler.net>",
89 "Testy McTestface <test@bitfehler.net>",
90 "Test email that is marked as read",
91 "With a test body",
92 );
93 inbox
94 .as_ref()
95 .store(&mgs.formatted(), &Flags::from([Flag::Seen]))
96 .unwrap();
97
98 let mgs = gen_message(
99 "Conrad Hoffmann <ch@bitfehler.net>",
100 "Testy McTestface <test@bitfehler.net>",
101 "Test email that is unread",
102 "And will hence not have any flags",
103 );
104 inbox.as_ref().deliver(&mgs.formatted()).unwrap();
105
106 let mgs = gen_message(
107 "Conrad Hoffmann <ch@bitfehler.net>",
108 "Testy McTestface <test@bitfehler.net>",
109 "Test email with a custom flag",
110 "With a test body",
111 );
112 inbox
113 .as_ref()
114 .store(
115 &mgs.formatted(),
116 &Flags::from([Flag::Seen, Flag::Custom("todo".to_string())]),
117 )
118 .unwrap();
119
120 let mgs = gen_message(
121 "Conrad Hoffmann <ch@bitfehler.net>",
122 "Testy McTestface <test@bitfehler.net>",
123 "Duplicate test email",
124 "Which should work just fine, we'll use different flags so this can be easily verified",
125 );
126 inbox
127 .as_ref()
128 .store(&mgs.formatted(), &Flags::from([Flag::Seen]))
129 .unwrap();
130 inbox
131 .as_ref()
132 .store(&mgs.formatted(), &Flags::from([Flag::Seen, Flag::Answered]))
133 .unwrap();
134
135 let folder = store.create_folder("lists/m2dir-dev").unwrap();
138
139 let mgs = gen_message(
140 "Conrad Hoffmann <ch@bitfehler.net>",
141 "Fake M2dir ML <m2dir@example.com>",
142 "Test email in a nested subfolder",
143 "Whose parent folder is not an m2dir (contains no emails)",
144 );
145 folder
146 .as_ref()
147 .store(&mgs.formatted(), &Flags::from([Flag::Seen]))
148 .unwrap();
149
150 let folder = store.create_folder("folder").unwrap();
151
152 let mgs = gen_message(
153 "Conrad Hoffmann <ch@bitfehler.net>",
154 "Testy McTestface <test@bitfehler.net>",
155 "Test email in a simple folder",
156 "Nothing special here",
157 );
158 folder
159 .as_ref()
160 .store(
161 &mgs.formatted(),
162 &Flags::from([
163 Flag::Answered,
164 Flag::Seen,
165 Flag::Custom("patch".to_string()),
166 ]),
167 )
168 .unwrap();
169
170 let folder = store.create_folder("folder/subfolder").unwrap();
171
172 let mgs = gen_message(
173 "Conrad Hoffmann <ch@bitfehler.net>",
174 "Testy McTestface <test@bitfehler.net>",
175 "Test email in a nested folder",
176 "Whose parent is also a folder with messages",
177 );
178 folder
179 .as_ref()
180 .store(&mgs.formatted(), &Flags::from([Flag::Seen]))
181 .unwrap();
182
183 let folder = store.create_folder("brokenflags").unwrap();
184
185 let mgs = gen_message(
186 "Conrad Hoffmann <ch@bitfehler.net>",
187 "Testy McTestface <test@bitfehler.net>",
188 "Test email with broken flags file",
189 "To test error handling",
190 );
191 let m = folder
192 .as_ref()
193 .store(&mgs.formatted(), &Flags::from([Flag::Seen]))
194 .unwrap();
195 let mut f = OpenOptions::new()
196 .append(true)
197 .open(m.flags_path())
198 .unwrap();
199 f.write("\n\\InvalidFlag\n".as_ref()).unwrap();
200
201 }
203
204 fn setup(dir: &TempDir, folder: &str) -> M2dir {
205 let m2dir = PathBuf::from_iter([dir.as_ref(), folder.as_ref()]);
206 generate_test_data(dir.as_ref());
207 M2dir::try_from(m2dir.as_ref()).unwrap()
208 }
209
210 #[test]
211 fn test_load_single() {
212 let tmpdir = tempdir().unwrap();
213 let m2dir = setup(&tmpdir, "folder");
214
215 assert_eq!(m2dir.count(), 1);
216
217 let msgs = m2dir.list();
218 let msg = msgs.into_iter().next().unwrap().unwrap();
219
220 assert!(msg
221 .path
222 .file_name()
223 .unwrap()
224 .to_string_lossy()
225 .starts_with("2020-11-29_04:49_ch@bitfehler.net,3wAAALlW9_qepw6p"));
226
227 let loaded_flags = msg.flags().unwrap();
228 let flags = Flags::from([
229 Flag::Answered,
230 Flag::Seen,
231 Flag::Custom(String::from("patch")),
232 ]);
233
234 assert_eq!(flags, loaded_flags);
235 }
236
237 #[test]
238 fn test_deliver() {
239 let tmpdir = tempdir().unwrap();
240 let m2dir = M2dir::create(tmpdir.path()).unwrap();
241 let msg = m2dir.deliver(TEST_MAIL_BODY).unwrap();
242
243 eprintln!("{}", msg.path.file_name().unwrap().to_string_lossy());
244 assert!(msg
245 .path
246 .file_name()
247 .unwrap()
248 .to_string_lossy()
249 .starts_with("2020-11-29_04:49_ch@bitfehler.net,4AAAADUevRZ_R3P7"));
250
251 assert_eq!(msg.flags().unwrap().len(), 0);
252 }
253
254 #[test]
255 fn test_store() {
256 let tmpdir = tempdir().unwrap();
257 let m2dir = M2dir::create(tmpdir.path()).unwrap();
258 let flags = Flags::from([Flag::Answered, Flag::Seen]);
259 let msg = m2dir.store(TEST_MAIL_BODY, &flags).unwrap();
260
261 assert!(msg
262 .path
263 .file_name()
264 .unwrap()
265 .to_string_lossy()
266 .starts_with("2020-11-29_04:49_ch@bitfehler.net,4AAAADUevRZ_R3P7"));
267
268 let loaded_flags = msg.flags().unwrap();
269
270 assert_eq!(flags, loaded_flags);
271 }
272
273 #[test]
274 fn test_copy() {
275 let srcdir = tempdir().unwrap();
276
277 let m2dir = setup(&srcdir, "folder");
278 let srcfolder = m2dir.path();
279
280 let msgs = m2dir.list();
281 let msg = msgs.into_iter().next().unwrap().unwrap();
282
283 let dstdir = tempdir().unwrap();
284 let m2dir = M2dir::create(dstdir.path()).unwrap();
285
286 assert_eq!(m2dir.count(), 0);
287
288 let cpy = msg.copy_to(&m2dir).unwrap();
289
290 assert_eq!(m2dir.count(), 1);
291 assert_eq!(msg.path.parent().unwrap(), srcfolder);
292 assert_eq!(cpy.path.parent().unwrap(), dstdir.path());
293
294 assert!(cpy
295 .path
296 .file_name()
297 .unwrap()
298 .to_string_lossy()
299 .starts_with("2020-11-29_04:49_ch@bitfehler.net,3wAAALlW9_qepw6p"));
300
301 let loaded_flags = cpy.flags().unwrap();
302 let flags = Flags::from([
303 Flag::Answered,
304 Flag::Seen,
305 Flag::Custom(String::from("patch")),
306 ]);
307
308 assert_eq!(flags, loaded_flags);
309
310 assert!(cpy.copy_to(&m2dir).is_err());
312 }
313
314 #[test]
315 fn test_move() {
316 let srcdir = tempdir().unwrap();
317 let m2dir = setup(&srcdir, "folder");
318
319 let msgs = m2dir.list();
320 let mut msg = msgs.into_iter().next().unwrap().unwrap();
321
322 let dstdir = tempdir().unwrap();
323 let m2dir = M2dir::create(dstdir.path()).unwrap();
324
325 assert_eq!(m2dir.count(), 0);
326
327 msg.move_to(&m2dir).unwrap();
328
329 assert_eq!(m2dir.count(), 1);
330 assert_eq!(msg.path.parent().unwrap(), dstdir.path());
331
332 assert!(msg
333 .path
334 .file_name()
335 .unwrap()
336 .to_string_lossy()
337 .starts_with("2020-11-29_04:49_ch@bitfehler.net,3wAAALlW9_qepw6p"));
338
339 let loaded_flags = msg.flags().unwrap();
340 let flags = Flags::from([
341 Flag::Answered,
342 Flag::Seen,
343 Flag::Custom(String::from("patch")),
344 ]);
345
346 assert_eq!(flags, loaded_flags);
347 }
348
349 #[test]
350 fn test_invalid_flags() {
351 let srcdir = tempdir().unwrap();
352 let m2dir = setup(&srcdir, "brokenflags");
353
354 let msgs = m2dir.list();
355 let msg = msgs.into_iter().next().unwrap().unwrap();
356
357 assert!(msg.flags().is_err());
358 }
359
360 #[test]
361 #[ignore]
362 fn generate() {
363 let target = std::env::var("M2DIR_GENERATE_TEST_DATA")
366 .expect("Env var M2DIR_GENERATE_TEST_DATA must be set to target path");
367 generate_test_data(target)
368 }
369}