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 dir_probe::probe_directory(path.as_path())
668 .map_err(|err| std::io::Error::other(format!("{err:#}")))?;
669 path.pop();
670 }
671 Ok(())
672 }
673
674 fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
675 'retry: loop {
676 match path.metadata() {
677 Ok(meta) => {
678 if meta.is_dir() {
679 return Ok(());
680 }
681 return Err(std::io::Error::new(
682 std::io::ErrorKind::NotADirectory,
683 format!(
684 "{} already exists as non-directory {meta:?}",
685 path.display()
686 ),
687 ));
688 }
689 Err(err) => {
690 if err.kind() != std::io::ErrorKind::NotFound {
691 return Err(err);
692 }
693
694 if let Some(parent) = path.parent() {
695 self.create_dir_all(parent)?;
696 }
697
698 if let Err(err) = std::fs::create_dir(path) {
699 match err.kind() {
700 std::io::ErrorKind::AlreadyExists => {
701 continue 'retry;
705 }
706 std::io::ErrorKind::IsADirectory => {
707 return Ok(());
710 }
711 _ => {
712 return Err(err);
713 }
714 }
715 }
716
717 #[cfg(unix)]
718 if let Some(mode) = self.dir_mode {
719 chmod(path, mode)?;
720 }
721 return Ok(());
722 }
723 }
724 }
725 }
726
727 pub fn store_new(&self, data: &[u8]) -> std::result::Result<String, MaildirError> {
732 self.store(Subfolder::New, data, "")
733 }
734
735 pub fn store_cur_with_flags(
740 &self,
741 data: &[u8],
742 flags: &str,
743 ) -> std::result::Result<String, MaildirError> {
744 self.store(
745 Subfolder::Cur,
746 data,
747 &format!(
748 "{}2,{}",
749 INFORMATIONAL_SUFFIX_SEPARATOR,
750 Self::normalize_flags(flags)
751 ),
752 )
753 }
754
755 fn store(
756 &self,
757 subfolder: Subfolder,
758 data: &[u8],
759 info: &str,
760 ) -> std::result::Result<String, MaildirError> {
761 let pid = std::process::id();
766 let hostname = gethostname::gethostname()
767 .into_string()
768 .unwrap_or_else(|_| "localhost".to_string());
772
773 let mut tmppath = self.path.clone();
777 tmppath.push("tmp");
778
779 let mut file;
780 let mut secs;
781 let mut nanos;
782 let mut counter;
783
784 loop {
785 let ts = time::SystemTime::now().duration_since(time::UNIX_EPOCH)?;
786 secs = ts.as_secs();
787 nanos = ts.subsec_nanos();
788 counter = COUNTER.fetch_add(1, Ordering::SeqCst);
789
790 tmppath.push(format!("{secs}.#{counter:x}M{nanos}P{pid}.{hostname}"));
791
792 match std::fs::OpenOptions::new()
793 .write(true)
794 .create_new(true)
795 .open(&tmppath)
796 {
797 Ok(f) => {
798 file = f;
799
800 #[cfg(unix)]
801 if let Some(mode) = self.file_mode {
802 use std::os::unix::fs::PermissionsExt;
803 let mode = std::fs::Permissions::from_mode(mode);
804 file.set_permissions(mode)?;
805 }
806 break;
807 }
808 Err(err) => {
809 if err.kind() != ErrorKind::AlreadyExists {
810 return Err(err.into());
811 }
812 tmppath.pop();
813 }
814 }
815 }
816
817 struct UnlinkOnError {
823 path_to_unlink: Option<PathBuf>,
824 }
825
826 impl Drop for UnlinkOnError {
827 fn drop(&mut self) {
828 if let Some(path) = self.path_to_unlink.take() {
829 std::fs::remove_file(path).ok();
831 }
832 }
833 }
834
835 let mut unlink_guard = UnlinkOnError {
837 path_to_unlink: Some(tmppath.clone()),
838 };
839
840 file.write_all(data)?;
841 file.sync_all()?;
842
843 let meta = file.metadata()?;
844 let mut newpath = self.path.clone();
845 newpath.push(match subfolder {
846 Subfolder::New => "new",
847 Subfolder::Cur => "cur",
848 });
849
850 #[cfg(unix)]
851 let dev = meta.dev();
852 #[cfg(windows)]
853 let dev: u64 = 0;
854
855 #[cfg(unix)]
856 let ino = meta.ino();
857 #[cfg(windows)]
858 let ino: u64 = 0;
859
860 #[cfg(unix)]
861 let size = meta.size();
862 #[cfg(windows)]
863 let size = meta.file_size();
864
865 let id = format!("{secs}.#{counter:x}M{nanos}P{pid}V{dev}I{ino}.{hostname},S={size}");
866 newpath.push(format!("{}{}", id, info));
867
868 std::fs::rename(&tmppath, &newpath)?;
869 unlink_guard.path_to_unlink.take();
870 Ok(id)
871 }
872}
873
874#[cfg(unix)]
875fn chmod(path: &Path, mode: u32) -> std::io::Result<()> {
876 use std::os::unix::fs::PermissionsExt;
877 let mode = std::fs::Permissions::from_mode(mode);
878 std::fs::set_permissions(path, mode)
879}