From 8218fca2ef5777d7f4ec742bc08cdb19a5d099a7 Mon Sep 17 00:00:00 2001 From: Bill Thiede Date: Tue, 12 Aug 2025 08:16:26 -0700 Subject: [PATCH] server: include reason in dmarc report --- server/src/nm.rs | 61 +++++++++++------------------- server/templates/dmarc_report.html | 4 ++ 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/server/src/nm.rs b/server/src/nm.rs index 21709b1..55bbf41 100644 --- a/server/src/nm.rs +++ b/server/src/nm.rs @@ -1199,6 +1199,7 @@ pub struct FormattedRecord { pub disposition: String, pub dkim: String, pub spf: String, + pub reason: Vec, pub auth_results: Option, } @@ -1280,7 +1281,7 @@ pub struct PolicyEvaluated { pub spf: Option, pub reason: Option>, } -#[derive(Debug, serde::Deserialize)] +#[derive(Debug, serde::Deserialize, Clone)] pub struct Reason { #[serde(rename = "type")] pub reason_type: Option, @@ -1325,13 +1326,13 @@ pub fn parse_dmarc_report(xml: &str) -> Result { chrono::LocalResult::Single(d) => Some(d), _ => None, } - .map(|d| d.format("%Y-%m-%d").to_string()) + .map(|d| d.format("%Y-%m-%d %H:%M:%S").to_string()) .unwrap_or_else(|| "".to_string()), end: match Utc.timestamp_opt(dr.end.unwrap_or(0) as i64, 0) { chrono::LocalResult::Single(d) => Some(d), _ => None, } - .map(|d| d.format("%Y-%m-%d").to_string()) + .map(|d| d.format("%Y-%m-%d %H:%M:%S").to_string()) .unwrap_or_else(|| "".to_string()), }); FormattedReportMetadata { @@ -1378,41 +1379,25 @@ pub fn parse_dmarc_report(xml: &str) -> Result { }); FormattedRecord { - source_ip: rec - .row - .as_ref() - .and_then(|r| r.source_ip.clone()) - .unwrap_or_else(|| "".to_string()), - count: rec - .row - .as_ref() - .and_then(|r| r.count.map(|c| c.to_string())) - .unwrap_or_else(|| "".to_string()), - header_from: rec - .identifiers - .as_ref() - .and_then(|i| i.header_from.clone()) - .unwrap_or_else(|| "".to_string()), - disposition: rec - .row - .as_ref() - .and_then(|r| r.policy_evaluated.as_ref()) - .and_then(|p| p.disposition.clone()) - .unwrap_or_else(|| "".to_string()), - dkim: rec - .row - .as_ref() - .and_then(|r| r.policy_evaluated.as_ref()) - .and_then(|p| p.dkim.clone()) - .unwrap_or_else(|| "".to_string()), - spf: rec - .row - .as_ref() - .and_then(|r| r.policy_evaluated.as_ref()) - .and_then(|p| p.spf.clone()) - .unwrap_or_else(|| "".to_string()), - auth_results, - } + source_ip: rec.row.as_ref().and_then(|r| r.source_ip.clone()).unwrap_or_else(|| "".to_string()), + count: rec.row.as_ref().and_then(|r| r.count.map(|c| c.to_string())).unwrap_or_else(|| "".to_string()), + header_from: rec.identifiers.as_ref().and_then(|i| i.header_from.clone()).unwrap_or_else(|| "".to_string()), + disposition: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.disposition.clone()).unwrap_or_else(|| "".to_string()), + dkim: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.dkim.clone()).unwrap_or_else(|| "".to_string()), + spf: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.spf.clone()).unwrap_or_else(|| "".to_string()), + reason: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.reason.clone()).unwrap_or_else(|| Vec::new()).into_iter().map(|r| { + let mut s = String::new(); + if let Some(reason_type) = r.reason_type { + s.push_str(&format!("Type: {}", reason_type)); + } + if let Some(comment) = r.comment { + if !s.is_empty() { s.push_str(", "); } + s.push_str(&format!("Comment: {}", comment)); + } + s + }).collect(), + auth_results, + } }) .collect() }); diff --git a/server/templates/dmarc_report.html b/server/templates/dmarc_report.html index b5505e0..db76c3e 100644 --- a/server/templates/dmarc_report.html +++ b/server/templates/dmarc_report.html @@ -74,6 +74,10 @@
{% endfor %} + {% for reason in rec.reason %} + Reason: {{ reason }}
+ {% endfor %} + {% endif %}