1use crate::error::{HypermailError, Result};
2use std::io::{BufRead, BufReader, Read};
3
4const MAX_MESSAGE_SIZE: usize = 100 * 1024 * 1024;
10
11const MAX_LINE_SIZE: usize = 10 * 1024 * 1024;
17
18#[derive(Debug, Clone, PartialEq)]
20pub enum MboxFormat {
21 MboxO,
22 MboxRd,
23 MboxCl,
24 MboxCl2,
25}
26
27#[derive(Debug, Clone)]
29pub struct RawMessage {
30 pub from_line: String,
31 pub headers: Vec<u8>,
32 pub body: Vec<u8>,
33}
34
35pub struct MboxReader<R: Read> {
42 reader: BufReader<R>,
43 format: MboxFormat,
44 line_num: usize,
45 buffer: Vec<u8>,
46 eof: bool,
47 max_message_size: usize,
48}
49
50impl<R: Read> MboxReader<R> {
51 pub fn new(reader: R, format: MboxFormat) -> Self {
53 MboxReader {
54 reader: BufReader::new(reader),
55 format,
56 line_num: 0,
57 buffer: Vec::new(),
58 eof: false,
59 max_message_size: MAX_MESSAGE_SIZE,
60 }
61 }
62
63 pub fn with_max_message_size(mut self, size: usize) -> Self {
65 self.max_message_size = size;
66 self
67 }
68
69 fn is_from_line(line: &[u8]) -> bool {
70 line.starts_with(b"From ") && line.len() > 5
71 }
72
73 fn unescape_mboxrd(line: &[u8]) -> Vec<u8> {
74 if line.len() > 1 && line[0] == b'>' {
78 let gt_count = line.iter().take_while(|&&b| b == b'>').count();
80 if line[gt_count..].starts_with(b"From ") {
82 return line[1..].to_vec();
83 }
84 }
85 line.to_vec()
86 }
87}
88
89impl<R: Read> Iterator for MboxReader<R> {
90 type Item = Result<RawMessage>;
91
92 fn next(&mut self) -> Option<Self::Item> {
93 if self.eof {
94 return None;
95 }
96
97 loop {
98 let mut line = Vec::new();
99 self.line_num += 1;
100
101 match self.reader.read_until(b'\n', &mut line) {
102 Ok(0) => {
103 self.eof = true;
104 if self.buffer.is_empty() {
105 return None;
106 }
107 break;
108 },
109 Ok(_) => {},
110 Err(e) => {
111 return Some(Err(HypermailError::MboxParse {
112 line: self.line_num,
113 message: format!("read error: {e}"),
114 }))
115 },
116 }
117
118 if line.len() > MAX_LINE_SIZE {
121 return Some(Err(HypermailError::MboxParse {
122 line: self.line_num,
123 message: format!(
124 "line exceeds maximum size ({} bytes > {} bytes)",
125 line.len(),
126 MAX_LINE_SIZE
127 ),
128 }));
129 }
130
131 if line.last() == Some(&b'\n') {
132 line.pop();
133 if line.last() == Some(&b'\r') {
134 line.pop();
135 }
136 }
137
138 if self.buffer.is_empty() {
139 if Self::is_from_line(&line) {
140 self.buffer = line;
141 self.buffer.push(b'\n');
142 continue;
143 }
144 self.buffer = line;
145 self.buffer.push(b'\n');
146 continue;
147 }
148
149 if Self::is_from_line(&line) {
150 let raw = self.buffer.split_off(0);
151 self.buffer = line;
152 self.buffer.push(b'\n');
153 return Some(Ok(parse_raw_message(&raw)));
154 }
155
156 let new_size = self.buffer.len() + line.len() + 1;
159 if new_size > self.max_message_size {
160 self.buffer.clear();
161 return Some(Err(HypermailError::MboxParse {
162 line: self.line_num,
163 message: format!(
164 "message exceeds maximum size ({} bytes > {} bytes)",
165 new_size, self.max_message_size
166 ),
167 }));
168 }
169
170 if self.format == MboxFormat::MboxRd {
171 let unescaped = Self::unescape_mboxrd(&line);
172 self.buffer.extend_from_slice(&unescaped);
173 } else {
174 self.buffer.extend_from_slice(&line);
175 }
176 self.buffer.push(b'\n');
177 }
178
179 if self.buffer.is_empty() {
180 return None;
181 }
182 let raw = std::mem::take(&mut self.buffer);
183 Some(Ok(parse_raw_message(&raw)))
184 }
185}
186
187fn parse_raw_message(data: &[u8]) -> RawMessage {
188 let from_end = data.iter().position(|&b| b == b'\n').unwrap_or(data.len());
189 let from_line = String::from_utf8_lossy(&data[..from_end]).trim_end().to_string();
190
191 let rest = if from_end + 1 < data.len() {
192 &data[from_end + 1..]
193 } else {
194 &[]
195 };
196
197 let sep = rest
198 .windows(2)
199 .position(|w| w == b"\n\n")
200 .or_else(|| rest.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 2));
201
202 if let Some(headers_end) = sep {
203 let header_bytes = rest[..headers_end].to_vec();
204 let body_bytes = if headers_end + 2 < rest.len() {
205 let skip = if rest[headers_end..].starts_with(b"\r\n") {
206 4
207 } else {
208 2
209 };
210 if headers_end + skip < rest.len() {
211 rest[headers_end + skip..].to_vec()
212 } else {
213 Vec::new()
214 }
215 } else {
216 Vec::new()
217 };
218 RawMessage { from_line, headers: header_bytes, body: body_bytes }
219 } else {
220 RawMessage { from_line, headers: rest.to_vec(), body: Vec::new() }
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use std::io::Cursor;
228
229 fn create_mbox_data() -> Vec<u8> {
230 b"From alice@example.com Mon Jan 01 12:00:00 2024\n\
231 From: Alice <alice@example.com>\n\
232 Subject: First message\n\
233 Message-ID: <001@example.com>\n\
234 Date: Mon, 01 Jan 2024 12:00:00 +0000\n\
235 \n\
236 This is the first message body.\n\
237 \n\
238 From bob@example.com Mon Jan 01 13:00:00 2024\n\
239 From: Bob <bob@example.com>\n\
240 Subject: Re: First message\n\
241 Message-ID: <002@example.com>\n\
242 In-Reply-To: <001@example.com>\n\
243 Date: Mon, 01 Jan 2024 13:00:00 +0000\n\
244 \n\
245 This is a reply.\n\
246 \n\
247 From carol@example.com Mon Jan 01 14:00:00 2024\n\
248 From: Carol <carol@example.com>\n\
249 Subject: Another thread\n\
250 Message-ID: <003@example.com>\n\
251 Date: Mon, 01 Jan 2024 14:00:00 +0000\n\
252 \n\
253 A different conversation.\n"
254 .to_vec()
255 }
256
257 #[test]
258 fn test_parse_mbox_basic() {
259 let data = create_mbox_data();
260 let cursor = Cursor::new(data);
261 let reader = MboxReader::new(cursor, MboxFormat::MboxO);
262 let messages: Vec<Result<RawMessage>> = reader.collect();
263 assert_eq!(messages.len(), 3);
264
265 let msg0 = messages[0].as_ref().unwrap();
266 assert!(msg0.from_line.contains("alice@example.com"));
267
268 let headers = crate::headers::parse_headers(&msg0.headers);
269 assert_eq!(crate::headers::find_header(&headers, "Subject"), Some("First message"));
270 }
271
272 #[test]
273 fn test_from_line_parsing() {
274 assert!(MboxReader::<std::io::Empty>::is_from_line(b"From alice@example.com Mon Jan 01"));
275 assert!(!MboxReader::<std::io::Empty>::is_from_line(b"From: alice@example.com"));
276 assert!(!MboxReader::<std::io::Empty>::is_from_line(b""));
277 }
278
279 #[test]
280 fn test_empty_mbox() {
281 let cursor = Cursor::new(b"");
282 let reader = MboxReader::new(cursor, MboxFormat::MboxO);
283 let count = reader.count();
284 assert_eq!(count, 0);
285 }
286
287 #[test]
288 fn test_mbox_no_headers() {
289 let data = b"From alice@example.com\n\nJust a body\n".to_vec();
290 let cursor = Cursor::new(data);
291 let reader = MboxReader::new(cursor, MboxFormat::MboxO);
292 let messages: Vec<Result<RawMessage>> = reader.collect();
293 assert_eq!(messages.len(), 1);
294 assert!(messages[0].is_ok());
295 }
296
297 #[test]
298 fn test_multipart_message_with_from_in_body() {
299 let data = b"From alice@example.com\n\
300 From: Alice <alice@example.com>\n\
301 Subject: Test\n\
302 \n\
303 This line looks like\n\
304 >From someone else\n\
305 but shouldn't be split.\n"
306 .to_vec();
307 let cursor = Cursor::new(data);
308 let reader = MboxReader::new(cursor, MboxFormat::MboxRd);
309 let messages: Vec<Result<RawMessage>> = reader.collect();
310 assert_eq!(messages.len(), 1);
311 let msg = messages[0].as_ref().unwrap();
312 assert_eq!(msg.from_line, "From alice@example.com");
313 let body_str = std::str::from_utf8(&msg.body).unwrap();
314 assert!(body_str.contains("From someone else"));
315 }
316
317 #[test]
318 fn test_max_message_size_exceeded() {
319 let data = b"From alice@example.com\n\
320 From: Alice <alice@example.com>\n\
321 \n\
322 This is a very long body line that exceeds our tiny limit.\n"
323 .to_vec();
324 let cursor = Cursor::new(data);
325 let reader = MboxReader::new(cursor, MboxFormat::MboxO).with_max_message_size(10);
326 let results: Vec<Result<RawMessage>> = reader.collect();
327 assert!(
328 results.iter().any(|r| r.is_err()),
329 "Should fail when message exceeds size limit"
330 );
331 }
332
333 #[test]
334 fn test_mboxrd_unescape() {
335 let data = b"From alice@example.com\n\
337 Subject: Test\n\
338 \n\
339 >From someone we know\n"
340 .to_vec();
341 let cursor = Cursor::new(data);
342 let reader = MboxReader::new(cursor, MboxFormat::MboxRd);
343 let messages: Vec<Result<RawMessage>> = reader.collect();
344 assert_eq!(messages.len(), 1);
345 let msg = messages[0].as_ref().unwrap();
346 let body = std::str::from_utf8(&msg.body).unwrap();
347 assert!(body.contains("From someone"), "'>From' should be unescaped to 'From'");
348 assert!(!body.contains(">From"), "unescaped body should not contain '>From'");
349 }
350
351 #[test]
352 fn test_parse_mbox_three_messages_bodies() {
353 let data = create_mbox_data();
354 let cursor = Cursor::new(data);
355 let reader = MboxReader::new(cursor, MboxFormat::MboxO);
356 let messages: Vec<Result<RawMessage>> = reader.collect();
357 assert_eq!(messages.len(), 3);
358 let body0 = std::str::from_utf8(&messages[0].as_ref().unwrap().body).unwrap();
359 assert!(body0.contains("first message body"));
360 let body1 = std::str::from_utf8(&messages[1].as_ref().unwrap().body).unwrap();
361 assert!(body1.contains("reply"));
362 }
363}