// // Copyright (c) 2025-2026 rustmailer.com (https://rustmailer.com) // // This file is part of the Bichon Email Archiving Project // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use memmap2::Mmap; use std::fs; use std::io; use std::path::Path; pub struct MboxFile { map: Mmap, } impl MboxFile { pub fn from_file(name: &Path) -> io::Result { let file = fs::File::open(name)?; let metadata = file.metadata()?; if metadata.len() == 0 { return Err(io::Error::new( io::ErrorKind::InvalidData, "Empty MBOX file", )); } let map = unsafe { Mmap::map(&file)? }; Ok(Self { map }) } pub fn iter(&self) -> MboxReader<'_> { MboxReader::new(&self.map) } } pub struct Entry<'a> { pub offset: usize, pub data: &'a [u8], } pub struct MboxReader<'a> { data: &'a [u8], len: usize, scan_pos: usize, body_start: Option, } impl<'a> MboxReader<'a> { fn new(data: &'a [u8]) -> Self { Self { data, len: data.len(), scan_pos: 0, body_start: None, } } fn is_from_line(&self, i: usize) -> bool { if i + 5 > self.len { return false; } if i == 0 { &self.data[0..5] == b"From " } else { self.data[i - 1] == b'\n' && &self.data[i..i + 5] == b"From " } } fn skip_from_line(&self, mut i: usize) -> usize { while i < self.len && self.data[i] != b'\n' { i += 1; } if i < self.len { i += 1; } i } } impl<'a> Iterator for MboxReader<'a> { type Item = Entry<'a>; fn next(&mut self) -> Option { while self.scan_pos < self.len { if self.is_from_line(self.scan_pos) { let from_pos = self.scan_pos; let body_pos = self.skip_from_line(from_pos); if let Some(start) = self.body_start { let entry = Entry { offset: start, data: &self.data[start..from_pos], }; self.body_start = Some(body_pos); self.scan_pos = body_pos; return Some(entry); } else { self.body_start = Some(body_pos); self.scan_pos = body_pos; continue; } } self.scan_pos += 1; } if let Some(start) = self.body_start.take() { return Some(Entry { offset: start, data: &self.data[start..self.len], }); } None } } #[cfg(test)] mod tests { use mail_parser::MessageParser; use crate::mbox::gmail::determine_folder; use super::*; fn collect_entries(data: &[u8]) -> Vec<&[u8]> { let reader = MboxReader::new(data); reader.map(|e| e.data).collect() } #[test] fn two_mails() { let data = b"From a\nmail1\nFrom b\nmail2\n"; let e = collect_entries(data); assert_eq!(e, vec![b"mail1\n", b"mail2\n"]); } #[test] fn no_trailing_newline() { let data = b"From a\nmail1"; let e = collect_entries(data); assert_eq!(e, vec![b"mail1"]); } #[test] fn from_inside_body() { let data = b"From a\nhello\nFrom is here\nbye\n"; let e = collect_entries(data); assert_eq!(e.len(), 2); } #[test] fn inline_from_not_separator() { let data = b"From a\nhello From world\n"; let e = collect_entries(data); assert_eq!(e.len(), 1); } #[test] fn realistic_mbox() { let data = b"From a\nH:1\n\nbody1\nFrom b\nH:2\n\nbody2\n"; let e = collect_entries(data); assert_eq!(e.len(), 2); } #[test] fn empty_body() { let data = b"From a\nFrom b\nbody\n"; let e = collect_entries(data); assert_eq!(e[0], b""); assert_eq!(e[1], b"body\n"); } #[test] fn only_from_line() { let data = b"From a\n"; let e = collect_entries(data); assert_eq!(e.len(), 1); assert_eq!(e[0], b""); } #[test] fn windows_newlines() { let data = b"From a\r\nbody\r\nFrom b\r\nbody2\r\n"; let e = collect_entries(data); assert_eq!(e.len(), 2); } #[test] fn many_small_mails() { let mut data = Vec::new(); for _ in 0..1000 { data.extend_from_slice(b"From a\nx\n"); } let e = collect_entries(&data); assert_eq!(e.len(), 1000); } #[test] fn test11() { let mbox = MboxFile::from_file(Path::new("e:\\test.mbox")).unwrap(); for e in mbox.iter() { let body = e.data; let message = MessageParser::new().parse(body).unwrap(); let labels = message.header("X-Gmail-Labels").unwrap().as_text().unwrap(); //println!("offset={} X-Gmail-Labels={:?}", e.offset, labels); println!( "X-Gmail-Labels={:?}, determine_folder={}", labels, determine_folder(labels) ) } } }