maildir/
lib.rs

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
96/// This struct represents a single email message inside
97/// the maildir. Creation of the struct does not automatically
98/// load the content of the email file into memory - however,
99/// that may happen upon calling functions that require parsing
100/// the email.
101pub 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
213/// An iterator over the email messages in a particular
214/// maildir subfolder (either `cur` or `new`). This iterator
215/// produces a `std::io::Result<MailEntry>`, which can be an
216/// `Err` if an error was encountered while trying to read
217/// file system properties on a particular entry, or if an
218/// invalid file was found in the maildir. Files starting with
219/// a dot (.) character in the maildir folder are ignored.
220pub 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            // we need to skip over files starting with a '.'
254            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
339/// An iterator over the maildir subdirectories. This iterator
340/// produces a `std::io::Result<Maildir>`, which can be an
341/// `Err` if an error was encountered while trying to read
342/// file system properties on a particular entry. Only
343/// subdirectories starting with a single period are included.
344pub 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                // a dir name should start by one single period
375                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                // the entry should be a directory
381                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
399/// The main entry point for this library. This struct can be
400/// instantiated from a path using the `from` implementations.
401/// The path passed in to the `from` should be the root of the
402/// maildir (the folder containing `cur`, `new`, and `tmp`).
403pub 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    /// Create a Maildir from a path-compatible parameter
413    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    /// Set the directory permission mode.
424    /// By default this is left unspecified, which causes
425    /// directories to be created with permissions
426    /// suitable for the owner, obeying the standard unix
427    /// umask semantics.
428    /// If you choose to assign the permission modes here,
429    /// the umask will be ignored and the explicit modes
430    /// that you set will be used on any directories
431    /// that are created by Maildir.
432    /// This will NOT change modes on existing directories
433    /// they will only be applied to directores created
434    /// by this instance of Maildir
435    pub fn set_dir_mode(&mut self, dir_mode: Option<u32>) {
436        self.dir_mode = dir_mode;
437    }
438
439    /// Set the file permission mode.
440    /// By default this is left unspecified, which causes
441    /// files to be created with permissions
442    /// suitable for the owner, obeying the standard unix
443    /// umask semantics.
444    /// If you choose to assign the permission modes here,
445    /// the umask will be ignored and the explicit modes
446    /// that you set will be used on any files
447    /// that are created by Maildir.
448    /// This will NOT change modes on existing files
449    /// they will only be applied to files created
450    /// by this instance of Maildir
451    pub fn set_file_mode(&mut self, file_mode: Option<u32>) {
452        self.file_mode = file_mode;
453    }
454
455    /// Returns the path of the maildir base folder.
456    pub fn path(&self) -> &Path {
457        &self.path
458    }
459
460    /// Returns the number of messages found inside the `new`
461    /// maildir folder.
462    pub fn count_new(&self) -> usize {
463        self.list_new().count()
464    }
465
466    /// Returns the number of messages found inside the `cur`
467    /// maildir folder.
468    pub fn count_cur(&self) -> usize {
469        self.list_cur().count()
470    }
471
472    /// Returns an iterator over the messages inside the `new`
473    /// maildir folder. The order of messages in the iterator
474    /// is not specified, and is not guaranteed to be stable
475    /// over multiple invocations of this method.
476    pub fn list_new(&self) -> MailEntries {
477        MailEntries::new(self.path.clone(), Subfolder::New)
478    }
479
480    /// Returns an iterator over the messages inside the `cur`
481    /// maildir folder. The order of messages in the iterator
482    /// is not specified, and is not guaranteed to be stable
483    /// over multiple invocations of this method.
484    pub fn list_cur(&self) -> MailEntries {
485        MailEntries::new(self.path.clone(), Subfolder::Cur)
486    }
487
488    /// Returns an iterator over the maildir subdirectories.
489    /// The order of subdirectories in the iterator
490    /// is not specified, and is not guaranteed to be stable
491    /// over multiple invocations of this method.
492    pub fn list_subdirs(&self) -> MaildirEntries {
493        MaildirEntries::new(self.path.clone())
494    }
495
496    /// Moves a message from the `new` maildir folder to the
497    /// `cur` maildir folder. The id passed in should be
498    /// obtained from the iterator produced by `list_new`.
499    pub fn move_new_to_cur(&self, id: &str) -> std::io::Result<()> {
500        self.move_new_to_cur_with_flags(id, "")
501    }
502
503    /// Moves a message from the `new` maildir folder to the `cur` maildir folder, and sets the
504    /// given flags. The id passed in should be obtained from the iterator produced by `list_new`.
505    ///
506    /// The possible flags are described e.g. at <https://cr.yp.to/proto/maildir.html> or
507    /// <http://www.courier-mta.org/maildir.html>.
508    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    /// Copies a message from the current maildir to the targetted maildir.
520    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    /// Moves a message from the current maildir to the targetted maildir.
545    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    /// Tries to find the message with the given id in the
560    /// maildir. This searches both the `new` and the `cur`
561    /// folders.
562    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    /// Updates the flags for the message with the given id in the
611    /// maildir. This only searches the `cur` folder, because that's
612    /// the folder where messages have flags. Returns an error if the
613    /// message was not found. All existing flags are overwritten with
614    /// the new flags provided.
615    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    /// Adds the given flags to the message with the given id in the maildir.
620    /// This only searches the `cur` folder, because that's the folder where
621    /// messages have flags. Returns an error if the message was not found.
622    /// Flags are deduplicated, so setting a already-set flag has no effect.
623    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    /// Removes the given flags to the message with the given id in the maildir.
632    /// This only searches the `cur` folder, because that's the folder where
633    /// messages have flags. Returns an error if the message was not found.
634    /// If the message doesn't have the flag(s) to be removed, those flags are
635    /// ignored.
636    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    /// Deletes the message with the given id in the maildir.
643    /// This searches both the `new` and the `cur` folders,
644    /// and deletes the file from the filesystem. Returns an
645    /// error if no message was found with the given id.
646    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    /// Creates all neccessary directories if they don't exist yet. It is the library user's
657    /// responsibility to call this before using `store_new`.
658    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                                // The file now exists, but we're not
696                                // sure what its type is: restart
697                                // the operation to find out.
698                                continue 'retry;
699                            }
700                            std::io::ErrorKind::IsADirectory => {
701                                // We lost a race to create it,
702                                // but the outcome is the one we wanted.
703                                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    /// Stores the given message data as a new message file in the Maildir `new` folder. Does not
722    /// create the neccessary directories, so if in doubt call `create_dirs` before using
723    /// `store_new`.
724    /// Returns the Id of the inserted message on success.
725    pub fn store_new(&self, data: &[u8]) -> std::result::Result<String, MaildirError> {
726        self.store(Subfolder::New, data, "")
727    }
728
729    /// Stores the given message data as a new message file in the Maildir `cur` folder, adding the
730    /// given `flags` to it. The possible flags are explained e.g. at
731    /// <https://cr.yp.to/proto/maildir.html> or <http://www.courier-mta.org/maildir.html>.
732    /// Returns the Id of the inserted message on success.
733    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        // try to get some uniquenes, as described at http://cr.yp.to/proto/maildir.html
756        // dovecot and courier IMAP use <timestamp>.M<usec>P<pid>.<hostname> for tmp-files and then
757        // move to <timestamp>.M<usec>P<pid>V<dev>I<ino>.<hostname>,S=<size_in_bytes> when moving
758        // to new dir. see for example http://www.courier-mta.org/maildir.html.
759        let pid = std::process::id();
760        let hostname = gethostname::gethostname()
761            .into_string()
762            // the hostname is always ASCII in order to be a valid DNS
763            // name, so into_string() will always succeed. The error case
764            // here is to satisfy the compiler which doesn't know this.
765            .unwrap_or_else(|_| "localhost".to_string());
766
767        // loop when conflicting filenames occur, as described at
768        // http://www.courier-mta.org/maildir.html
769        // this assumes that pid and hostname don't change.
770        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        /// At this point, `file` is our new file at `tmppath`.
812        /// If we leave the scope of this function prior to
813        /// successfully writing the file to its final location,
814        /// we need to ensure that we remove the temporary file.
815        /// This struct takes care of that detail.
816        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                    // Best effort to remove it
824                    std::fs::remove_file(path).ok();
825                }
826            }
827        }
828
829        // Ensure that we remove the temporary file on failure
830        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}