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 fn read_data(&mut self) -> std::io::Result<()> {
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 Ok(())
121 }
122
123 pub fn parsed(&mut self) -> Result<MimePart, MailEntryError> {
124 self.read_data()?;
125 let bytes = self
126 .data
127 .data()
128 .expect("read_data should have returned an Err!");
129
130 MimePart::parse(bytes).map_err(MailEntryError::ParseError)
131 }
132
133 pub fn headers(&mut self) -> Result<HeaderMap, MailEntryError> {
134 self.read_data()?;
135 let bytes = self
136 .data
137 .data()
138 .expect("read_data should have returned an Err!");
139
140 let HeaderParseResult { headers, .. } =
141 Header::parse_headers(bytes).map_err(MailEntryError::ParseError)?;
142
143 Ok(headers)
144 }
145
146 pub fn received(&mut self) -> Result<DateTime<FixedOffset>, MailEntryError> {
147 self.read_data()?;
148 let headers = self.headers()?;
149 let received = headers.get_first("Received");
150 match received {
151 Some(v) => v
152 .get_raw_value()
153 .rsplit(';')
154 .nth(0)
155 .ok_or(MailEntryError::DateError("Unable to split Received header"))
156 .and_then(|ts| DateTime::parse_from_rfc2822(ts).map_err(MailEntryError::from)),
157 None => Err("No Received header found")?,
158 }
159 }
160
161 pub fn date(&mut self) -> Result<DateTime<FixedOffset>, MailEntryError> {
162 self.read_data()?;
163 let headers = self.headers()?;
164 let date = headers.get_first("Date");
165 match date {
166 Some(ts) => ts.as_date().map_err(MailEntryError::from),
167 None => Err("No Date header found")?,
168 }
169 }
170
171 pub fn flags(&self) -> &str {
172 &self.flags
173 }
174
175 pub fn is_draft(&self) -> bool {
176 self.flags.contains('D')
177 }
178
179 pub fn is_flagged(&self) -> bool {
180 self.flags.contains('F')
181 }
182
183 pub fn is_passed(&self) -> bool {
184 self.flags.contains('P')
185 }
186
187 pub fn is_replied(&self) -> bool {
188 self.flags.contains('R')
189 }
190
191 pub fn is_seen(&self) -> bool {
192 self.flags.contains('S')
193 }
194
195 pub fn is_trashed(&self) -> bool {
196 self.flags.contains('T')
197 }
198
199 pub fn path(&self) -> &PathBuf {
200 &self.path
201 }
202}
203
204enum Subfolder {
205 New,
206 Cur,
207}
208
209pub struct MailEntries {
217 path: PathBuf,
218 subfolder: Subfolder,
219 readdir: Option<fs::ReadDir>,
220}
221
222impl MailEntries {
223 fn new(path: PathBuf, subfolder: Subfolder) -> MailEntries {
224 MailEntries {
225 path,
226 subfolder,
227 readdir: None,
228 }
229 }
230}
231
232impl Iterator for MailEntries {
233 type Item = std::io::Result<MailEntry>;
234
235 fn next(&mut self) -> Option<std::io::Result<MailEntry>> {
236 if self.readdir.is_none() {
237 let mut dir_path = self.path.clone();
238 dir_path.push(match self.subfolder {
239 Subfolder::New => "new",
240 Subfolder::Cur => "cur",
241 });
242 self.readdir = match fs::read_dir(dir_path) {
243 Err(_) => return None,
244 Ok(v) => Some(v),
245 };
246 }
247
248 loop {
249 let dir_entry = self.readdir.iter_mut().next().unwrap().next();
251 let result = dir_entry.map(|e| {
252 let entry = e?;
253 let filename = String::from(entry.file_name().to_string_lossy().deref());
254 if filename.starts_with('.') {
255 return Ok(None);
256 }
257 let (id, flags) = match self.subfolder {
258 Subfolder::New => (Some(filename.as_str()), Some("")),
259 Subfolder::Cur => {
260 let delim = format!("{}2,", INFORMATIONAL_SUFFIX_SEPARATOR);
261 let mut iter = filename.split(&delim);
262 (iter.next(), iter.next())
263 }
264 };
265 if id.is_none() || flags.is_none() {
266 return Err(std::io::Error::new(
267 std::io::ErrorKind::InvalidData,
268 "Non-maildir file found in maildir",
269 ));
270 }
271 Ok(Some(MailEntry {
272 id: String::from(id.unwrap()),
273 flags: String::from(flags.unwrap()),
274 path: entry.path(),
275 data: MailData::None,
276 }))
277 });
278 return match result {
279 None => None,
280 Some(Err(e)) => Some(Err(e)),
281 Some(Ok(None)) => continue,
282 Some(Ok(Some(v))) => Some(Ok(v)),
283 };
284 }
285 }
286}
287
288#[derive(Debug)]
289pub enum MaildirError {
290 Io(std::io::Error),
291 Utf8(std::str::Utf8Error),
292 Time(std::time::SystemTimeError),
293}
294
295impl fmt::Display for MaildirError {
296 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
297 use MaildirError::*;
298
299 match *self {
300 Io(ref e) => write!(f, "IO Error: {}", e),
301 Utf8(ref e) => write!(f, "UTF8 Encoding Error: {}", e),
302 Time(ref e) => write!(f, "Time Error: {}", e),
303 }
304 }
305}
306
307impl error::Error for MaildirError {
308 fn source(&self) -> Option<&(dyn error::Error + 'static)> {
309 use MaildirError::*;
310
311 match *self {
312 Io(ref e) => Some(e),
313 Utf8(ref e) => Some(e),
314 Time(ref e) => Some(e),
315 }
316 }
317}
318
319impl From<std::io::Error> for MaildirError {
320 fn from(e: std::io::Error) -> MaildirError {
321 MaildirError::Io(e)
322 }
323}
324impl From<std::str::Utf8Error> for MaildirError {
325 fn from(e: std::str::Utf8Error) -> MaildirError {
326 MaildirError::Utf8(e)
327 }
328}
329impl From<std::time::SystemTimeError> for MaildirError {
330 fn from(e: std::time::SystemTimeError) -> MaildirError {
331 MaildirError::Time(e)
332 }
333}
334
335pub struct MaildirEntries {
341 path: PathBuf,
342 readdir: Option<fs::ReadDir>,
343}
344
345impl MaildirEntries {
346 fn new(path: PathBuf) -> MaildirEntries {
347 MaildirEntries {
348 path,
349 readdir: None,
350 }
351 }
352}
353
354impl Iterator for MaildirEntries {
355 type Item = std::io::Result<Maildir>;
356
357 fn next(&mut self) -> Option<std::io::Result<Maildir>> {
358 if self.readdir.is_none() {
359 self.readdir = match fs::read_dir(&self.path) {
360 Err(_) => return None,
361 Ok(v) => Some(v),
362 };
363 }
364
365 loop {
366 let dir_entry = self.readdir.iter_mut().next().unwrap().next();
367 let result = dir_entry.map(|e| {
368 let entry = e?;
369
370 let filename = String::from(entry.file_name().to_string_lossy().deref());
372 if !filename.starts_with('.') || filename.starts_with("..") {
373 return Ok(None);
374 }
375
376 let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or_default();
378 if !is_dir {
379 return Ok(None);
380 }
381
382 Ok(Some(Maildir::with_path(self.path.join(filename))))
383 });
384
385 return match result {
386 None => None,
387 Some(Err(e)) => Some(Err(e)),
388 Some(Ok(None)) => continue,
389 Some(Ok(Some(v))) => Some(Ok(v)),
390 };
391 }
392 }
393}
394
395pub struct Maildir {
400 path: PathBuf,
401 #[cfg(unix)]
402 dir_mode: Option<u32>,
403 #[cfg(unix)]
404 file_mode: Option<u32>,
405}
406
407impl Maildir {
408 pub fn with_path<P: Into<PathBuf>>(p: P) -> Self {
410 Self {
411 path: p.into(),
412 #[cfg(unix)]
413 dir_mode: None,
414 #[cfg(unix)]
415 file_mode: None,
416 }
417 }
418
419 pub fn set_dir_mode(&mut self, dir_mode: Option<u32>) {
432 self.dir_mode = dir_mode;
433 }
434
435 pub fn set_file_mode(&mut self, file_mode: Option<u32>) {
448 self.file_mode = file_mode;
449 }
450
451 pub fn path(&self) -> &Path {
453 &self.path
454 }
455
456 pub fn count_new(&self) -> usize {
459 self.list_new().count()
460 }
461
462 pub fn count_cur(&self) -> usize {
465 self.list_cur().count()
466 }
467
468 pub fn list_new(&self) -> MailEntries {
473 MailEntries::new(self.path.clone(), Subfolder::New)
474 }
475
476 pub fn list_cur(&self) -> MailEntries {
481 MailEntries::new(self.path.clone(), Subfolder::Cur)
482 }
483
484 pub fn list_subdirs(&self) -> MaildirEntries {
489 MaildirEntries::new(self.path.clone())
490 }
491
492 pub fn move_new_to_cur(&self, id: &str) -> std::io::Result<()> {
496 self.move_new_to_cur_with_flags(id, "")
497 }
498
499 pub fn move_new_to_cur_with_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
505 let src = self.path.join("new").join(id);
506 let dst = self.path.join("cur").join(format!(
507 "{}{}2,{}",
508 id,
509 INFORMATIONAL_SUFFIX_SEPARATOR,
510 Self::normalize_flags(flags)
511 ));
512 fs::rename(src, dst)
513 }
514
515 pub fn copy_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> {
517 let entry = self.find(id).ok_or_else(|| {
518 std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found")
519 })?;
520 let filename = entry.path().file_name().ok_or_else(|| {
521 std::io::Error::new(
522 std::io::ErrorKind::InvalidData,
523 "Invalid mail entry file name",
524 )
525 })?;
526
527 let src_path = entry.path();
528 let dst_path = target.path().join("cur").join(filename);
529 if src_path == &dst_path {
530 return Err(std::io::Error::new(
531 std::io::ErrorKind::InvalidInput,
532 "Target maildir needs to be different from the source",
533 ));
534 }
535
536 fs::copy(src_path, dst_path)?;
537 Ok(())
538 }
539
540 pub fn move_to(&self, id: &str, target: &Maildir) -> std::io::Result<()> {
542 let entry = self.find(id).ok_or_else(|| {
543 std::io::Error::new(std::io::ErrorKind::NotFound, "Mail entry not found")
544 })?;
545 let filename = entry.path().file_name().ok_or_else(|| {
546 std::io::Error::new(
547 std::io::ErrorKind::InvalidData,
548 "Invalid mail entry file name",
549 )
550 })?;
551 fs::rename(entry.path(), target.path().join("cur").join(filename))?;
552 Ok(())
553 }
554
555 pub fn find(&self, id: &str) -> Option<MailEntry> {
559 let filter = |entry: &std::io::Result<MailEntry>| match *entry {
560 Err(_) => false,
561 Ok(ref e) => e.id() == id,
562 };
563
564 self.list_new()
565 .find(&filter)
566 .or_else(|| self.list_cur().find(&filter))
567 .map(|e| e.unwrap())
568 }
569
570 fn normalize_flags(flags: &str) -> String {
571 let mut flag_chars = flags.chars().collect::<Vec<char>>();
572 flag_chars.sort();
573 flag_chars.dedup();
574 flag_chars.into_iter().collect()
575 }
576
577 fn update_flags<F>(&self, id: &str, flag_op: F) -> std::io::Result<()>
578 where
579 F: Fn(&str) -> String,
580 {
581 let filter = |entry: &std::io::Result<MailEntry>| match *entry {
582 Err(_) => false,
583 Ok(ref e) => e.id() == id,
584 };
585
586 match self.list_cur().find(&filter).map(|e| e.unwrap()) {
587 Some(m) => {
588 let src = m.path();
589 let mut dst = m.path().clone();
590 dst.pop();
591 dst.push(format!(
592 "{}{}2,{}",
593 m.id(),
594 INFORMATIONAL_SUFFIX_SEPARATOR,
595 flag_op(m.flags())
596 ));
597 fs::rename(src, dst)
598 }
599 None => Err(std::io::Error::new(
600 std::io::ErrorKind::NotFound,
601 "Mail entry not found",
602 )),
603 }
604 }
605
606 pub fn set_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
612 self.update_flags(id, |_old_flags| Self::normalize_flags(flags))
613 }
614
615 pub fn add_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
620 let flag_merge = |old_flags: &str| {
621 let merged = String::from(old_flags) + flags;
622 Self::normalize_flags(&merged)
623 };
624 self.update_flags(id, flag_merge)
625 }
626
627 pub fn remove_flags(&self, id: &str, flags: &str) -> std::io::Result<()> {
633 let flag_strip =
634 |old_flags: &str| old_flags.chars().filter(|c| !flags.contains(*c)).collect();
635 self.update_flags(id, flag_strip)
636 }
637
638 pub fn delete(&self, id: &str) -> std::io::Result<()> {
643 match self.find(id) {
644 Some(m) => fs::remove_file(m.path()),
645 None => Err(std::io::Error::new(
646 std::io::ErrorKind::NotFound,
647 "Mail entry not found",
648 )),
649 }
650 }
651
652 pub fn create_dirs(&self) -> std::io::Result<()> {
655 let mut path = self.path.clone();
656 for d in &["cur", "new", "tmp"] {
657 path.push(d);
658 self.create_dir_all(path.as_path())?;
659 path.pop();
660 }
661 Ok(())
662 }
663
664 fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
665 'retry: loop {
666 match path.metadata() {
667 Ok(meta) => {
668 if meta.is_dir() {
669 return Ok(());
670 }
671 return Err(std::io::Error::new(
672 std::io::ErrorKind::NotADirectory,
673 format!(
674 "{} already exists as non-directory {meta:?}",
675 path.display()
676 ),
677 ));
678 }
679 Err(err) => {
680 if err.kind() != std::io::ErrorKind::NotFound {
681 return Err(err);
682 }
683
684 if let Some(parent) = path.parent() {
685 self.create_dir_all(parent)?;
686 }
687
688 if let Err(err) = std::fs::create_dir(path) {
689 match err.kind() {
690 std::io::ErrorKind::AlreadyExists => {
691 continue 'retry;
695 }
696 std::io::ErrorKind::IsADirectory => {
697 return Ok(());
700 }
701 _ => {
702 return Err(err);
703 }
704 }
705 }
706
707 #[cfg(unix)]
708 if let Some(mode) = self.dir_mode {
709 chmod(path, mode)?;
710 }
711 return Ok(());
712 }
713 }
714 }
715 }
716
717 pub fn store_new(&self, data: &[u8]) -> std::result::Result<String, MaildirError> {
722 self.store(Subfolder::New, data, "")
723 }
724
725 pub fn store_cur_with_flags(
730 &self,
731 data: &[u8],
732 flags: &str,
733 ) -> std::result::Result<String, MaildirError> {
734 self.store(
735 Subfolder::Cur,
736 data,
737 &format!(
738 "{}2,{}",
739 INFORMATIONAL_SUFFIX_SEPARATOR,
740 Self::normalize_flags(flags)
741 ),
742 )
743 }
744
745 fn store(
746 &self,
747 subfolder: Subfolder,
748 data: &[u8],
749 info: &str,
750 ) -> std::result::Result<String, MaildirError> {
751 let pid = std::process::id();
756 let hostname = gethostname::gethostname()
757 .into_string()
758 .unwrap_or_else(|_| "localhost".to_string());
762
763 let mut tmppath = self.path.clone();
767 tmppath.push("tmp");
768
769 let mut file;
770 let mut secs;
771 let mut nanos;
772 let mut counter;
773
774 loop {
775 let ts = time::SystemTime::now().duration_since(time::UNIX_EPOCH)?;
776 secs = ts.as_secs();
777 nanos = ts.subsec_nanos();
778 counter = COUNTER.fetch_add(1, Ordering::SeqCst);
779
780 tmppath.push(format!("{secs}.#{counter:x}M{nanos}P{pid}.{hostname}"));
781
782 match std::fs::OpenOptions::new()
783 .write(true)
784 .create_new(true)
785 .open(&tmppath)
786 {
787 Ok(f) => {
788 file = f;
789
790 #[cfg(unix)]
791 if let Some(mode) = self.file_mode {
792 use std::os::unix::fs::PermissionsExt;
793 let mode = std::fs::Permissions::from_mode(mode);
794 file.set_permissions(mode)?;
795 }
796 break;
797 }
798 Err(err) => {
799 if err.kind() != ErrorKind::AlreadyExists {
800 return Err(err.into());
801 }
802 tmppath.pop();
803 }
804 }
805 }
806
807 struct UnlinkOnError {
813 path_to_unlink: Option<PathBuf>,
814 }
815
816 impl Drop for UnlinkOnError {
817 fn drop(&mut self) {
818 if let Some(path) = self.path_to_unlink.take() {
819 std::fs::remove_file(path).ok();
821 }
822 }
823 }
824
825 let mut unlink_guard = UnlinkOnError {
827 path_to_unlink: Some(tmppath.clone()),
828 };
829
830 file.write_all(data)?;
831 file.sync_all()?;
832
833 let meta = file.metadata()?;
834 let mut newpath = self.path.clone();
835 newpath.push(match subfolder {
836 Subfolder::New => "new",
837 Subfolder::Cur => "cur",
838 });
839
840 #[cfg(unix)]
841 let dev = meta.dev();
842 #[cfg(windows)]
843 let dev: u64 = 0;
844
845 #[cfg(unix)]
846 let ino = meta.ino();
847 #[cfg(windows)]
848 let ino: u64 = 0;
849
850 #[cfg(unix)]
851 let size = meta.size();
852 #[cfg(windows)]
853 let size = meta.file_size();
854
855 let id = format!("{secs}.#{counter:x}M{nanos}P{pid}V{dev}I{ino}.{hostname},S={size}");
856 newpath.push(format!("{}{}", id, info));
857
858 std::fs::rename(&tmppath, &newpath)?;
859 unlink_guard.path_to_unlink.take();
860 Ok(id)
861 }
862}
863
864#[cfg(unix)]
865fn chmod(path: &Path, mode: u32) -> std::io::Result<()> {
866 use std::os::unix::fs::PermissionsExt;
867 let mode = std::fs::Permissions::from_mode(mode);
868 std::fs::set_permissions(path, mode)
869}