//! Message summaries and body extraction. use mailparse::{MailHeaderMap, ParsedMail}; use serde::Serialize; #[derive(Debug, Serialize)] pub struct MsgSummary { pub uid: u32, pub flags: Vec, pub size: Option, pub date: Option, pub from: Option, pub to: Option, pub subject: Option, } /// The header fields shown in listings, decoded (RFC 2047) by mailparse. #[derive(Debug, Default, PartialEq)] pub struct HeaderSummary { pub date: Option, pub from: Option, pub to: Option, pub subject: Option, } pub fn parse_header_summary(raw: &[u8]) -> HeaderSummary { match mailparse::parse_headers(raw) { Ok((headers, _)) => HeaderSummary { date: headers.get_first_value("Date"), from: headers.get_first_value("From"), to: headers.get_first_value("To"), subject: headers.get_first_value("Subject"), }, Err(_) => HeaderSummary::default(), } } pub fn flag_to_string(f: &imap::types::Flag) -> String { use imap::types::Flag::*; match f { Seen => "\\Seen".into(), Answered => "\\Answered".into(), Flagged => "\\Flagged".into(), Deleted => "\\Deleted".into(), Draft => "\\Draft".into(), Recent => "\\Recent".into(), Custom(c) => c.to_string(), other => format!("{other:?}"), } } pub fn summarize(f: &imap::types::Fetch) -> Option { let uid = f.uid?; let h = f.header().map(parse_header_summary).unwrap_or_default(); Some(MsgSummary { uid, flags: f.flags().iter().map(flag_to_string).collect(), size: f.size, date: h.date, from: h.from, to: h.to, subject: h.subject, }) } fn find_part(mail: &ParsedMail, mimetype: &str) -> Option { if mail.ctype.mimetype.eq_ignore_ascii_case(mimetype) { return mail.get_body().ok(); } mail.subparts.iter().find_map(|sp| find_part(sp, mimetype)) } /// Picks the best displayable body: text/plain by default, text/html on request, /// falling back to the other one when the preferred part is missing. pub fn extract_body(mail: &ParsedMail, prefer_html: bool) -> Option { let order: [&str; 2] = if prefer_html { ["text/html", "text/plain"] } else { ["text/plain", "text/html"] }; order.iter().find_map(|mt| find_part(mail, mt)) } #[cfg(test)] mod tests { use super::*; const PLAIN: &[u8] = b"From: Alice \r\n\ To: bob@example.com\r\n\ Date: Fri, 17 Jul 2026 10:00:00 -0300\r\n\ Subject: Hello\r\n\ Content-Type: text/plain; charset=utf-8\r\n\ \r\n\ Just checking in.\r\n"; // Subject uses RFC 2047 encoded words; body parts use quoted-printable/base64. const MULTIPART: &[u8] = b"From: =?utf-8?Q?Mar=C3=ADa?= \r\n\ To: bob@example.com\r\n\ Subject: =?utf-8?B?SG9sYSDwn5qA?=\r\n\ MIME-Version: 1.0\r\n\ Content-Type: multipart/alternative; boundary=XYZ\r\n\ \r\n\ --XYZ\r\n\ Content-Type: text/plain; charset=utf-8\r\n\ Content-Transfer-Encoding: quoted-printable\r\n\ \r\n\ Hola, =C2=BFtodo bien?\r\n\ --XYZ\r\n\ Content-Type: text/html; charset=utf-8\r\n\ Content-Transfer-Encoding: base64\r\n\ \r\n\ PGI+SG9sYTwvYj4=\r\n\ --XYZ--\r\n"; #[test] fn header_summary_reads_basic_fields() { let h = parse_header_summary(PLAIN); assert_eq!(h.from.as_deref(), Some("Alice ")); assert_eq!(h.subject.as_deref(), Some("Hello")); assert_eq!(h.date.as_deref(), Some("Fri, 17 Jul 2026 10:00:00 -0300")); } #[test] fn header_summary_decodes_rfc2047() { let h = parse_header_summary(MULTIPART); assert_eq!(h.from.as_deref(), Some("María ")); assert_eq!(h.subject.as_deref(), Some("Hola 🚀")); } #[test] fn garbage_headers_yield_empty_summary() { assert_eq!( parse_header_summary(b"\xff\xfe\x00"), HeaderSummary::default() ); } #[test] fn plain_body_is_extracted() { let mail = mailparse::parse_mail(PLAIN).unwrap(); assert_eq!( extract_body(&mail, false).unwrap().trim(), "Just checking in." ); } #[test] fn multipart_prefers_plain_and_decodes_qp() { let mail = mailparse::parse_mail(MULTIPART).unwrap(); assert_eq!( extract_body(&mail, false).unwrap().trim(), "Hola, ¿todo bien?" ); } #[test] fn html_flag_prefers_html_and_decodes_base64() { let mail = mailparse::parse_mail(MULTIPART).unwrap(); assert_eq!(extract_body(&mail, true).unwrap().trim(), "Hola"); } #[test] fn html_only_message_falls_back_when_plain_requested() { let raw = b"Content-Type: text/html\r\n\r\n

hi

\r\n"; let mail = mailparse::parse_mail(raw).unwrap(); assert_eq!(extract_body(&mail, false).unwrap().trim(), "

hi

"); } }