vomit_m2dir/
m2dir.rs

1use base64::{engine::general_purpose, Engine};
2use chrono::{DateTime, Local, Utc};
3use mail_parser::MessageParser;
4use std::{
5    ffi::OsStr,
6    fs,
7    io::Write,
8    path::{Path, PathBuf},
9};
10
11use crate::flags::{self, Flags};
12use crate::{util, Error, Message, Messages};
13
14/// An m2dir as defined in the m2dir spec.
15///
16/// Any instance created by this implementation is guaranteed to be an existing
17/// directory with a `.m2dir` marker file.
18pub struct M2dir {
19    pub(crate) path: PathBuf,
20}
21
22impl TryFrom<&Path> for M2dir {
23    type Error = Error;
24
25    fn try_from(path: &Path) -> Result<Self, Error> {
26        if !path.is_dir() {
27            return Err(Error::FolderNotFound);
28        }
29        let marker = PathBuf::from_iter([path.as_os_str(), OsStr::new(".m2dir")]);
30        if !marker.is_file() {
31            // TODO better error message
32            return Err(Error::FolderNotFound);
33        }
34        Ok(M2dir {
35            path: PathBuf::from(path),
36        })
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        Ok(M2dir {
58            path: PathBuf::from(path.as_ref()),
59        })
60    }
61
62    /// Returns the path of the m2dir.
63    pub fn path(&self) -> &Path {
64        &self.path
65    }
66
67    /// Returns the number of messages found inside the m2dir.
68    pub fn count(&self) -> usize {
69        self.list().count()
70    }
71
72    /// Returns an iterator over the messages in the m2dir.
73    ///
74    /// The order of messages in the iterator is not specified, and is not
75    /// guaranteed to be stable over multiple invocations of this method.
76    pub fn list(&self) -> Messages {
77        Messages::new(self.path.clone())
78    }
79
80    pub fn deliver(&self, data: &[u8]) -> Result<Message, Error> {
81        self.store(data, &Flags::new())
82    }
83
84    pub fn find(&self, id: &str) -> Result<Option<Message>, Error> {
85        for msg in self.list() {
86            let msg = msg?;
87            if msg.id() == id {
88                return Ok(Some(msg));
89            }
90        }
91        Ok(None)
92    }
93
94    /// Stores the given message data as a new message in the m2dir, adding the
95    /// given `flags` to it. Returns the inserted message on success.
96    pub fn store(&self, data: &[u8], flags: &Flags) -> Result<Message, Error> {
97        let message = MessageParser::default()
98            .parse_headers(data)
99            .ok_or(Error::Parse())?;
100
101        // Get the messages date from the header or use "now" if that fails.
102        // This is used for the human readable part of the filename.
103        let date = match message.date() {
104            None => Local::now(),
105            Some(dt) => DateTime::from_timestamp(dt.to_timestamp(), 0)
106                .unwrap_or_else(Utc::now)
107                .with_timezone(&Local),
108        };
109        let from = message
110            .from()
111            .and_then(|f| f.first())
112            .and_then(|a| a.address())
113            .unwrap_or("<parse_error>");
114
115        let mut id = [0u8; 12];
116        id[0..4].copy_from_slice(&(data.len() as u32).to_le_bytes());
117        let digest = &util::fnv64(&id[0..4], data).to_le_bytes();
118        id[4..].copy_from_slice(digest); // the digest part
119        let mut id_b64 = general_purpose::URL_SAFE.encode(id);
120
121        let final_name = format!("{}_{},{}", date.format("%Y-%m-%d_%H:%M"), from, id_b64);
122        let mut final_path = self.path.clone();
123        final_path.push(&final_name);
124
125        if final_path.exists() {
126            // This message is a duplicate, gets a special ID
127            let mut i = 0u16;
128            while final_path.exists() {
129                i = i.wrapping_add(1);
130                if i == 0 {
131                    return Err(Error::TooManyDuplicates);
132                }
133                final_path.set_file_name(format!(
134                    "{}_{},{}.{}",
135                    date.format("%Y-%m-%d_%H:%M"),
136                    from,
137                    id_b64,
138                    i
139                ));
140            }
141            id_b64 = format!("{}.{}", id_b64, i);
142        }
143
144        if flags.len() > 0 {
145            let flags_path = flags::flags_path_for(&self.path, &id_b64);
146
147            util::write_atomic(&flags_path, |f| {
148                for flag in flags.iter() {
149                    writeln!(f, "{}", flag)?;
150                }
151                Ok(())
152            })
153            .map_err(|e| Error::WriteFlags(flags_path, e))?;
154        }
155
156        util::write_atomic(&final_path, |f| f.write_all(data)).map_err(|e| {
157            if flags.len() > 0 {
158                let flags_path = flags::flags_path_for(&self.path, &id_b64);
159                _ = fs::remove_file(flags_path);
160            }
161            e
162        })?;
163
164        Ok(Message {
165            id: id_b64,
166            path: final_path,
167        })
168    }
169}