mailbox_formats/maildir/
reader.rs1use 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#[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 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 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 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 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 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 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}