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    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
209/// An iterator over the email messages in a particular
210/// maildir subfolder (either `cur` or `new`). This iterator
211/// produces a `std::io::Result<MailEntry>`, which can be an
212/// `Err` if an error was encountered while trying to read
213/// file system properties on a particular entry, or if an
214/// invalid file was found in the maildir. Files starting with
215/// a dot (.) character in the maildir folder are ignored.
216pub 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            // we need to skip over files starting with a '.'
250            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
335/// An iterator over the maildir subdirectories. This iterator
336/// produces a `std::io::Result<Maildir>`, which can be an
337/// `Err` if an error was encountered while trying to read
338/// file system properties on a particular entry. Only
339/// subdirectories starting with a single period are included.
340pub 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                // a dir name should start by one single period
371                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                // the entry should be a directory
377                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
395/// The main entry point for this library. This struct can be
396/// instantiated from a path using the `from` implementations.
397/// The path passed in to the `from` should be the root of the
398/// maildir (the folder containing `cur`, `new`, and `tmp`).
399pub 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    /// Create a Maildir from a path-compatible parameter
409    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    /// Set the directory permission mode.
420    /// By default this is left unspecified, which causes
421    /// directories to be created with permissions
422    /// suitable for the owner, obeying the standard unix
423    /// umask semantics.
424    /// If you choose to assign the permission modes here,
425    /// the umask will be ignored and the explicit modes
426    /// that you set will be used on any directories
427    /// that are created by Maildir.
428    /// This will NOT change modes on existing directories
429    /// they will only be applied to directores created
430    /// by this instance of Maildir
431    pub fn set_dir_mode(&mut self, dir_mode: Option<u32>) {
432        self.dir_mode = dir_mode;
433    }
434
435    /// Set the file permission mode.
436    /// By default this is left unspecified, which causes
437    /// files to be created with permissions
438    /// suitable for the owner, obeying the standard unix
439    /// umask semantics.
440    /// If you choose to assign the permission modes here,
441    /// the umask will be ignored and the explicit modes
442    /// that you set will be used on any files
443    /// that are created by Maildir.
444    /// This will NOT change modes on existing files
445    /// they will only be applied to files created
446    /// by this instance of Maildir
447    pub fn set_file_mode(&mut self, file_mode: Option<u32>) {
448        self.file_mode = file_mode;
449    }
450
451    /// Returns the path of the maildir base folder.
452    pub fn path(&self) -> &Path {
453        &self.path
454    }
455
456    /// Returns the number of messages found inside the `new`
457    /// maildir folder.
458    pub fn count_new(&self) -> usize {
459        self.list_new().count()
460    }
461
462    /// Returns the number of messages found inside the `cur`
463    /// maildir folder.
464    pub fn count_cur(&self) -> usize {
465        self.list_cur().count()
466    }
467
468    /// Returns an iterator over the messages inside the `new`
469    /// maildir folder. The order of messages in the iterator
470    /// is not specified, and is not guaranteed to be stable
471    /// over multiple invocations of this method.
472    pub fn list_new(&self) -> MailEntries {
473        MailEntries::new(self.path.clone(), Subfolder::New)
474    }
475
476    /// Returns an iterator over the messages inside the `cur`
477    /// maildir folder. The order of messages in the iterator
478    /// is not specified, and is not guaranteed to be stable
479    /// over multiple invocations of this method.
480    pub fn list_cur(&self) -> MailEntries {
481        MailEntries::new(self.path.clone(), Subfolder::Cur)
482    }
483
484    /// Returns an iterator over the maildir subdirectories.
485    /// The order of subdirectories in the iterator
486    /// is not specified, and is not guaranteed to be stable
487    /// over multiple invocations of this method.
488    pub fn list_subdirs(&self) -> MaildirEntries {
489        MaildirEntries::new(self.path.clone())
490    }
491
492    /// Moves a message from the `new` maildir folder to the
493    /// `cur` maildir folder. The id passed in should be
494    /// obtained from the iterator produced by `list_new`.
495    pub fn move_new_to_cur(&self, id: &str) -> std::io::Result<()> {
496        self.move_new_to_cur_with_flags(id, "")
497    }
498
499    /// Moves a message from the `new` maildir folder to the `cur` maildir folder, and sets the
500    /// given flags. The id passed in should be obtained from the iterator produced by `list_new`.
501    ///
502    /// The possible flags are described e.g. at <https://cr.yp.to/proto/maildir.html> or
503    /// <http://www.courier-mta.org/maildir.html>.
504    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    /// Copies a message from the current maildir to the targetted maildir.
516    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    /// Moves a message from the current maildir to the targetted maildir.
541    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    /// Tries to find the message with the given id in the
556    /// maildir. This searches both the `new` and the `cur`
557    /// folders.
558    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    /// Updates the flags for the message with the given id in the
607    /// maildir. This only searches the `cur` folder, because that's
608    /// the folder where messages have flags. Returns an error if the
609    /// message was not found. All existing flags are overwritten with
610    /// the new flags provided.
611    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    /// Adds the given flags to the message with the given id in the maildir.
616    /// This only searches the `cur` folder, because that's the folder where
617    /// messages have flags. Returns an error if the message was not found.
618    /// Flags are deduplicated, so setting a already-set flag has no effect.
619    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    /// Removes the given flags to the message with the given id in the maildir.
628    /// This only searches the `cur` folder, because that's the folder where
629    /// messages have flags. Returns an error if the message was not found.
630    /// If the message doesn't have the flag(s) to be removed, those flags are
631    /// ignored.
632    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    /// Deletes the message with the given id in the maildir.
639    /// This searches both the `new` and the `cur` folders,
640    /// and deletes the file from the filesystem. Returns an
641    /// error if no message was found with the given id.
642    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    /// Creates all neccessary directories if they don't exist yet. It is the library user's
653    /// responsibility to call this before using `store_new`.
654    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                                // The file now exists, but we're not
692                                // sure what its type is: restart
693                                // the operation to find out.
694                                continue 'retry;
695                            }
696                            std::io::ErrorKind::IsADirectory => {
697                                // We lost a race to create it,
698                                // but the outcome is the one we wanted.
699                                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    /// Stores the given message data as a new message file in the Maildir `new` folder. Does not
718    /// create the neccessary directories, so if in doubt call `create_dirs` before using
719    /// `store_new`.
720    /// Returns the Id of the inserted message on success.
721    pub fn store_new(&self, data: &[u8]) -> std::result::Result<String, MaildirError> {
722        self.store(Subfolder::New, data, "")
723    }
724
725    /// Stores the given message data as a new message file in the Maildir `cur` folder, adding the
726    /// given `flags` to it. The possible flags are explained e.g. at
727    /// <https://cr.yp.to/proto/maildir.html> or <http://www.courier-mta.org/maildir.html>.
728    /// Returns the Id of the inserted message on success.
729    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        // try to get some uniquenes, as described at http://cr.yp.to/proto/maildir.html
752        // dovecot and courier IMAP use <timestamp>.M<usec>P<pid>.<hostname> for tmp-files and then
753        // move to <timestamp>.M<usec>P<pid>V<dev>I<ino>.<hostname>,S=<size_in_bytes> when moving
754        // to new dir. see for example http://www.courier-mta.org/maildir.html.
755        let pid = std::process::id();
756        let hostname = gethostname::gethostname()
757            .into_string()
758            // the hostname is always ASCII in order to be a valid DNS
759            // name, so into_string() will always succeed. The error case
760            // here is to satisfy the compiler which doesn't know this.
761            .unwrap_or_else(|_| "localhost".to_string());
762
763        // loop when conflicting filenames occur, as described at
764        // http://www.courier-mta.org/maildir.html
765        // this assumes that pid and hostname don't change.
766        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        /// At this point, `file` is our new file at `tmppath`.
808        /// If we leave the scope of this function prior to
809        /// successfully writing the file to its final location,
810        /// we need to ensure that we remove the temporary file.
811        /// This struct takes care of that detail.
812        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                    // Best effort to remove it
820                    std::fs::remove_file(path).ok();
821                }
822            }
823        }
824
825        // Ensure that we remove the temporary file on failure
826        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}