Skip to main content

vomit_m2dir/
m2dir.rs

1use chrono::{DateTime, Local, Utc};
2use mail_parser::MessageParser;
3use std::{
4    env,
5    ffi::OsStr,
6    fs,
7    io::Write,
8    path::{Path, PathBuf},
9};
10
11use crate::{
12    flags::{self, Flags},
13    util::sanitize_filename,
14    ID,
15};
16use crate::{util, Error, Message, Messages};
17
18/// An m2dir as defined in the m2dir spec.
19///
20/// Any instance created by this implementation is guaranteed to be an existing
21/// directory with a `.m2dir` marker file.
22#[derive(Clone, Debug, PartialEq, Eq)]
23pub struct M2dir {
24    pub(crate) path: PathBuf,
25}
26
27impl TryFrom<&Path> for M2dir {
28    type Error = Error;
29
30    fn try_from(path: &Path) -> Result<Self, Error> {
31        let path = path.canonicalize()?;
32        let marker = PathBuf::from_iter([path.as_os_str(), OsStr::new(".m2dir")]);
33        if !marker.is_file() {
34            return Err(Error::FolderNotFound);
35        }
36        Ok(M2dir { path })
37    }
38}
39
40impl M2dir {
41    /// Open an existing m2dir.
42    ///
43    /// Will fail if `path` does not exist or is not a valid m2dir. To create a
44    /// new m2dir use [M2dir::create] instead.
45    pub fn open(path: impl AsRef<Path>) -> Result<Self, Error> {
46        M2dir::try_from(path.as_ref())
47    }
48
49    /// Creates a new m2dir.
50    ///
51    /// Unlike [M2dir::open], this method will try to create the m2dir if it
52    /// does not exist.
53    pub fn create(path: impl AsRef<Path>) -> Result<Self, Error> {
54        fs::create_dir_all(&path)?;
55        let marker = PathBuf::from_iter([path.as_ref().as_os_str(), OsStr::new(".m2dir")]);
56        let _ = fs::File::create(marker)?;
57        M2dir::open(path)
58    }
59
60    /// Returns the path of the m2dir.
61    ///
62    /// The path is guaranteed to be [canonical].
63    ///
64    /// [canonical]: https://doc.rust-lang.org/std/path/struct.Path.html#method.canonicalize
65    pub fn path(&self) -> &Path {
66        &self.path
67    }
68
69    /// Returns the number of messages found in this m2dir.
70    pub fn count(&self) -> usize {
71        self.list().count()
72    }
73
74    /// Returns an iterator over the messages in this m2dir.
75    ///
76    /// The order of messages in the iterator is not specified, and is not
77    /// guaranteed to be stable over multiple invocations of this method.
78    pub fn list(&self) -> Messages {
79        Messages::new(self.path.clone())
80    }
81
82    /// Deliver a new message to this m2dir.
83    pub fn deliver(&self, data: &[u8]) -> Result<Message, Error> {
84        self.store(data, &Flags::new())
85    }
86
87    /// Find the message with the given unique ID in this m2dir.
88    pub fn find(&self, id: &ID) -> Result<Option<Message>, Error> {
89        for msg in self.list() {
90            let msg = msg?;
91            if msg.id() == id {
92                return Ok(Some(msg));
93            }
94        }
95        Ok(None)
96    }
97
98    /// Stores the given message data as a new message in the m2dir, adding the
99    /// given `flags` to it. Returns the inserted message on success.
100    pub fn store(&self, data: &[u8], flags: &Flags) -> Result<Message, Error> {
101        let message = MessageParser::default()
102            .parse_headers(data)
103            .ok_or(Error::Parse())?;
104
105        // Get the messages date from the header or use "now" if that fails.
106        // This is used for the human readable part of the filename.
107        let date = match message.date() {
108            None => Local::now(),
109            Some(dt) => DateTime::from_timestamp(dt.to_timestamp(), 0)
110                .unwrap_or_else(Utc::now)
111                .with_timezone(&Local),
112        };
113        let from = message
114            .from()
115            .and_then(|f| f.first())
116            .and_then(|a| a.address())
117            .unwrap_or("<parse_error>");
118        let from = sanitize_filename(from);
119
120        let id = ID::new(data);
121
122        let fmt = env::var("VOMIT_M2DIR_DATE_FORMAT").unwrap_or("%Y-%m-%d_%H:%M".to_string());
123
124        let final_name = format!("{}_{},{}", date.format(&fmt), from, id);
125        let mut final_path = self.path.clone();
126        final_path.push(&final_name);
127
128        if !flags.is_empty() {
129            let flags_path = flags::flags_path_for(&self.path, id.as_ref());
130
131            util::write_atomic(&flags_path, |f| {
132                for flag in flags.iter() {
133                    writeln!(f, "{}", flag)?;
134                }
135                Ok(())
136            })
137            .map_err(|e| Error::WriteFlags(flags_path, e))?;
138        }
139
140        util::write_atomic(&final_path, |f| f.write_all(data)).inspect_err(|_| {
141            if !flags.is_empty() {
142                let flags_path = flags::flags_path_for(&self.path, id.as_ref());
143                _ = fs::remove_file(flags_path);
144            }
145        })?;
146
147        Ok(Message {
148            id,
149            path: final_path,
150        })
151    }
152}