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