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()
157 .rsplit(';')
158 .nth(0)
159 .ok_or(MailEntryError::DateError("Unable to split Received header"))
160 .and_then(|ts| DateTime::parse_from_rfc2822(ts).map_err(MailEntryError::from)),
161 None => Err("No Received header found")?,
162 }
163 }
164
165 pub fn date(&mut self) -> Result<DateTime<FixedOffset>, MailEntryError> {
166 self.read_data()?;
167 let headers = self.headers()?;
168 let date = headers.get_first("Date");
169 match date {
170 Some(ts) => ts.as_date().map_err(MailEntryError::from),
171 None => Err("No Date header found")?,
172 }
173 }
174
175 pub fn flags(&self) -> &str {
176 &self.flags
177 }
178
179 pub fn is_draft(&self) -> bool {
180 self.flags.contains('D')
181 }
182
183 pub fn is_flagged(&self) -> bool {
184 self.flags.contains('F')
185 }
186
187 pub fn is_passed(&self) -> bool {
188 self.flags.contains('P')
189 }
190
191 pub fn is_replied(&self) -> bool {
192 self.flags.contains('R')
193 }
194
195 pub fn is_seen(&self) -> bool {
196 self.flags.contains('S')
197 }
198
199 pub fn is_trashed(&self) -> bool {
200 self.flags.contains('T')
201 }
202
203 pub fn path(&self) -> &PathBuf {
204 &self.path
205 }
206}
207
208enum Subfolder {
209 New,
210 Cur,
211}
212
213pub struct MailEntries {
221 path: PathBuf,
222 subfolder: Subfolder,
223 readdir: Option<fs::ReadDir>,
224}
225
226impl MailEntries {
227 fn new(path: PathBuf, subfolder: Subfolder) -> MailEntries {
228 MailEntries {
229 path,
230 subfolder,
231 readdir: None,
232 }
233 }
234}
235
236impl Iterator for MailEntries {
237 type Item = std::io::Result<MailEntry>;
238
239 fn next(&mut self) -> Option<std::io::Result<MailEntry>> {
240 if self.readdir.is_none() {
241 let mut dir_path = self.path.clone();
242 dir_path.push(match self.subfolder {
243 Subfolder::New => "new",
244 Subfolder::Cur => "cur",
245 });
246 self.readdir = match fs::read_dir(dir_path) {
247 Err(_) => return None,
248 Ok(v) => Some(v),
249 };
250 }
251
252 loop {
253 let dir_entry = self.readdir.iter_mut().next().unwrap().next();
255 let result = dir_entry.map(|e| {
256 let entry = e?;
257 let filename = String::from(entry.file_name().to_string_lossy().deref());
258 if filename.starts_with('.') {
259 return Ok(None);
260 }
261 let (id, flags) = match self.subfolder {
262 Subfolder::New => (Some(filename.as_str()), Some("")),
263 Subfolder::Cur => {
264 let delim = format!("{}2,", INFORMATIONAL_SUFFIX_SEPARATOR);
265 let mut iter = filename.split(&delim);
266 (iter.next(), iter.next())
267 }
268 };
269 if id.is_none() || flags.is_none() {
270 return Err(std::io::Error::new(
271 std::io::ErrorKind::InvalidData,
272 "Non-maildir file found in maildir",
273 ));
274 }
275 Ok(Some(MailEntry {
276 id: String::from(id.unwrap()),
277 flags: String::from(flags.unwrap()),
278 path: entry.path(),
279 data: MailData::None,
280 }))
281 });
282 return match result {
283 None => None,
284 Some(Err(e)) => Some(Err(e)),
285 Some(Ok(None)) => continue,
286 Some(Ok(Some(v))) => Some(Ok(v)),
287 };
288 }
289 }
290}
291
292#[derive(Debug)]
293pub enum MaildirError {
294 Io(std::io::Error),
295 Utf8(std::str::Utf8Error),
296 Time(std::time::SystemTimeError),
297}
298
299impl fmt::Display for MaildirError {
300 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
301 use MaildirError::*;
302
303 match *self {
304 Io(ref e) => write!(f, "IO Error: {}", e),
305 Utf8(ref e) => write!(f, "UTF8 Encoding Error: {}", e),
306 Time(ref e) => write!(f, "Time Error: {}", e),
307 }
308 }
309}
310
311impl error::Error for MaildirError {
312 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
313 use MaildirError::*;
314
315 match *self {
316 Io(ref e) => Some(e),
317 Utf8(ref e) => Some(e),
318 Time(ref e) => Some(e),
319 }
320 }
321}
322
323impl From<std::io::Error> for MaildirError {
324 fn from(e: std::io::Error) -> MaildirError {
325 MaildirError::Io(e)
326 }
327}
328impl From<std::str::Utf8Error> for MaildirError {
329 fn from(e: std::str::Utf8Error) -> MaildirError {
330 MaildirError::Utf8(e)
331 }
332}
333impl From<std::time::SystemTimeError> for MaildirError {
334 fn from(e: std::time::SystemTimeError) -> MaildirError {
335 MaildirError::Time(e)
336 }
337}
338
339pub struct MaildirEntries {
345 path: PathBuf,
346 readdir: Option<fs::ReadDir>,
347}
348
349impl MaildirEntries {
350 fn new(path: PathBuf) -> MaildirEntries {
351 MaildirEntries {
352 path,
353 readdir: None,
354 }
355 }
356}
357
358impl Iterator for MaildirEntries {
359 type Item = std::io::Result<Maildir>;
360
361 fn next(&mut self) -> Option<std::io::Result<Maildir>> {
362 if self.readdir.is_none() {
363 self.readdir = match fs::read_dir(&self.path) {
364 Err(_) => return None,
365 Ok(v) => Some(v),
366 };
367 }
368
369 loop {
370 let dir_entry = self.readdir.iter_mut().next().unwrap().next();
371 let result = dir_entry.map(|e| {
372 let entry = e?;
373
374 let filename = String::from(entry.file_name().to_string_lossy().deref());
376 if !filename.starts_with('.') || filename.starts_with("..") {
377 return Ok(None);
378 }
379
380 let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or_default();
382 if !is_dir {
383 return Ok(None);
384 }
385
386 Ok(Some(Maildir::with_path(self.path.join(filename))))
387 });
388
389 return match result {
390 None => None,
391 Some(Err(e)) => Some(Err(e)),
392 Some(Ok(None)) => continue,
393 Some(Ok(Some(v))) => Some(Ok(v)),
394 };
395 }
396 }
397}
398
399pub struct Maildir {
404 path: PathBuf,
405 #[cfg(unix)]
406 dir_mode: Option<u32>,
407 #[cfg(unix)]
408 file_mode: Option<u32>,
409}
410
411impl Maildir {
412 pub fn with_path<P: Into<PathBuf>>(p: P) -> Self {
414 Self {
415 path: p.into(),
416 #[cfg(unix)]
417 dir_mode: None,
418 #[cfg(unix)]
419 file_mode: None,
420 }
421 }
422
423 pub fn set_dir_mode(&mut self, dir_mode: Option<u32>) {
436 self.dir_mode = dir_mode;
437 }
438
439 pub fn set_file_mode(&mut self, file_mode: Option<u32>) {
452 self.file_mode = file_mode;
453 }
454
455 pub fn path(&self) -> &Path {
457 &self.path
458 }
459
460 pub fn count_new(&self) -> usize {
463 self.list_new().count()
464 }
465
466 pub fn count_cur(&self) -> usize {
469 self.list_cur().count()
470 }
471
472 pub fn list_new(&self) -> MailEntries {
477 MailEntries::new(self.path.clone(), Subfolder::New)
478 }
479
480 pub fn list_cur(&self) -> MailEntries {
485 MailEntries::new(self.path.clone(), Subfolder::Cur)
486 }
487
488 pub fn list_subdirs(&self) -> MaildirEntries {
493 MaildirEntries::new(self.path.clone())
494 }
495
496 pub fn move_new_to_cur(&self, id: &str) -> std::io::Result<()> {
500 self.move_new_to_cur_with_flags(id, "")
501 }
502
503 pub fn move_new_to_cur_with_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
509 let src = self.path.join("new").join(id);
510 let dst = self.path.join("cur").join(format!(
511 "{}{}2,{}",
512 id,
513 INFORMATIONAL_SUFFIX_SEPARATOR,
514 Self::normalize_flags(flags)
515 ));
516 fs::rename(src, dst)
517 }
518
519 pub fn copy_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> {
521 let entry = self.find(id).ok_or_else(|| {
522 std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found")
523 })?;
524 let filename = entry.path().file_name().ok_or_else(|| {
525 std::io::Error::new(
526 std::io::ErrorKind::InvalidData,
527 "Invalid mail entry file name",
528 )
529 })?;
530
531 let src_path = entry.path();
532 let dst_path = target.path().join("cur").join(filename);
533 if src_path == &dst_path {
534 return Err(std::io::Error::new(
535 std::io::ErrorKind::InvalidInput,
536 "Target maildir needs to be different from the source",
537 ));
538 }
539
540 fs::copy(src_path, dst_path)?;
541 Ok(())
542 }
543
544 pub fn move_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> {
546 let entry = self.find(id).ok_or_else(|| {
547 std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found")
548 })?;
549 let filename = entry.path().file_name().ok_or_else(|| {
550 std::io::Error::new(
551 std::io::ErrorKind::InvalidData,
552 "Invalid mail entry file name",
553 )
554 })?;
555 fs::rename(entry.path(), target.path().join("cur").join(filename))?;
556 Ok(())
557 }
558
559 pub fn find(&self, id: &str) -> Option<MailEntry> {
563 let filter = |entry: &std::io::Result<MailEntry>| match *entry {
564 Err(_) => false,
565 Ok(ref e) => e.id() == id,
566 };
567
568 self.list_new()
569 .find(&filter)
570 .or_else(|| self.list_cur().find(&filter))
571 .map(|e| e.unwrap())
572 }
573
574 fn normalize_flags(flags: &str) -> String {
575 let mut flag_chars = flags.chars().collect::<Vec<char>>();
576 flag_chars.sort();
577 flag_chars.dedup();
578 flag_chars.into_iter().collect()
579 }
580
581 fn update_flags<F>(&self, id: &str, flag_op: F) -> std::io::Result<()>
582 where
583 F: Fn(&str) -> String,
584 {
585 let filter = |entry: &std::io::Result<MailEntry>| match *entry {
586 Err(_) => false,
587 Ok(ref e) => e.id() == id,
588 };
589
590 match self.list_cur().find(&filter).map(|e| e.unwrap()) {
591 Some(m) => {
592 let src = m.path();
593 let mut dst = m.path().clone();
594 dst.pop();
595 dst.push(format!(
596 "{}{}2,{}",
597 m.id(),
598 INFORMATIONAL_SUFFIX_SEPARATOR,
599 flag_op(m.flags())
600 ));
601 fs::rename(src, dst)
602 }
603 None => Err(std::io::Error::new(
604 std::io::ErrorKind::NotFound,
605 "Mail entry not found",
606 )),
607 }
608 }
609
610 pub fn set_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
616 self.update_flags(id, |_old_flags| Self::normalize_flags(flags))
617 }
618
619 pub fn add_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
624 let flag_merge = |old_flags: &str| {
625 let merged = String::from(old_flags) + flags;
626 Self::normalize_flags(&merged)
627 };
628 self.update_flags(id, flag_merge)
629 }
630
631 pub fn remove_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
637 let flag_strip =
638 |old_flags: &str| old_flags.chars().filter(|c| !flags.contains(*c)).collect();
639 self.update_flags(id, flag_strip)
640 }
641
642 pub fn delete(&self, id: &str) -> std::io::Result<()> {
647 match self.find(id) {
648 Some(m) => fs::remove_file(m.path()),
649 None => Err(std::io::Error::new(
650 std::io::ErrorKind::NotFound,
651 "Mail entry not found",
652 )),
653 }
654 }
655
656 pub fn create_dirs(&self) -> std::io::Result<()> {
659 let mut path = self.path.clone();
660 for d in &["cur", "new", "tmp"] {
661 path.push(d);
662 self.create_dir_all(path.as_path())?;
663 path.pop();
664 }
665 Ok(())
666 }
667
668 fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
669 'retry: loop {
670 match path.metadata() {
671 Ok(meta) => {
672 if meta.is_dir() {
673 return Ok(());
674 }
675 return Err(std::io::Error::new(
676 std::io::ErrorKind::NotADirectory,
677 format!(
678 "{} already exists as non-directory {meta:?}",
679 path.display()
680 ),
681 ));
682 }
683 Err(err) => {
684 if err.kind() != std::io::ErrorKind::NotFound {
685 return Err(err);
686 }
687
688 if let Some(parent) = path.parent() {
689 self.create_dir_all(parent)?;
690 }
691
692 if let Err(err) = std::fs::create_dir(path) {
693 match err.kind() {
694 std::io::ErrorKind::AlreadyExists => {
695 continue 'retry;
699 }
700 std::io::ErrorKind::IsADirectory => {
701 return Ok(());
704 }
705 _ => {
706 return Err(err);
707 }
708 }
709 }
710
711 #[cfg(unix)]
712 if let Some(mode) = self.dir_mode {
713 chmod(path, mode)?;
714 }
715 return Ok(());
716 }
717 }
718 }
719 }
720
721 pub fn store_new(&self, data: &[u8]) -> std::result::Result<String, MaildirError> {
726 self.store(Subfolder::New, data, "")
727 }
728
729 pub fn store_cur_with_flags(
734 &self,
735 data: &[u8],
736 flags: &str,
737 ) -> std::result::Result<String, MaildirError> {
738 self.store(
739 Subfolder::Cur,
740 data,
741 &format!(
742 "{}2,{}",
743 INFORMATIONAL_SUFFIX_SEPARATOR,
744 Self::normalize_flags(flags)
745 ),
746 )
747 }
748
749 fn store(
750 &self,
751 subfolder: Subfolder,
752 data: &[u8],
753 info: &str,
754 ) -> std::result::Result<String, MaildirError> {
755 let pid = std::process::id();
760 let hostname = gethostname::gethostname()
761 .into_string()
762 .unwrap_or_else(|_| "localhost".to_string());
766
767 let mut tmppath = self.path.clone();
771 tmppath.push("tmp");
772
773 let mut file;
774 let mut secs;
775 let mut nanos;
776 let mut counter;
777
778 loop {
779 let ts = time::SystemTime::now().duration_since(time::UNIX_EPOCH)?;
780 secs = ts.as_secs();
781 nanos = ts.subsec_nanos();
782 counter = COUNTER.fetch_add(1, Ordering::SeqCst);
783
784 tmppath.push(format!("{secs}.#{counter:x}M{nanos}P{pid}.{hostname}"));
785
786 match std::fs::OpenOptions::new()
787 .write(true)
788 .create_new(true)
789 .open(&tmppath)
790 {
791 Ok(f) => {
792 file = f;
793
794 #[cfg(unix)]
795 if let Some(mode) = self.file_mode {
796 use std::os::unix::fs::PermissionsExt;
797 let mode = std::fs::Permissions::from_mode(mode);
798 file.set_permissions(mode)?;
799 }
800 break;
801 }
802 Err(err) => {
803 if err.kind() != ErrorKind::AlreadyExists {
804 return Err(err.into());
805 }
806 tmppath.pop();
807 }
808 }
809 }
810
811 struct UnlinkOnError {
817 path_to_unlink: Option<PathBuf>,
818 }
819
820 impl Drop for UnlinkOnError {
821 fn drop(&mut self) {
822 if let Some(path) = self.path_to_unlink.take() {
823 std::fs::remove_file(path).ok();
825 }
826 }
827 }
828
829 let mut unlink_guard = UnlinkOnError {
831 path_to_unlink: Some(tmppath.clone()),
832 };
833
834 file.write_all(data)?;
835 file.sync_all()?;
836
837 let meta = file.metadata()?;
838 let mut newpath = self.path.clone();
839 newpath.push(match subfolder {
840 Subfolder::New => "new",
841 Subfolder::Cur => "cur",
842 });
843
844 #[cfg(unix)]
845 let dev = meta.dev();
846 #[cfg(windows)]
847 let dev: u64 = 0;
848
849 #[cfg(unix)]
850 let ino = meta.ino();
851 #[cfg(windows)]
852 let ino: u64 = 0;
853
854 #[cfg(unix)]
855 let size = meta.size();
856 #[cfg(windows)]
857 let size = meta.file_size();
858
859 let id = format!("{secs}.#{counter:x}M{nanos}P{pid}V{dev}I{ino}.{hostname},S={size}");
860 newpath.push(format!("{}{}", id, info));
861
862 std::fs::rename(&tmppath, &newpath)?;
863 unlink_guard.path_to_unlink.take();
864 Ok(id)
865 }
866}
867
868#[cfg(unix)]
869fn chmod(path: &Path, mode: u32) -> std::io::Result<()> {
870 use std::os::unix::fs::PermissionsExt;
871 let mode = std::fs::Permissions::from_mode(mode);
872 std::fs::set_permissions(path, mode)
873}