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_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
214/// An iterator over the email messages in a particular
215/// maildir subfolder (either `cur` or `new`). This iterator
216/// produces a `std::io::Result<MailEntry>`, which can be an
217/// `Err` if an error was encountered while trying to read
218/// file system properties on a particular entry, or if an
219/// invalid file was found in the maildir. Files starting with
220/// a dot (.) character in the maildir folder are ignored.
221pub 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            // we need to skip over files starting with a '.'
255            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
340/// An iterator over the maildir subdirectories. This iterator
341/// produces a `std::io::Result<Maildir>`, which can be an
342/// `Err` if an error was encountered while trying to read
343/// file system properties on a particular entry. Only
344/// subdirectories starting with a single period are included.
345pub 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                // a dir name should start by one single period
376                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                // the entry should be a directory
382                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
400/// The main entry point for this library. This struct can be
401/// instantiated from a path using the `from` implementations.
402/// The path passed in to the `from` should be the root of the
403/// maildir (the folder containing `cur`, `new`, and `tmp`).
404pub 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    /// Create a Maildir from a path-compatible parameter
414    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    /// Set the directory permission mode.
425    /// By default this is left unspecified, which causes
426    /// directories to be created with permissions
427    /// suitable for the owner, obeying the standard unix
428    /// umask semantics.
429    /// If you choose to assign the permission modes here,
430    /// the umask will be ignored and the explicit modes
431    /// that you set will be used on any directories
432    /// that are created by Maildir.
433    /// This will NOT change modes on existing directories
434    /// they will only be applied to directores created
435    /// by this instance of Maildir
436    pub fn set_dir_mode(&mut self, dir_mode: Option<u32>) {
437        self.dir_mode = dir_mode;
438    }
439
440    /// Set the file permission mode.
441    /// By default this is left unspecified, which causes
442    /// files to be created with permissions
443    /// suitable for the owner, obeying the standard unix
444    /// umask semantics.
445    /// If you choose to assign the permission modes here,
446    /// the umask will be ignored and the explicit modes
447    /// that you set will be used on any files
448    /// that are created by Maildir.
449    /// This will NOT change modes on existing files
450    /// they will only be applied to files created
451    /// by this instance of Maildir
452    pub fn set_file_mode(&mut self, file_mode: Option<u32>) {
453        self.file_mode = file_mode;
454    }
455
456    /// Returns the path of the maildir base folder.
457    pub fn path(&self) -> &Path {
458        &self.path
459    }
460
461    /// Returns the number of messages found inside the `new`
462    /// maildir folder.
463    pub fn count_new(&self) -> usize {
464        self.list_new().count()
465    }
466
467    /// Returns the number of messages found inside the `cur`
468    /// maildir folder.
469    pub fn count_cur(&self) -> usize {
470        self.list_cur().count()
471    }
472
473    /// Returns an iterator over the messages inside the `new`
474    /// maildir folder. The order of messages in the iterator
475    /// is not specified, and is not guaranteed to be stable
476    /// over multiple invocations of this method.
477    pub fn list_new(&self) -> MailEntries {
478        MailEntries::new(self.path.clone(), Subfolder::New)
479    }
480
481    /// Returns an iterator over the messages inside the `cur`
482    /// maildir folder. The order of messages in the iterator
483    /// is not specified, and is not guaranteed to be stable
484    /// over multiple invocations of this method.
485    pub fn list_cur(&self) -> MailEntries {
486        MailEntries::new(self.path.clone(), Subfolder::Cur)
487    }
488
489    /// Returns an iterator over the maildir subdirectories.
490    /// The order of subdirectories in the iterator
491    /// is not specified, and is not guaranteed to be stable
492    /// over multiple invocations of this method.
493    pub fn list_subdirs(&self) -> MaildirEntries {
494        MaildirEntries::new(self.path.clone())
495    }
496
497    /// Moves a message from the `new` maildir folder to the
498    /// `cur` maildir folder. The id passed in should be
499    /// obtained from the iterator produced by `list_new`.
500    pub fn move_new_to_cur(&self, id: &str) -> std::io::Result<()> {
501        self.move_new_to_cur_with_flags(id, "")
502    }
503
504    /// Moves a message from the `new` maildir folder to the `cur` maildir folder, and sets the
505    /// given flags. The id passed in should be obtained from the iterator produced by `list_new`.
506    ///
507    /// The possible flags are described e.g. at <https://cr.yp.to/proto/maildir.html> or
508    /// <http://www.courier-mta.org/maildir.html>.
509    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    /// Copies a message from the current maildir to the targetted maildir.
521    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    /// Moves a message from the current maildir to the targetted maildir.
546    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    /// Tries to find the message with the given id in the
561    /// maildir. This searches both the `new` and the `cur`
562    /// folders.
563    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    /// Updates the flags for the message with the given id in the
612    /// maildir. This only searches the `cur` folder, because that's
613    /// the folder where messages have flags. Returns an error if the
614    /// message was not found. All existing flags are overwritten with
615    /// the new flags provided.
616    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    /// Adds the given flags to the message with the given id in the maildir.
621    /// This only searches the `cur` folder, because that's the folder where
622    /// messages have flags. Returns an error if the message was not found.
623    /// Flags are deduplicated, so setting a already-set flag has no effect.
624    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    /// Removes the given flags to the message with the given id in the maildir.
633    /// This only searches the `cur` folder, because that's the folder where
634    /// messages have flags. Returns an error if the message was not found.
635    /// If the message doesn't have the flag(s) to be removed, those flags are
636    /// ignored.
637    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    /// Deletes the message with the given id in the maildir.
644    /// This searches both the `new` and the `cur` folders,
645    /// and deletes the file from the filesystem. Returns an
646    /// error if no message was found with the given id.
647    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    /// Creates all neccessary directories if they don't exist yet. It is the library user's
658    /// responsibility to call this before using `store_new`.
659    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            path.pop();
665        }
666        Ok(())
667    }
668
669    fn create_dir_all(&self, path: &Path) -> std::io::Result<()> {
670        'retry: loop {
671            match path.metadata() {
672                Ok(meta) => {
673                    if meta.is_dir() {
674                        return Ok(());
675                    }
676                    return Err(std::io::Error::new(
677                        std::io::ErrorKind::NotADirectory,
678                        format!(
679                            "{} already exists as non-directory {meta:?}",
680                            path.display()
681                        ),
682                    ));
683                }
684                Err(err) => {
685                    if err.kind() != std::io::ErrorKind::NotFound {
686                        return Err(err);
687                    }
688
689                    if let Some(parent) = path.parent() {
690                        self.create_dir_all(parent)?;
691                    }
692
693                    if let Err(err) = std::fs::create_dir(path) {
694                        match err.kind() {
695                            std::io::ErrorKind::AlreadyExists => {
696                                // The file now exists, but we're not
697                                // sure what its type is: restart
698                                // the operation to find out.
699                                continue 'retry;
700                            }
701                            std::io::ErrorKind::IsADirectory => {
702                                // We lost a race to create it,
703                                // but the outcome is the one we wanted.
704                                return Ok(());
705                            }
706                            _ => {
707                                return Err(err);
708                            }
709                        }
710                    }
711
712                    #[cfg(unix)]
713                    if let Some(mode) = self.dir_mode {
714                        chmod(path, mode)?;
715                    }
716                    return Ok(());
717                }
718            }
719        }
720    }
721
722    /// Stores the given message data as a new message file in the Maildir `new` folder. Does not
723    /// create the neccessary directories, so if in doubt call `create_dirs` before using
724    /// `store_new`.
725    /// Returns the Id of the inserted message on success.
726    pub fn store_new(&self, data: &[u8]) -> std::result::Result<String, MaildirError> {
727        self.store(Subfolder::New, data, "")
728    }
729
730    /// Stores the given message data as a new message file in the Maildir `cur` folder, adding the
731    /// given `flags` to it. The possible flags are explained e.g. at
732    /// <https://cr.yp.to/proto/maildir.html> or <http://www.courier-mta.org/maildir.html>.
733    /// Returns the Id of the inserted message on success.
734    pub fn store_cur_with_flags(
735        &self,
736        data: &[u8],
737        flags: &str,
738    ) -> std::result::Result<String, MaildirError> {
739        self.store(
740            Subfolder::Cur,
741            data,
742            &format!(
743                "{}2,{}",
744                INFORMATIONAL_SUFFIX_SEPARATOR,
745                Self::normalize_flags(flags)
746            ),
747        )
748    }
749
750    fn store(
751        &self,
752        subfolder: Subfolder,
753        data: &[u8],
754        info: &str,
755    ) -> std::result::Result<String, MaildirError> {
756        // try to get some uniquenes, as described at http://cr.yp.to/proto/maildir.html
757        // dovecot and courier IMAP use <timestamp>.M<usec>P<pid>.<hostname> for tmp-files and then
758        // move to <timestamp>.M<usec>P<pid>V<dev>I<ino>.<hostname>,S=<size_in_bytes> when moving
759        // to new dir. see for example http://www.courier-mta.org/maildir.html.
760        let pid = std::process::id();
761        let hostname = gethostname::gethostname()
762            .into_string()
763            // the hostname is always ASCII in order to be a valid DNS
764            // name, so into_string() will always succeed. The error case
765            // here is to satisfy the compiler which doesn't know this.
766            .unwrap_or_else(|_| "localhost".to_string());
767
768        // loop when conflicting filenames occur, as described at
769        // http://www.courier-mta.org/maildir.html
770        // this assumes that pid and hostname don't change.
771        let mut tmppath = self.path.clone();
772        tmppath.push("tmp");
773
774        let mut file;
775        let mut secs;
776        let mut nanos;
777        let mut counter;
778
779        loop {
780            let ts = time::SystemTime::now().duration_since(time::UNIX_EPOCH)?;
781            secs = ts.as_secs();
782            nanos = ts.subsec_nanos();
783            counter = COUNTER.fetch_add(1, Ordering::SeqCst);
784
785            tmppath.push(format!("{secs}.#{counter:x}M{nanos}P{pid}.{hostname}"));
786
787            match std::fs::OpenOptions::new()
788                .write(true)
789                .create_new(true)
790                .open(&tmppath)
791            {
792                Ok(f) => {
793                    file = f;
794
795                    #[cfg(unix)]
796                    if let Some(mode) = self.file_mode {
797                        use std::os::unix::fs::PermissionsExt;
798                        let mode = std::fs::Permissions::from_mode(mode);
799                        file.set_permissions(mode)?;
800                    }
801                    break;
802                }
803                Err(err) => {
804                    if err.kind() != ErrorKind::AlreadyExists {
805                        return Err(err.into());
806                    }
807                    tmppath.pop();
808                }
809            }
810        }
811
812        /// At this point, `file` is our new file at `tmppath`.
813        /// If we leave the scope of this function prior to
814        /// successfully writing the file to its final location,
815        /// we need to ensure that we remove the temporary file.
816        /// This struct takes care of that detail.
817        struct UnlinkOnError {
818            path_to_unlink: Option<PathBuf>,
819        }
820
821        impl Drop for UnlinkOnError {
822            fn drop(&mut self) {
823                if let Some(path) = self.path_to_unlink.take() {
824                    // Best effort to remove it
825                    std::fs::remove_file(path).ok();
826                }
827            }
828        }
829
830        // Ensure that we remove the temporary file on failure
831        let mut unlink_guard = UnlinkOnError {
832            path_to_unlink: Some(tmppath.clone()),
833        };
834
835        file.write_all(data)?;
836        file.sync_all()?;
837
838        let meta = file.metadata()?;
839        let mut newpath = self.path.clone();
840        newpath.push(match subfolder {
841            Subfolder::New => "new",
842            Subfolder::Cur => "cur",
843        });
844
845        #[cfg(unix)]
846        let dev = meta.dev();
847        #[cfg(windows)]
848        let dev: u64 = 0;
849
850        #[cfg(unix)]
851        let ino = meta.ino();
852        #[cfg(windows)]
853        let ino: u64 = 0;
854
855        #[cfg(unix)]
856        let size = meta.size();
857        #[cfg(windows)]
858        let size = meta.file_size();
859
860        let id = format!("{secs}.#{counter:x}M{nanos}P{pid}V{dev}I{ino}.{hostname},S={size}");
861        newpath.push(format!("{}{}", id, info));
862
863        std::fs::rename(&tmppath, &newpath)?;
864        unlink_guard.path_to_unlink.take();
865        Ok(id)
866    }
867}
868
869#[cfg(unix)]
870fn chmod(path: &Path, mode: u32) -> std::io::Result<()> {
871    use std::os::unix::fs::PermissionsExt;
872    let mode = std::fs::Permissions::from_mode(mode);
873    std::fs::set_permissions(path, mode)
874}