use super::extract::{document_to_markdown, parse_message_bytes};
const FROM_SEP: &[u8] = b"From ";
fn looks_like_header_start(line: &[u8]) -> bool {
let mut saw_name = false;
for (i, &b) in line.iter().take(100).enumerate() {
match b {
b':' => return saw_name && i > 0,
33..=57 | 59..=126 => saw_name = true,
_ => return false,
}
}
false
}
fn postmark_tail(line: &[u8]) -> bool {
let tail: &[u8] = &line[FROM_SEP.len()..];
let tail = trim_ascii(tail);
tail.is_empty() || tail == b"-" || (tail.contains(&b':') && tail.iter().any(u8::is_ascii_digit))
}
fn trim_ascii(mut b: &[u8]) -> &[u8] {
while let [rest @ .., last] = b {
if last.is_ascii_whitespace() {
b = rest;
} else {
break;
}
}
while let [first, rest @ ..] = b {
if first.is_ascii_whitespace() {
b = rest;
} else {
break;
}
}
b
}
pub fn split_mbox(raw: &[u8]) -> Vec<Vec<u8>> {
let lines: Vec<&[u8]> = split_keep_eol(raw);
let mut messages: Vec<Vec<u8>> = Vec::new();
let mut current: Vec<u8> = Vec::new();
let mut started = false;
for (idx, &line) in lines.iter().enumerate() {
let next_is_headerish = lines
.get(idx + 1)
.map(|l| looks_like_header_start(l))
.unwrap_or(false);
if line_starts_with(line, FROM_SEP)
&& (idx == 0 || (postmark_tail(line) && next_is_headerish))
{
if started && !current.is_empty() {
messages.push(std::mem::take(&mut current));
}
started = true;
current.clear();
continue; }
if !started {
started = true;
}
if is_escaped_from(line) {
current.extend_from_slice(&line[1..]);
} else {
current.extend_from_slice(line);
}
}
if !current.is_empty() {
messages.push(current);
}
messages
}
fn line_starts_with(line: &[u8], prefix: &[u8]) -> bool {
line.len() >= prefix.len() && &line[..prefix.len()] == prefix
}
fn is_escaped_from(line: &[u8]) -> bool {
let mut i = 0;
while i < line.len() && line[i] == b'>' {
i += 1;
}
i > 0 && line_starts_with(&line[i..], FROM_SEP)
}
fn split_keep_eol(raw: &[u8]) -> Vec<&[u8]> {
let mut out = Vec::new();
let mut start = 0usize;
for (i, &b) in raw.iter().enumerate() {
if b == b'\n' {
out.push(&raw[start..=i]);
start = i + 1;
}
}
if start < raw.len() {
out.push(&raw[start..]);
}
out
}
#[derive(Clone)]
pub struct MboxMessageInfo {
pub index: usize,
pub subject: Option<String>,
pub from: Option<String>,
pub date: Option<String>,
pub message_id: Option<String>,
pub in_reply_to: Vec<String>,
pub references: Vec<String>,
}
pub struct MboxLoad {
pub markdown: String,
pub images: crate::chunk::ExtractedImages,
pub count: usize,
pub infos: Vec<MboxMessageInfo>,
pub skipped: Vec<String>,
}
pub fn mbox_to_markdown(raw: &[u8]) -> MboxLoad {
let messages = split_mbox(raw);
let count = messages.len();
let mut out = String::new();
let mut images: crate::chunk::ExtractedImages = Vec::new();
out.push_str(&format!(
"# Mailbox — {count} message{}\n\n",
if count == 1 { "" } else { "s" }
));
let mut infos: Vec<MboxMessageInfo> = Vec::with_capacity(count);
let mut skipped: Vec<String> = Vec::new();
for (i, msg_bytes) in messages.iter().enumerate() {
let doc = match parse_message_bytes(msg_bytes) {
Ok(doc) => doc,
Err(e) => {
skipped.push(format!("message {}: {e}", i + 1));
continue;
}
};
infos.push(MboxMessageInfo {
index: i + 1,
subject: doc.subject.clone(),
from: doc.from.clone(),
date: doc.date.clone(),
message_id: doc.message_id.clone(),
in_reply_to: doc.in_reply_to.clone(),
references: doc.references.clone(),
});
out.push_str(&format!("## Message {}\n\n", i + 1));
out.push_str(&document_to_markdown(&doc, 3));
out.push_str("\n\n");
for (name, bytes) in doc.images {
images.push((format!("msg{}_{name}", i + 1), bytes));
}
}
MboxLoad {
markdown: out.trim().to_string(),
images,
count,
infos,
skipped,
}
}