1use chrono::{DateTime, FixedOffset};
2use mailparsing::{Header, HeaderMap, HeaderParseResult, MailParsingError, MimePart};
3use std::io::prelude::*;
4use std::io::ErrorKind;
5use std::ops::Deref;
6#[cfg(unix)]
7use std::os::unix::fs::MetadataExt;
8#[cfg(windows)]
9use std::os::windows::fs::MetadataExt;
10use std::path::{Path, PathBuf};
11use std::sync::atomic::{AtomicUsize, Ordering};
12use std::{error, fmt, fs, time};
13
14static COUNTER: AtomicUsize = AtomicUsize::new(0);
15
16#[cfg(unix)]
17const INFORMATIONAL_SUFFIX_SEPARATOR: &str = ":";
18#[cfg(windows)]
19const INFORMATIONAL_SUFFIX_SEPARATOR: &str = ";";
20
21#[derive(Debug)]
22pub enum MailEntryError {
23 IOError(std::io::Error),
24 ParseError(MailParsingError),
25 DateError(&'static str),
26 ChronoError(chrono::format::ParseError),
27}
28
29impl fmt::Display for MailEntryError {
30 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
31 match *self {
32 MailEntryError::IOError(ref err) => write!(f, "IO error: {err}"),
33 MailEntryError::ParseError(ref err) => write!(f, "Parse error: {err}"),
34 MailEntryError::DateError(ref msg) => write!(f, "Date error: {msg}"),
35 MailEntryError::ChronoError(ref err) => write!(f, "Date error: {err:#}"),
36 }
37 }
38}
39
40impl error::Error for MailEntryError {
41 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
42 match *self {
43 MailEntryError::IOError(ref err) => Some(err),
44 MailEntryError::ParseError(ref err) => Some(err),
45 MailEntryError::DateError(_) => None,
46 MailEntryError::ChronoError(ref err) => Some(err),
47 }
48 }
49}
50
51impl From<chrono::format::ParseError> for MailEntryError {
52 fn from(err: chrono::format::ParseError) -> MailEntryError {
53 MailEntryError::ChronoError(err)
54 }
55}
56
57impl From<std::io::Error> for MailEntryError {
58 fn from(err: std::io::Error) -> MailEntryError {
59 MailEntryError::IOError(err)
60 }
61}
62
63impl From<MailParsingError> for MailEntryError {
64 fn from(err: MailParsingError) -> MailEntryError {
65 MailEntryError::ParseError(err)
66 }
67}
68
69impl From<&'static str> for MailEntryError {
70 fn from(err: &'static str) -> MailEntryError {
71 MailEntryError::DateError(err)
72 }
73}
74
75enum MailData {
76 None,
77 Bytes(Vec<u8>),
78}
79
80impl MailData {
81 fn is_none(&self) -> bool {
82 match self {
83 MailData::None => true,
84 _ => false,
85 }
86 }
87
88 fn data(&self) -> Option<&[u8]> {
89 match self {
90 Self::None => None,
91 MailData::Bytes(ref b) => Some(b),
92 }
93 }
94}
95
96pub struct MailEntry {
102 id: String,
103 flags: String,
104 path: PathBuf,
105 data: MailData,
106}
107
108impl MailEntry {
109 pub fn id(&self) -> &str {
110 &self.id
111 }
112
113 pub fn read_data(&mut self) -> std::io::Result<&[u8]> {
114 if self.data.is_none() {
115 let mut f = fs::File::open(&self.path)?;
116 let mut d = Vec::<u8>::new();
117 f.read_to_end(&mut d)?;
118 self.data = MailData::Bytes(d);
119 }
120
121 match &self.data {
122 MailData::None => unreachable!(),
123 MailData::Bytes(b) => Ok(&b),
124 }
125 }
126
127 pub fn parsed(&'_ mut self) -> Result<MimePart<'_>, MailEntryError> {
128 self.read_data()?;
129 let bytes = self
130 .data
131 .data()
132 .expect("read_data should have returned an Err!");
133
134 MimePart::parse(bytes).map_err(MailEntryError::ParseError)
135 }
136
137 pub fn headers(&'_ mut self) -> Result<HeaderMap<'_>, MailEntryError> {
138 self.read_data()?;
139 let bytes = self
140 .data
141 .data()
142 .expect("read_data should have returned an Err!");
143
144 let HeaderParseResult { headers, .. } =
145 Header::parse_headers(bytes).map_err(MailEntryError::ParseError)?;
146
147 Ok(headers)
148 }
149
150 pub fn received(&mut self) -> Result<DateTime<FixedOffset>, MailEntryError> {
151 self.read_data()?;
152 let headers = self.headers()?;
153 let received = headers.get_first("Received");
154 match received {
155 Some(v) => v
156 .get_raw_value_string()
157 .map_err(MailEntryError::from)?
158 .rsplit(';')
159 .nth(0)
160 .ok_or(MailEntryError::DateError("Unable to split Received header"))
161 .and_then(|ts| DateTime::parse_from_rfc2822(ts).map_err(MailEntryError::from)),
162 None => Err("No Received header found")?,
163 }
164 }
165
166 pub fn date(&mut self) -> Result<DateTime<FixedOffset>, MailEntryError> {
167 self.read_data()?;
168 let headers = self.headers()?;
169 let date = headers.get_first("Date");
170 match date {
171 Some(ts) => ts.as_date().map_err(MailEntryError::from),
172 None => Err("No Date header found")?,
173 }
174 }
175
176 pub fn flags(&self) -> &str {
177 &self.flags
178 }
179
180 pub fn is_draft(&self) -> bool {
181 self.flags.contains('D')
182 }
183
184 pub fn is_flagged(&self) -> bool {
185 self.flags.contains('F')
186 }
187
188 pub fn is_passed(&self) -> bool {
189 self.flags.contains('P')
190 }
191
192 pub fn is_replied(&self) -> bool {
193 self.flags.contains('R')
194 }
195
196 pub fn is_seen(&self) -> bool {
197 self.flags.contains('S')
198 }
199
200 pub fn is_trashed(&self) -> bool {
201 self.flags.contains('T')
202 }
203
204 pub fn path(&self) -> &PathBuf {
205 &self.path
206 }
207}
208
209enum Subfolder {
210 New,
211 Cur,
212}
213
214pub struct MailEntries {
222 path: PathBuf,
223 subfolder: Subfolder,
224 readdir: Option<fs::ReadDir>,
225}
226
227impl MailEntries {
228 fn new(path: PathBuf, subfolder: Subfolder) -> MailEntries {
229 MailEntries {
230 path,
231 subfolder,
232 readdir: None,
233 }
234 }
235}
236
237impl Iterator for MailEntries {
238 type Item = std::io::Result<MailEntry>;
239
240 fn next(&mut self) -> Option<std::io::Result<MailEntry>> {
241 if self.readdir.is_none() {
242 let mut dir_path = self.path.clone();
243 dir_path.push(match self.subfolder {
244 Subfolder::New => "new",
245 Subfolder::Cur => "cur",
246 });
247 self.readdir = match fs::read_dir(dir_path) {
248 Err(_) => return None,
249 Ok(v) => Some(v),
250 };
251 }
252
253 loop {
254 let dir_entry = self.readdir.iter_mut().next().unwrap().next();
256 let result = dir_entry.map(|e| {
257 let entry = e?;
258 let filename = String::from(entry.file_name().to_string_lossy().deref());
259 if filename.starts_with('.') {
260 return Ok(None);
261 }
262 let (id, flags) = match self.subfolder {
263 Subfolder::New => (Some(filename.as_str()), Some("")),
264 Subfolder::Cur => {
265 let delim = format!("{}2,", INFORMATIONAL_SUFFIX_SEPARATOR);
266 let mut iter = filename.split(&delim);
267 (iter.next(), iter.next())
268 }
269 };
270 if id.is_none() || flags.is_none() {
271 return Err(std::io::Error::new(
272 std::io::ErrorKind::InvalidData,
273 "Non-maildir file found in maildir",
274 ));
275 }
276 Ok(Some(MailEntry {
277 id: String::from(id.unwrap()),
278 flags: String::from(flags.unwrap()),
279 path: entry.path(),
280 data: MailData::None,
281 }))
282 });
283 return match result {
284 None => None,
285 Some(Err(e)) => Some(Err(e)),
286 Some(Ok(None)) => continue,
287 Some(Ok(Some(v))) => Some(Ok(v)),
288 };
289 }
290 }
291}
292
293#[derive(Debug)]
294pub enum MaildirError {
295 Io(std::io::Error),
296 Utf8(std::str::Utf8Error),
297 Time(std::time::SystemTimeError),
298}
299
300impl fmt::Display for MaildirError {
301 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
302 use MaildirError::*;
303
304 match *self {
305 Io(ref e) => write!(f, "IO Error: {}", e),
306 Utf8(ref e) => write!(f, "UTF8 Encoding Error: {}", e),
307 Time(ref e) => write!(f, "Time Error: {}", e),
308 }
309 }
310}
311
312impl error::Error for MaildirError {
313 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
314 use MaildirError::*;
315
316 match *self {
317 Io(ref e) => Some(e),
318 Utf8(ref e) => Some(e),
319 Time(ref e) => Some(e),
320 }
321 }
322}
323
324impl From<std::io::Error> for MaildirError {
325 fn from(e: std::io::Error) -> MaildirError {
326 MaildirError::Io(e)
327 }
328}
329impl From<std::str::Utf8Error> for MaildirError {
330 fn from(e: std::str::Utf8Error) -> MaildirError {
331 MaildirError::Utf8(e)
332 }
333}
334impl From<std::time::SystemTimeError> for MaildirError {
335 fn from(e: std::time::SystemTimeError) -> MaildirError {
336 MaildirError::Time(e)
337 }
338}
339
340pub struct MaildirEntries {
346 path: PathBuf,
347 readdir: Option<fs::ReadDir>,
348}
349
350impl MaildirEntries {
351 fn new(path: PathBuf) -> MaildirEntries {
352 MaildirEntries {
353 path,
354 readdir: None,
355 }
356 }
357}
358
359impl Iterator for MaildirEntries {
360 type Item = std::io::Result<Maildir>;
361
362 fn next(&mut self) -> Option<std::io::Result<Maildir>> {
363 if self.readdir.is_none() {
364 self.readdir = match fs::read_dir(&self.path) {
365 Err(_) => return None,
366 Ok(v) => Some(v),
367 };
368 }
369
370 loop {
371 let dir_entry = self.readdir.iter_mut().next().unwrap().next();
372 let result = dir_entry.map(|e| {
373 let entry = e?;
374
375 let filename = String::from(entry.file_name().to_string_lossy().deref());
377 if !filename.starts_with('.') || filename.starts_with("..") {
378 return Ok(None);
379 }
380
381 let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or_default();
383 if !is_dir {
384 return Ok(None);
385 }
386
387 Ok(Some(Maildir::with_path(self.path.join(filename))))
388 });
389
390 return match result {
391 None => None,
392 Some(Err(e)) => Some(Err(e)),
393 Some(Ok(None)) => continue,
394 Some(Ok(Some(v))) => Some(Ok(v)),
395 };
396 }
397 }
398}
399
400pub struct Maildir {
405 path: PathBuf,
406 #[cfg(unix)]
407 dir_mode: Option<u32>,
408 #[cfg(unix)]
409 file_mode: Option<u32>,
410}
411
412impl Maildir {
413 pub fn with_path<P: Into<PathBuf>>(p: P) -> Self {
415 Self {
416 path: p.into(),
417 #[cfg(unix)]
418 dir_mode: None,
419 #[cfg(unix)]
420 file_mode: None,
421 }
422 }
423
424 pub fn set_dir_mode(&mut self, dir_mode: Option<u32>) {
437 self.dir_mode = dir_mode;
438 }
439
440 pub fn set_file_mode(&mut self, file_mode: Option<u32>) {
453 self.file_mode = file_mode;
454 }
455
456 pub fn path(&self) -> &Path {
458 &self.path
459 }
460
461 pub fn count_new(&self) -> usize {
464 self.list_new().count()
465 }
466
467 pub fn count_cur(&self) -> usize {
470 self.list_cur().count()
471 }
472
473 pub fn list_new(&self) -> MailEntries {
478 MailEntries::new(self.path.clone(), Subfolder::New)
479 }
480
481 pub fn list_cur(&self) -> MailEntries {
486 MailEntries::new(self.path.clone(), Subfolder::Cur)
487 }
488
489 pub fn list_subdirs(&self) -> MaildirEntries {
494 MaildirEntries::new(self.path.clone())
495 }
496
497 pub fn move_new_to_cur(&self, id: &str) -> std::io::Result<()> {
501 self.move_new_to_cur_with_flags(id, "")
502 }
503
504 pub fn move_new_to_cur_with_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
510 let src = self.path.join("new").join(id);
511 let dst = self.path.join("cur").join(format!(
512 "{}{}2,{}",
513 id,
514 INFORMATIONAL_SUFFIX_SEPARATOR,
515 Self::normalize_flags(flags)
516 ));
517 fs::rename(src, dst)
518 }
519
520 pub fn copy_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> {
522 let entry = self.find(id).ok_or_else(|| {
523 std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found")
524 })?;
525 let filename = entry.path().file_name().ok_or_else(|| {
526 std::io::Error::new(
527 std::io::ErrorKind::InvalidData,
528 "Invalid mail entry file name",
529 )
530 })?;
531
532 let src_path = entry.path();
533 let dst_path = target.path().join("cur").join(filename);
534 if src_path == &dst_path {
535 return Err(std::io::Error::new(
536 std::io::ErrorKind::InvalidInput,
537 "Target maildir needs to be different from the source",
538 ));
539 }
540
541 fs::copy(src_path, dst_path)?;
542 Ok(())
543 }
544
545 pub fn move_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> {
547 let entry = self.find(id).ok_or_else(|| {
548 std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found")
549 })?;
550 let filename = entry.path().file_name().ok_or_else(|| {
551 std::io::Error::new(
552 std::io::ErrorKind::InvalidData,
553 "Invalid mail entry file name",
554 )
555 })?;
556 fs::rename(entry.path(), target.path().join("cur").join(filename))?;
557 Ok(())
558 }
559
560 pub fn find(&self, id: &str) -> Option<MailEntry> {
564 let filter = |entry: &std::io::Result<MailEntry>| match *entry {
565 Err(_) => false,
566 Ok(ref e) => e.id() == id,
567 };
568
569 self.list_new()
570 .find(&filter)
571 .or_else(|| self.list_cur().find(&filter))
572 .map(|e| e.unwrap())
573 }
574
575 fn normalize_flags(flags: &str) -> String {
576 let mut flag_chars = flags.chars().collect::<Vec<char>>();
577 flag_chars.sort();
578 flag_chars.dedup();
579 flag_chars.into_iter().collect()
580 }
581
582 fn update_flags<F>(&self, id: &str, flag_op: F) -> std::io::Result<()>
583 where
584 F: Fn(&str) -> String,
585 {
586 let filter = |entry: &std::io::Result<MailEntry>| match *entry {
587 Err(_) => false,
588 Ok(ref e) => e.id() == id,
589 };
590
591 match self.list_cur().find(&filter).map(|e| e.unwrap()) {
592 Some(m) => {
593 let src = m.path();
594 let mut dst = m.path().clone();
595 dst.pop();
596 dst.push(format!(
597 "{}{}2,{}",
598 m.id(),
599 INFORMATIONAL_SUFFIX_SEPARATOR,
600 flag_op(m.flags())
601 ));
602 fs::rename(src, dst)
603 }
604 None => Err(std::io::Error::new(
605 std::io::ErrorKind::NotFound,
606 "Mail entry not found",
607 )),
608 }
609 }
610
611 pub fn set_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
617 self.update_flags(id, |_old_flags| Self::normalize_flags(flags))
618 }
619
620 pub fn add_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
625 let flag_merge = |old_flags: &str| {
626 let merged = String::from(old_flags) + flags;
627 Self::normalize_flags(&merged)
628 };
629 self.update_flags(id, flag_merge)
630 }
631
632 pub fn remove_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
638 let flag_strip =
639 |old_flags: &str| old_flags.chars().filter(|c| !flags.contains(*c)).collect();
640 self.update_flags(id, flag_strip)
641 }
642
643 pub fn delete(&self, id: &str) -> std::io::Result<()> {
648 match self.find(id) {
649 Some(m) => fs::remove_file(m.path()),
650 None => Err(std::io::Error::new(
651 std::io::ErrorKind::NotFound,
652 "Mail entry not found",
653 )),
654 }
655 }
656
657 pub fn create_dirs(&self) -> std::io::Result<()> {
660 let mut path = self.path.clone();
661 for d in &["cur", "new", "tmp"] {
662 path.push(d);
663 self.create_dir_all(path.as_path())?;
664 path.pop();
665 }
666 Ok(())
667 }
668
669 fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
670 'retry: loop {
671 match path.metadata() {
672 Ok(meta) => {
673 if meta.is_dir() {
674 return Ok(());
675 }
676 return Err(std::io::Error::new(
677 std::io::ErrorKind::NotADirectory,
678 format!(
679 "{} already exists as non-directory {meta:?}",
680 path.display()
681 ),
682 ));
683 }
684 Err(err) => {
685 if err.kind() != std::io::ErrorKind::NotFound {
686 return Err(err);
687 }
688
689 if let Some(parent) = path.parent() {
690 self.create_dir_all(parent)?;
691 }
692
693 if let Err(err) = std::fs::create_dir(path) {
694 match err.kind() {
695 std::io::ErrorKind::AlreadyExists => {
696 continue 'retry;
700 }
701 std::io::ErrorKind::IsADirectory => {
702 return Ok(());
705 }
706 _ => {
707 return Err(err);
708 }
709 }
710 }
711
712 #[cfg(unix)]
713 if let Some(mode) = self.dir_mode {
714 chmod(path, mode)?;
715 }
716 return Ok(());
717 }
718 }
719 }
720 }
721
722 pub fn store_new(&self, data: &[u8]) -> std::result::Result<String, MaildirError> {
727 self.store(Subfolder::New, data, "")
728 }
729
730 pub fn store_cur_with_flags(
735 &self,
736 data: &[u8],
737 flags: &str,
738 ) -> std::result::Result<String, MaildirError> {
739 self.store(
740 Subfolder::Cur,
741 data,
742 &format!(
743 "{}2,{}",
744 INFORMATIONAL_SUFFIX_SEPARATOR,
745 Self::normalize_flags(flags)
746 ),
747 )
748 }
749
750 fn store(
751 &self,
752 subfolder: Subfolder,
753 data: &[u8],
754 info: &str,
755 ) -> std::result::Result<String, MaildirError> {
756 let pid = std::process::id();
761 let hostname = gethostname::gethostname()
762 .into_string()
763 .unwrap_or_else(|_| "localhost".to_string());
767
768 let mut tmppath = self.path.clone();
772 tmppath.push("tmp");
773
774 let mut file;
775 let mut secs;
776 let mut nanos;
777 let mut counter;
778
779 loop {
780 let ts = time::SystemTime::now().duration_since(time::UNIX_EPOCH)?;
781 secs = ts.as_secs();
782 nanos = ts.subsec_nanos();
783 counter = COUNTER.fetch_add(1, Ordering::SeqCst);
784
785 tmppath.push(format!("{secs}.#{counter:x}M{nanos}P{pid}.{hostname}"));
786
787 match std::fs::OpenOptions::new()
788 .write(true)
789 .create_new(true)
790 .open(&tmppath)
791 {
792 Ok(f) => {
793 file = f;
794
795 #[cfg(unix)]
796 if let Some(mode) = self.file_mode {
797 use std::os::unix::fs::PermissionsExt;
798 let mode = std::fs::Permissions::from_mode(mode);
799 file.set_permissions(mode)?;
800 }
801 break;
802 }
803 Err(err) => {
804 if err.kind() != ErrorKind::AlreadyExists {
805 return Err(err.into());
806 }
807 tmppath.pop();
808 }
809 }
810 }
811
812 struct UnlinkOnError {
818 path_to_unlink: Option<PathBuf>,
819 }
820
821 impl Drop for UnlinkOnError {
822 fn drop(&mut self) {
823 if let Some(path) = self.path_to_unlink.take() {
824 std::fs::remove_file(path).ok();
826 }
827 }
828 }
829
830 let mut unlink_guard = UnlinkOnError {
832 path_to_unlink: Some(tmppath.clone()),
833 };
834
835 file.write_all(data)?;
836 file.sync_all()?;
837
838 let meta = file.metadata()?;
839 let mut newpath = self.path.clone();
840 newpath.push(match subfolder {
841 Subfolder::New => "new",
842 Subfolder::Cur => "cur",
843 });
844
845 #[cfg(unix)]
846 let dev = meta.dev();
847 #[cfg(windows)]
848 let dev: u64 = 0;
849
850 #[cfg(unix)]
851 let ino = meta.ino();
852 #[cfg(windows)]
853 let ino: u64 = 0;
854
855 #[cfg(unix)]
856 let size = meta.size();
857 #[cfg(windows)]
858 let size = meta.file_size();
859
860 let id = format!("{secs}.#{counter:x}M{nanos}P{pid}V{dev}I{ino}.{hostname},S={size}");
861 newpath.push(format!("{}{}", id, info));
862
863 std::fs::rename(&tmppath, &newpath)?;
864 unlink_guard.path_to_unlink.take();
865 Ok(id)
866 }
867}
868
869#[cfg(unix)]
870fn chmod(path: &Path, mode: u32) -> std::io::Result<()> {
871 use std::os::unix::fs::PermissionsExt;
872 let mode = std::fs::Permissions::from_mode(mode);
873 std::fs::set_permissions(path, mode)
874}