Skip to main content

mail_parser/mailbox/
maildir.rs

1/*
2 * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <hello@stalw.art>
3 *
4 * SPDX-License-Identifier: Apache-2.0 OR MIT
5 */
6
7use std::{
8    fs, io,
9    path::{Path, PathBuf},
10};
11
12/// Maildir folder iterator
13pub struct FolderIterator<'x> {
14    inbox: Option<MessageIterator>,
15    it_stack: Vec<fs::ReadDir>,
16    name_stack: Vec<String>,
17    prefix: Option<&'x str>,
18}
19
20/// Maildir message iterator
21pub struct MessageIterator {
22    name: Option<String>,
23    cur_it: fs::ReadDir,
24    new_it: fs::ReadDir,
25}
26
27/// Maildir message contents and metadata
28#[derive(Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
29pub struct Message {
30    internal_date: u64,
31    flags: Vec<Flag>,
32    contents: Vec<u8>,
33    path: PathBuf,
34}
35
36/// Flags of Maildir message
37#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
38pub enum Flag {
39    Passed,
40    Replied,
41    Seen,
42    Trashed,
43    Draft,
44    Flagged,
45}
46
47impl FolderIterator<'_> {
48    /// Creates a new Maildir folder iterator.
49    /// For Maildir++ mailboxes use `Some(".")` as the prefix.
50    /// For Dovecot Maildir mailboxes using LAYOUT=fs, use `None` as the prefix.
51    pub fn new(
52        path: impl Into<PathBuf>,
53        sub_folder_prefix: Option<&str>,
54    ) -> io::Result<FolderIterator<'_>> {
55        let path = path.into();
56
57        Ok(FolderIterator {
58            it_stack: vec![fs::read_dir(&path)?],
59            name_stack: Vec::new(),
60            inbox: match MessageIterator::new_(&path, None) {
61                Ok(inbox) => inbox.into(),
62                Err(err) => {
63                    if err.kind() == io::ErrorKind::NotFound {
64                        None
65                    } else {
66                        return Err(err);
67                    }
68                }
69            },
70            prefix: sub_folder_prefix,
71        })
72    }
73}
74
75impl MessageIterator {
76    /// Creates a new Maildir message iterator
77    pub fn new(path: impl Into<PathBuf>) -> io::Result<MessageIterator> {
78        MessageIterator::new_(&path.into(), None)
79    }
80
81    fn new_(path: &Path, name: Option<String>) -> io::Result<MessageIterator> {
82        let mut cur_path = path.to_path_buf();
83        cur_path.push("cur");
84        if !cur_path.exists() {
85            return Err(io::Error::new(
86                io::ErrorKind::NotFound,
87                "Invalid Maildir format, 'cur' directory not found.",
88            ));
89        }
90        let mut new_path = path.to_path_buf();
91        new_path.push("new");
92        if !new_path.exists() {
93            return Err(io::Error::new(
94                io::ErrorKind::NotFound,
95                "Invalid Maildir format, 'new' directory not found.",
96            ));
97        }
98
99        Ok(MessageIterator {
100            name,
101            cur_it: fs::read_dir(cur_path)?,
102            new_it: fs::read_dir(new_path)?,
103        })
104    }
105
106    /// Returns the mailbox name of None for 'INBOX'.
107    pub fn name(&self) -> Option<&str> {
108        self.name.as_deref()
109    }
110}
111
112impl Iterator for FolderIterator<'_> {
113    type Item = io::Result<MessageIterator>;
114
115    fn next(&mut self) -> Option<Self::Item> {
116        if let Some(inbox) = self.inbox.take() {
117            return Some(Ok(inbox));
118        }
119
120        loop {
121            let entry = match self.it_stack.last_mut().unwrap().next() {
122                Some(Ok(entry)) => entry,
123                Some(Err(err)) => return Some(Err(err)),
124                None => {
125                    self.it_stack.pop();
126                    self.name_stack.pop();
127
128                    if !self.it_stack.is_empty() {
129                        continue;
130                    } else {
131                        return None;
132                    }
133                }
134            };
135
136            let path = entry.path();
137            if path.is_dir()
138                && let Some(name) =
139                    path.file_name()
140                        .and_then(|name| name.to_str())
141                        .and_then(|name| {
142                            if !["cur", "new", "tmp"].contains(&name) {
143                                if let Some(prefix) = self.prefix {
144                                    name.strip_prefix(prefix)
145                                } else {
146                                    name.into()
147                                }
148                            } else {
149                                None
150                            }
151                        })
152            {
153                match fs::read_dir(&path) {
154                    Ok(next_it) => {
155                        self.it_stack.push(next_it);
156                        self.name_stack.push(name.to_string());
157                    }
158                    Err(err) => {
159                        return Some(Err(err));
160                    }
161                }
162
163                match MessageIterator::new_(
164                    &path,
165                    self.name_stack.join(self.prefix.unwrap_or("/")).into(),
166                ) {
167                    Ok(folder) => return Some(Ok(folder)),
168                    Err(err) => {
169                        if err.kind() != io::ErrorKind::NotFound {
170                            return Some(Err(err));
171                        }
172                    }
173                }
174            }
175        }
176    }
177}
178
179impl Iterator for MessageIterator {
180    type Item = io::Result<Message>;
181
182    fn next(&mut self) -> Option<Self::Item> {
183        loop {
184            let entry = match self.cur_it.next().or_else(|| self.new_it.next()) {
185                Some(Ok(entry)) => entry,
186                Some(Err(err)) => return Some(Err(err)),
187                None => return None,
188            };
189            let path = entry.path();
190            if path.is_file()
191                && let Some(name) = path.file_name().and_then(|name| name.to_str())
192                && !name.starts_with('.')
193            {
194                let internal_date =
195                    match fs::metadata(&path)
196                        .and_then(|m| m.modified())
197                        .and_then(|d| {
198                            d.duration_since(std::time::UNIX_EPOCH)
199                                .map(|d| d.as_secs())
200                                .map_err(|e| {
201                                    io::Error::new(io::ErrorKind::InvalidData, e.to_string())
202                                })
203                        }) {
204                        Ok(metadata) => metadata,
205                        Err(err) => return Some(Err(err)),
206                    };
207                let contents = match fs::read(&path) {
208                    Ok(contents) => contents,
209                    Err(err) => return Some(Err(err)),
210                };
211                let mut flags = Vec::new();
212                if let Some((_, part)) = name.rsplit_once("2,") {
213                    for &ch in part.as_bytes() {
214                        match ch {
215                            b'P' => flags.push(Flag::Passed),
216                            b'R' => flags.push(Flag::Replied),
217                            b'S' => flags.push(Flag::Seen),
218                            b'T' => flags.push(Flag::Trashed),
219                            b'D' => flags.push(Flag::Draft),
220                            b'F' => flags.push(Flag::Flagged),
221                            _ => {
222                                if !ch.is_ascii_alphanumeric() {
223                                    break;
224                                }
225                            }
226                        }
227                    }
228                }
229                return Some(Ok(Message {
230                    contents,
231                    internal_date,
232                    flags,
233                    path: path.to_path_buf(),
234                }));
235            }
236        }
237    }
238}
239
240impl Message {
241    /// Returns the message creation date in seconds since UNIX epoch
242    pub fn internal_date(&self) -> u64 {
243        self.internal_date
244    }
245
246    /// Returns the message flags
247    pub fn flags(&self) -> &[Flag] {
248        &self.flags
249    }
250
251    /// Returns the path to the message file
252    pub fn path(&self) -> &Path {
253        &self.path
254    }
255
256    /// Returns the message contents
257    pub fn contents(&self) -> &[u8] {
258        &self.contents
259    }
260
261    /// Unwraps the message contents
262    pub fn unwrap_contents(self) -> Vec<u8> {
263        self.contents
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use std::path::PathBuf;
270
271    use crate::mailbox::maildir::{Flag, Message};
272
273    use super::FolderIterator;
274
275    #[test]
276    fn parse_maildir() {
277        let mut messages = Vec::new();
278        let expected_messages = vec![
279            (
280                "INBOX".to_string(),
281                Message {
282                    internal_date: 0,
283                    flags: vec![Flag::Seen],
284                    contents: vec![98, 10],
285                    path: "unknown".into(),
286                },
287            ),
288            (
289                "INBOX".to_string(),
290                Message {
291                    internal_date: 0,
292                    flags: vec![Flag::Seen, Flag::Trashed],
293                    contents: vec![97, 10],
294                    path: "unknown".into(),
295                },
296            ),
297            (
298                "My Folder".to_string(),
299                Message {
300                    internal_date: 0,
301                    flags: vec![],
302                    contents: vec![100, 10],
303                    path: "unknown".into(),
304                },
305            ),
306            (
307                "My Folder".to_string(),
308                Message {
309                    internal_date: 0,
310                    flags: vec![Flag::Trashed, Flag::Draft, Flag::Replied],
311                    contents: vec![99, 10],
312                    path: "unknown".into(),
313                },
314            ),
315            (
316                "My Folder.Nested Folder".to_string(),
317                Message {
318                    internal_date: 0,
319                    flags: vec![Flag::Replied, Flag::Draft, Flag::Flagged],
320                    contents: vec![102, 10],
321                    path: "unknown".into(),
322                },
323            ),
324            (
325                "My Folder.Nested Folder".to_string(),
326                Message {
327                    internal_date: 0,
328                    flags: vec![Flag::Flagged, Flag::Passed],
329                    contents: vec![101, 10],
330                    path: "unknown".into(),
331                },
332            ),
333        ];
334
335        for folder in FolderIterator::new(
336            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
337                .join("resources")
338                .join("maildir"),
339            ".".into(),
340        )
341        .unwrap()
342        {
343            let folder = folder.unwrap();
344            let name = folder.name().unwrap_or("INBOX").to_string();
345
346            for message in folder {
347                let mut message = message.unwrap();
348                assert_ne!(message.internal_date(), 0);
349                assert!(message.path.exists());
350                message.internal_date = 0;
351                message.path = PathBuf::from("unknown");
352                messages.push((name.clone(), message));
353            }
354        }
355
356        messages.sort_unstable();
357        assert_eq!(messages, expected_messages);
358    }
359}