From d0f4716d83ee9c3ccbd854d12fe8e3936b88e63d Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Mon, 11 Aug 2025 12:41:25 -0700 Subject: [PATCH] server: add gzip dmarc email support --- Cargo.lock | 1 + server/Cargo.toml | 1 + server/src/nm.rs | 102 +++++++++++++++++++++++++-------------- web/index.html | 3 +- web/src/view/mod.rs | 2 +- web/static/overrides.css | 9 ---- 6 files changed, 71 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7078335..79eb06b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3163,6 +3163,7 @@ dependencies = [ "chrono", "clap", "css-inline", + "flate2", "futures 0.3.31", "headers", "html-escape", diff --git a/server/Cargo.toml b/server/Cargo.toml index 0781af5..995297c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -24,6 +24,7 @@ cacher = { version = "0.2.0", registry = "xinu" } chrono = "0.4.40" clap = { version = "4.5.37", features = ["derive"] } css-inline = "0.17.0" +flate2 = "1.1.2" futures = "0.3.31" headers = "0.4.0" html-escape = "0.2.13" diff --git a/server/src/nm.rs b/server/src/nm.rs index de58bd7..9e6cdcd 100644 --- a/server/src/nm.rs +++ b/server/src/nm.rs @@ -451,42 +451,7 @@ fn extract_body(m: &ParsedMail, part_addr: &mut Vec) -> Result extract_mixed(m, part_addr), MULTIPART_ALTERNATIVE => extract_alternative(m, part_addr), MULTIPART_RELATED => extract_related(m, part_addr), - APPLICATION_ZIP => { - // Try to extract DMARC XML from ZIP - if let Ok(zip_bytes) = m.get_body_raw() { - if let Ok(mut archive) = ZipArchive::new(Cursor::new(&zip_bytes)) { - for i in 0..archive.len() { - if let Ok(mut file) = archive.by_index(i) { - let name = file.name().to_lowercase(); - // Google DMARC reports are typically named like "google.com!example.com!...xml" - // and may or may not contain "dmarc" in the filename. - if name.ends_with(".xml") && (name.contains("dmarc") || name.starts_with("google.com!")) { - let mut xml = String::new(); - use std::io::Read; - if file.read_to_string(&mut xml).is_ok() { - match parse_dmarc_report(&xml) { - Ok(report) => { - return Ok(Body::html(format!( - "
Google DMARC report summary:
{}
", - report - ))); - } - Err(e) => { - return Ok(Body::html(format!( - "
Failed to parse DMARC report XML: {}
", - e - ))); - } - } - } - } - } - } - } - } - // If no DMARC report found, fall through to unhandled - extract_unhandled(m) - } + APPLICATION_ZIP => extract_zip(m), _ => extract_unhandled(m), }; if let Err(err) = ret { @@ -496,6 +461,69 @@ fn extract_body(m: &ParsedMail, part_addr: &mut Vec) -> Result Result { + if let Ok(zip_bytes) = m.get_body_raw() { + if let Ok(mut archive) = ZipArchive::new(Cursor::new(&zip_bytes)) { + for i in 0..archive.len() { + if let Ok(mut file) = archive.by_index(i) { + let name = file.name().to_lowercase(); + // Google DMARC reports are typically named like "google.com!example.com!...xml" + // and may or may not contain "dmarc" in the filename. + if name.ends_with(".xml") && (name.contains("dmarc") || name.starts_with("google.com!")) { + let mut xml = String::new(); + use std::io::Read; + if file.read_to_string(&mut xml).is_ok() { + match parse_dmarc_report(&xml) { + Ok(report) => { + return Ok(Body::html(format!( + "
Google DMARC report summary:
{}
", + report + ))); + } + Err(e) => { + return Ok(Body::html(format!( + "
Failed to parse DMARC report XML: {}
", + e + ))); + } + } + } + } + } + } + } + } + // If no DMARC report found, fall through to unhandled + extract_unhandled(m) +} + +fn extract_gzip(m: &ParsedMail) -> Result { + if let Ok(gz_bytes) = m.get_body_raw() { + let mut decoder = flate2::read::GzDecoder::new(&gz_bytes[..]); + let mut xml = String::new(); + use std::io::Read; + if decoder.read_to_string(&mut xml).is_ok() { + match parse_dmarc_report(&xml) { + Ok(report) => { + return Ok(Body::html(format!( + "
Microsoft DMARC report summary:
{}
", + report + ))); + } + Err(e) => { + return Ok(Body::html(format!( + "
Failed to parse DMARC report XML: {}
", + e + ))); + } + } + } + } + extract_unhandled(m) +} + fn extract_unhandled(m: &ParsedMail) -> Result { let msg = format!( "Unhandled body content type:\n{}\n{}", @@ -565,6 +593,7 @@ fn extract_mixed(m: &ParsedMail, part_addr: &mut Vec) -> Result = m .subparts @@ -608,6 +637,7 @@ fn extract_mixed(m: &ParsedMail, part_addr: &mut Vec) -> Result parts.push(extract_gzip(sp)?), mt => parts.push(unhandled_html(MULTIPART_MIXED, mt)), } part_addr.pop(); diff --git a/web/index.html b/web/index.html index 1c41379..9738032 100644 --- a/web/index.html +++ b/web/index.html @@ -16,10 +16,11 @@ +
- \ No newline at end of file + diff --git a/web/src/view/mod.rs b/web/src/view/mod.rs index 53da2cd..ab53b6c 100644 --- a/web/src/view/mod.rs +++ b/web/src/view/mod.rs @@ -1025,7 +1025,7 @@ fn message_render(msg: &ShowThreadQueryThreadOnEmailThreadMessages, open: bool) ], IF!(open => div![ - C!["content", "bg-white", "text-black", "p-4", "min-w-full", "w-0","overflow-x-auto", from], + C!["content", "bg-white", "text-black", "p-4", "min-w-full", "w-0","overflow-x-auto", from.map(|f|format!("from-{f}"))], match &msg.body { ShowThreadQueryThreadOnEmailThreadMessagesBody::UnhandledContentType( ShowThreadQueryThreadOnEmailThreadMessagesBodyOnUnhandledContentType { contents ,content_tree}, diff --git a/web/static/overrides.css b/web/static/overrides.css index 126914b..05af127 100644 --- a/web/static/overrides.css +++ b/web/static/overrides.css @@ -57,15 +57,6 @@ html { margin-left: 2em; } -.mail-thread .content .noreply-news-bloomberg-com a { - background-color: initial !important; -} - -.mail-thread .content .noreply-news-bloomberg-com h2 { - margin: 0 !important; - padding: 0 !important; -} - /* Hackaday figures have unreadable black on dark grey */ .news-post figcaption.wp-caption-text { background-color: initial !important;