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                if 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}
179
180impl Iterator for MessageIterator {
181    type Item = io::Result<Message>;
182
183    fn next(&mut self) -> Option<Self::Item> {
184        loop {
185            let entry = match self.cur_it.next().or_else(|| self.new_it.next()) {
186                Some(Ok(entry)) => entry,
187                Some(Err(err)) => return Some(Err(err)),
188                None => return None,
189            };
190            let path = entry.path();
191            if path.is_file() {
192                if let Some(name) = path.file_name().and_then(|name| name.to_str()) {
193                    if !name.starts_with('.') {
194                        let internal_date = match fs::metadata(&path)
195                            .and_then(|m| m.modified())
196                            .and_then(|d| {
197                                d.duration_since(std::time::UNIX_EPOCH)
198                                    .map(|d| d.as_secs())
199                                    .map_err(|e| {
200                                        io::Error::new(io::ErrorKind::InvalidData, e.to_string())
201                                    })
202                            }) {
203                            Ok(metadata) => metadata,
204                            Err(err) => return Some(Err(err)),
205                        };
206                        let contents = match fs::read(&path) {
207                            Ok(contents) => contents,
208                            Err(err) => return Some(Err(err)),
209                        };
210                        let mut flags = Vec::new();
211                        if let Some((_, part)) = name.rsplit_once("2,") {
212                            for &ch in part.as_bytes() {
213                                match ch {
214                                    b'P' => flags.push(Flag::Passed),
215                                    b'R' => flags.push(Flag::Replied),
216                                    b'S' => flags.push(Flag::Seen),
217                                    b'T' => flags.push(Flag::Trashed),
218                                    b'D' => flags.push(Flag::Draft),
219                                    b'F' => flags.push(Flag::Flagged),
220                                    _ => {
221                                        if !ch.is_ascii_alphanumeric() {
222                                            break;
223                                        }
224                                    }
225                                }
226                            }
227                        }
228                        return Some(Ok(Message {
229                            contents,
230                            internal_date,
231                            flags,
232                            path: path.to_path_buf(),
233                        }));
234                    }
235                }
236            }
237        }
238    }
239}
240
241impl Message {
242    /// Returns the message creation date in seconds since UNIX epoch
243    pub fn internal_date(&self) -> u64 {
244        self.internal_date
245    }
246
247    /// Returns the message flags
248    pub fn flags(&self) -> &[Flag] {
249        &self.flags
250    }
251
252    /// Returns the path to the message file
253    pub fn path(&self) -> &Path {
254        &self.path
255    }
256
257    /// Returns the message contents
258    pub fn contents(&self) -> &[u8] {
259        &self.contents
260    }
261
262    /// Unwraps the message contents
263    pub fn unwrap_contents(self) -> Vec<u8> {
264        self.contents
265    }
266}
267
268#[cfg(test)]
269mod tests {
270    use std::path::PathBuf;
271
272    use crate::mailbox::maildir::{Flag, Message};
273
274    use super::FolderIterator;
275
276    #[test]
277    fn parse_maildir() {
278        let mut messages = Vec::new();
279        let expected_messages = vec![
280            (
281                "INBOX".to_string(),
282                Message {
283                    internal_date: 0,
284                    flags: vec![Flag::Seen],
285                    contents: vec![98, 10],
286                    path: "unknown".into(),
287                },
288            ),
289            (
290                "INBOX".to_string(),
291                Message {
292                    internal_date: 0,
293                    flags: vec![Flag::Seen, Flag::Trashed],
294                    contents: vec![97, 10],
295                    path: "unknown".into(),
296                },
297            ),
298            (
299                "My Folder".to_string(),
300                Message {
301                    internal_date: 0,
302                    flags: vec![],
303                    contents: vec![100, 10],
304                    path: "unknown".into(),
305                },
306            ),
307            (
308                "My Folder".to_string(),
309                Message {
310                    internal_date: 0,
311                    flags: vec![Flag::Trashed, Flag::Draft, Flag::Replied],
312                    contents: vec![99, 10],
313                    path: "unknown".into(),
314                },
315            ),
316            (
317                "My Folder.Nested Folder".to_string(),
318                Message {
319                    internal_date: 0,
320                    flags: vec![Flag::Replied, Flag::Draft, Flag::Flagged],
321                    contents: vec![102, 10],
322                    path: "unknown".into(),
323                },
324            ),
325            (
326                "My Folder.Nested Folder".to_string(),
327                Message {
328                    internal_date: 0,
329                    flags: vec![Flag::Flagged, Flag::Passed],
330                    contents: vec![101, 10],
331                    path: "unknown".into(),
332                },
333            ),
334        ];
335
336        for folder in FolderIterator::new(
337            PathBuf::from(env!("CARGO_MANIFEST_DIR"))
338                .join("resources")
339                .join("maildir"),
340            ".".into(),
341        )
342        .unwrap()
343        {
344            let folder = folder.unwrap();
345            let name = folder.name().unwrap_or("INBOX").to_string();
346
347            for message in folder {
348                let mut message = message.unwrap();
349                assert_ne!(message.internal_date(), 0);
350                assert!(message.path.exists());
351                message.internal_date = 0;
352                message.path = PathBuf::from("unknown");
353                messages.push((name.clone(), message));
354            }
355        }
356
357        messages.sort_unstable();
358        assert_eq!(messages, expected_messages);
359    }
360}