Skip to main content

mailbox_formats/maildir/
reader.rs

1//! Maildir reader: iterate `cur/` + `new/` entries.
2
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use crate::error::Result;
7use crate::raw_message::{Flags, RawMessage};
8
9use super::flags::parse_filename;
10use super::Maildir;
11
12/// One Maildir entry. Path is the on-disk location; `unique_id` is the
13/// portion before the separator; `flags` is parsed from the suffix.
14#[derive(Debug, Clone, PartialEq, Eq)]
15#[non_exhaustive]
16pub struct MaildirEntry {
17    pub path: PathBuf,
18    pub unique_id: String,
19    pub flags: Flags,
20}
21
22impl MaildirEntry {
23    /// Read the message body. Headers and body are returned in a
24    /// [`RawMessage`] with `envelope_from = None` (Maildir doesn't
25    /// carry envelope-from per message).
26    pub fn read(&self) -> Result<RawMessage> {
27        let raw = fs::read(&self.path)?;
28        let (headers, body) = split_headers_body(&raw);
29        Ok(RawMessage {
30            headers,
31            body,
32            envelope_from: None,
33            timestamp: file_mtime(&self.path),
34            flags: self.flags,
35        })
36    }
37}
38
39impl Maildir {
40    /// Iterator over `cur/` then `new/` entries, sorted by filename
41    /// for deterministic order.
42    pub fn iter(&self) -> impl Iterator<Item = Result<MaildirEntry>> + '_ {
43        let cur = self.root.join("cur");
44        let new = self.root.join("new");
45        let sep = self.separator;
46
47        let cur_entries = collect_entries(&cur, sep);
48        let new_entries = collect_entries(&new, sep);
49        cur_entries.into_iter().chain(new_entries)
50    }
51}
52
53fn collect_entries(dir: &Path, separator: char) -> Vec<Result<MaildirEntry>> {
54    let mut out: Vec<Result<MaildirEntry>> = Vec::new();
55    let read_dir = match fs::read_dir(dir) {
56        Ok(rd) => rd,
57        Err(e) => {
58            // Folder doesn't exist or is unreadable — propagate one
59            // error and stop iterating this side. Maildir::open already
60            // validates the tree, so this is unusual.
61            out.push(Err(e.into()));
62            return out;
63        }
64    };
65
66    let mut files: Vec<PathBuf> = Vec::new();
67    for entry in read_dir {
68        match entry {
69            Ok(e) => {
70                let path = e.path();
71                if path.is_file() {
72                    files.push(path);
73                }
74            }
75            Err(e) => out.push(Err(e.into())),
76        }
77    }
78    // Sort for deterministic order.
79    files.sort();
80    for path in files {
81        let name = match path.file_name().and_then(|n| n.to_str()) {
82            Some(n) => n,
83            None => continue,
84        };
85        let (unique_id, flags) = parse_filename(name, separator);
86        out.push(Ok(MaildirEntry {
87            path,
88            unique_id,
89            flags,
90        }));
91    }
92    out
93}
94
95fn split_headers_body(raw: &[u8]) -> (Vec<(String, Vec<u8>)>, Vec<u8>) {
96    // Find the header/body boundary: first occurrence of \r\n\r\n or \n\n.
97    let boundary = find_boundary(raw);
98    let (header_bytes, body) = match boundary {
99        Some((end, body_start)) => (&raw[..end], raw[body_start..].to_vec()),
100        None => (raw, Vec::new()),
101    };
102    let headers = parse_headers(header_bytes);
103    (headers, body)
104}
105
106fn find_boundary(raw: &[u8]) -> Option<(usize, usize)> {
107    // \r\n\r\n => header end = i, body start = i + 4
108    // \n\n     => header end = i, body start = i + 2
109    let mut i = 0;
110    while i + 3 < raw.len() {
111        if &raw[i..i + 4] == b"\r\n\r\n" {
112            return Some((i, i + 4));
113        }
114        if &raw[i..i + 2] == b"\n\n" {
115            return Some((i, i + 2));
116        }
117        i += 1;
118    }
119    None
120}
121
122fn parse_headers(raw: &[u8]) -> Vec<(String, Vec<u8>)> {
123    let mut headers: Vec<(String, Vec<u8>)> = Vec::new();
124    for line in raw.split(|&b| b == b'\n') {
125        let trimmed = trim_eol(line);
126        if trimmed.is_empty() {
127            continue;
128        }
129        if matches!(trimmed.first(), Some(b' ') | Some(b'\t')) {
130            if let Some((_, v)) = headers.last_mut() {
131                v.push(b' ');
132                v.extend_from_slice(trim_leading_ws(trimmed));
133            }
134            continue;
135        }
136        let colon = match trimmed.iter().position(|&b| b == b':') {
137            Some(c) => c,
138            None => continue,
139        };
140        let name = match std::str::from_utf8(&trimmed[..colon]) {
141            Ok(s) => s.to_string(),
142            Err(_) => continue,
143        };
144        let mut value = trimmed[colon + 1..].to_vec();
145        if value.first() == Some(&b' ') {
146            value.remove(0);
147        }
148        headers.push((name, value));
149    }
150    headers
151}
152
153fn trim_eol(line: &[u8]) -> &[u8] {
154    let mut end = line.len();
155    if end > 0 && line[end - 1] == b'\r' {
156        end -= 1;
157    }
158    &line[..end]
159}
160
161fn trim_leading_ws(line: &[u8]) -> &[u8] {
162    let mut start = 0;
163    while start < line.len() && matches!(line[start], b' ' | b'\t') {
164        start += 1;
165    }
166    &line[start..]
167}
168
169fn file_mtime(path: &Path) -> std::time::SystemTime {
170    fs::metadata(path)
171        .and_then(|m| m.modified())
172        .unwrap_or(std::time::SystemTime::UNIX_EPOCH)
173}