server: include reason in dmarc report

This commit is contained in:
Bill Thiede 2025-08-12 08:16:26 -07:00
parent 01164d6afa
commit 8218fca2ef
2 changed files with 27 additions and 38 deletions

View File

@ -1199,6 +1199,7 @@ pub struct FormattedRecord {
pub disposition: String, pub disposition: String,
pub dkim: String, pub dkim: String,
pub spf: String, pub spf: String,
pub reason: Vec<String>,
pub auth_results: Option<FormattedAuthResults>, pub auth_results: Option<FormattedAuthResults>,
} }
@ -1280,7 +1281,7 @@ pub struct PolicyEvaluated {
pub spf: Option<String>, pub spf: Option<String>,
pub reason: Option<Vec<Reason>>, pub reason: Option<Vec<Reason>>,
} }
#[derive(Debug, serde::Deserialize)] #[derive(Debug, serde::Deserialize, Clone)]
pub struct Reason { pub struct Reason {
#[serde(rename = "type")] #[serde(rename = "type")]
pub reason_type: Option<String>, pub reason_type: Option<String>,
@ -1325,13 +1326,13 @@ pub fn parse_dmarc_report(xml: &str) -> Result<String, ServerError> {
chrono::LocalResult::Single(d) => Some(d), chrono::LocalResult::Single(d) => Some(d),
_ => None, _ => 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()), .unwrap_or_else(|| "".to_string()),
end: match Utc.timestamp_opt(dr.end.unwrap_or(0) as i64, 0) { end: match Utc.timestamp_opt(dr.end.unwrap_or(0) as i64, 0) {
chrono::LocalResult::Single(d) => Some(d), chrono::LocalResult::Single(d) => Some(d),
_ => None, _ => 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()), .unwrap_or_else(|| "".to_string()),
}); });
FormattedReportMetadata { FormattedReportMetadata {
@ -1378,41 +1379,25 @@ pub fn parse_dmarc_report(xml: &str) -> Result<String, ServerError> {
}); });
FormattedRecord { FormattedRecord {
source_ip: rec source_ip: rec.row.as_ref().and_then(|r| r.source_ip.clone()).unwrap_or_else(|| "".to_string()),
.row count: rec.row.as_ref().and_then(|r| r.count.map(|c| c.to_string())).unwrap_or_else(|| "".to_string()),
.as_ref() header_from: rec.identifiers.as_ref().and_then(|i| i.header_from.clone()).unwrap_or_else(|| "".to_string()),
.and_then(|r| r.source_ip.clone()) disposition: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.disposition.clone()).unwrap_or_else(|| "".to_string()),
.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()),
count: rec spf: rec.row.as_ref().and_then(|r| r.policy_evaluated.as_ref()).and_then(|p| p.spf.clone()).unwrap_or_else(|| "".to_string()),
.row 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| {
.as_ref() let mut s = String::new();
.and_then(|r| r.count.map(|c| c.to_string())) if let Some(reason_type) = r.reason_type {
.unwrap_or_else(|| "".to_string()), s.push_str(&format!("Type: {}", reason_type));
header_from: rec }
.identifiers if let Some(comment) = r.comment {
.as_ref() if !s.is_empty() { s.push_str(", "); }
.and_then(|i| i.header_from.clone()) s.push_str(&format!("Comment: {}", comment));
.unwrap_or_else(|| "".to_string()), }
disposition: rec s
.row }).collect(),
.as_ref() auth_results,
.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,
}
}) })
.collect() .collect()
}); });

View File

@ -74,6 +74,10 @@
</span><br> </span><br>
{% endfor %} {% endfor %}
{% for reason in rec.reason %}
<span style="white-space:nowrap;">Reason: {{ reason }}</span><br>
{% endfor %}
{% endif %} {% endif %}
</td> </td>
</tr> </tr>