server: rework dmarc parsing to use askama
This commit is contained in:
parent
638e94b4ae
commit
c8850404b8
52
Cargo.lock
generated
52
Cargo.lock
generated
@ -193,6 +193,48 @@ version = "0.9.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
|
checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4"
|
||||||
|
dependencies = [
|
||||||
|
"askama_derive",
|
||||||
|
"itoa 1.0.15",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_derive"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f"
|
||||||
|
dependencies = [
|
||||||
|
"askama_parser",
|
||||||
|
"basic-toml",
|
||||||
|
"memchr",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc-hash",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"syn 2.0.104",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_parser"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"winnow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-graphql"
|
name = "async-graphql"
|
||||||
version = "7.0.17"
|
version = "7.0.17"
|
||||||
@ -524,6 +566,15 @@ version = "1.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "basic-toml"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@ -3152,6 +3203,7 @@ version = "0.17.27"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"askama",
|
||||||
"async-graphql",
|
"async-graphql",
|
||||||
"async-graphql-axum",
|
"async-graphql-axum",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
|||||||
@ -14,6 +14,7 @@ version.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
ammonia = "4.1.0"
|
ammonia = "4.1.0"
|
||||||
anyhow = "1.0.98"
|
anyhow = "1.0.98"
|
||||||
|
askama = { version = "0.14.0", features = ["derive"] }
|
||||||
async-graphql = { version = "7", features = ["log"] }
|
async-graphql = { version = "7", features = ["log"] }
|
||||||
async-graphql-axum = "7.0.16"
|
async-graphql-axum = "7.0.16"
|
||||||
async-trait = "0.1.88"
|
async-trait = "0.1.88"
|
||||||
|
|||||||
@ -2,4 +2,5 @@ fn main() {
|
|||||||
// Calling `build_info_build::build_script` collects all data and makes it available to `build_info::build_info!`
|
// Calling `build_info_build::build_script` collects all data and makes it available to `build_info::build_info!`
|
||||||
// and `build_info::format!` in the main program.
|
// and `build_info::format!` in the main program.
|
||||||
build_info_build::build_script();
|
build_info_build::build_script();
|
||||||
|
println!("cargo:rerun-if-changed=templates");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,4 +39,6 @@ pub enum ServerError {
|
|||||||
QueryParseError(#[from] QueryParserError),
|
QueryParseError(#[from] QueryParserError),
|
||||||
#[error("impossible: {0}")]
|
#[error("impossible: {0}")]
|
||||||
InfaillibleError(#[from] Infallible),
|
InfaillibleError(#[from] Infallible),
|
||||||
|
#[error("askama error: {0}")]
|
||||||
|
AskamaError(#[from] askama::Error),
|
||||||
}
|
}
|
||||||
|
|||||||
457
server/src/nm.rs
457
server/src/nm.rs
@ -4,6 +4,8 @@ use std::{
|
|||||||
io::Cursor,
|
io::Cursor,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use askama::Template;
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
use letterbox_notmuch::Notmuch;
|
use letterbox_notmuch::Notmuch;
|
||||||
use letterbox_shared::{compute_color, Rule};
|
use letterbox_shared::{compute_color, Rule};
|
||||||
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
|
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
|
||||||
@ -471,7 +473,9 @@ fn extract_zip(m: &ParsedMail) -> Result<Body, ServerError> {
|
|||||||
let name = file.name().to_lowercase();
|
let name = file.name().to_lowercase();
|
||||||
// Google DMARC reports are typically named like "google.com!example.com!...xml"
|
// Google DMARC reports are typically named like "google.com!example.com!...xml"
|
||||||
// and may or may not contain "dmarc" in the filename.
|
// and may or may not contain "dmarc" in the filename.
|
||||||
if name.ends_with(".xml") && (name.contains("dmarc") || name.starts_with("google.com!")) {
|
if name.ends_with(".xml")
|
||||||
|
&& (name.contains("dmarc") || name.starts_with("google.com!"))
|
||||||
|
{
|
||||||
let mut xml = String::new();
|
let mut xml = String::new();
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
if file.read_to_string(&mut xml).is_ok() {
|
if file.read_to_string(&mut xml).is_ok() {
|
||||||
@ -1175,194 +1179,265 @@ fn find_tags<'a, 'b>(rules: &'a [Rule], headers: &'b [MailHeader]) -> (bool, Has
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add this helper function to parse the DMARC XML and summarize it.
|
// Add this helper function to parse the DMARC XML and summarize it.
|
||||||
fn parse_dmarc_report(xml: &str) -> Result<String, ServerError> {
|
#[derive(Debug, serde::Deserialize)]
|
||||||
#[derive(Debug, serde::Deserialize)]
|
pub struct FormattedDateRange {
|
||||||
struct Feedback {
|
pub begin: String,
|
||||||
report_metadata: Option<ReportMetadata>,
|
pub end: String,
|
||||||
policy_published: Option<PolicyPublished>,
|
}
|
||||||
record: Option<Vec<Record>>,
|
|
||||||
}
|
pub struct FormattedReportMetadata {
|
||||||
#[derive(Debug, serde::Deserialize)]
|
pub org_name: String,
|
||||||
struct ReportMetadata {
|
pub email: String,
|
||||||
org_name: Option<String>,
|
pub report_id: String,
|
||||||
email: Option<String>,
|
pub date_range: Option<FormattedDateRange>,
|
||||||
report_id: Option<String>,
|
}
|
||||||
date_range: Option<DateRange>,
|
|
||||||
}
|
pub struct FormattedRecord {
|
||||||
#[derive(Debug, serde::Deserialize)]
|
pub source_ip: String,
|
||||||
struct DateRange {
|
pub count: String,
|
||||||
begin: Option<u64>,
|
pub header_from: String,
|
||||||
end: Option<u64>,
|
pub disposition: String,
|
||||||
}
|
pub dkim: String,
|
||||||
#[derive(Debug, serde::Deserialize)]
|
pub spf: String,
|
||||||
struct PolicyPublished {
|
pub auth_results: Option<FormattedAuthResults>,
|
||||||
domain: Option<String>,
|
}
|
||||||
adkim: Option<String>,
|
|
||||||
aspf: Option<String>,
|
pub struct FormattedAuthResults {
|
||||||
p: Option<String>,
|
pub dkim: Vec<FormattedAuthDKIM>,
|
||||||
sp: Option<String>,
|
pub spf: Vec<FormattedAuthSPF>,
|
||||||
pct: Option<String>,
|
}
|
||||||
}
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
pub struct FormattedAuthDKIM {
|
||||||
struct Record {
|
pub domain: String,
|
||||||
row: Option<Row>,
|
pub result: String,
|
||||||
identifiers: Option<Identifiers>,
|
pub selector: String,
|
||||||
auth_results: Option<AuthResults>,
|
}
|
||||||
}
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
pub struct FormattedAuthSPF {
|
||||||
struct Row {
|
pub domain: String,
|
||||||
source_ip: Option<String>,
|
pub result: String,
|
||||||
count: Option<u64>,
|
pub scope: String,
|
||||||
policy_evaluated: Option<PolicyEvaluated>,
|
}
|
||||||
}
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
pub struct FormattedPolicyPublished {
|
||||||
struct PolicyEvaluated {
|
pub domain: String,
|
||||||
disposition: Option<String>,
|
pub adkim: String,
|
||||||
dkim: Option<String>,
|
pub aspf: String,
|
||||||
spf: Option<String>,
|
pub p: String,
|
||||||
reason: Option<Vec<Reason>>,
|
pub sp: String,
|
||||||
}
|
pub pct: String,
|
||||||
#[derive(Debug, serde::Deserialize)]
|
}
|
||||||
struct Reason {
|
|
||||||
#[serde(rename = "type")]
|
pub struct FormattedFeedback {
|
||||||
reason_type: Option<String>,
|
pub report_metadata: Option<FormattedReportMetadata>,
|
||||||
comment: Option<String>,
|
pub policy_published: Option<FormattedPolicyPublished>,
|
||||||
}
|
pub record: Option<Vec<FormattedRecord>>,
|
||||||
#[derive(Debug, serde::Deserialize)]
|
}
|
||||||
struct Identifiers {
|
|
||||||
header_from: Option<String>,
|
#[derive(Debug, serde::Deserialize)]
|
||||||
}
|
pub struct Feedback {
|
||||||
#[derive(Debug, serde::Deserialize)]
|
pub report_metadata: Option<ReportMetadata>,
|
||||||
struct AuthResults {
|
pub policy_published: Option<PolicyPublished>,
|
||||||
dkim: Option<Vec<AuthDKIM>>,
|
pub record: Option<Vec<Record>>,
|
||||||
spf: Option<Vec<AuthSPF>>,
|
}
|
||||||
}
|
#[derive(Debug, serde::Deserialize)]
|
||||||
#[derive(Debug, serde::Deserialize)]
|
pub struct ReportMetadata {
|
||||||
struct AuthDKIM {
|
pub org_name: Option<String>,
|
||||||
domain: Option<String>,
|
pub email: Option<String>,
|
||||||
result: Option<String>,
|
pub report_id: Option<String>,
|
||||||
selector: Option<String>,
|
pub date_range: Option<DateRange>,
|
||||||
}
|
}
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
struct AuthSPF {
|
pub struct DateRange {
|
||||||
domain: Option<String>,
|
pub begin: Option<u64>,
|
||||||
result: Option<String>,
|
pub end: Option<u64>,
|
||||||
scope: Option<String>,
|
}
|
||||||
}
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
pub struct PolicyPublished {
|
||||||
let feedback: Feedback = xml_from_str(xml)
|
pub domain: Option<String>,
|
||||||
.map_err(|e| ServerError::StringError(format!("DMARC XML parse error: {e}")))?;
|
pub adkim: Option<String>,
|
||||||
let mut summary = String::new();
|
pub aspf: Option<String>,
|
||||||
if let Some(meta) = feedback.report_metadata {
|
pub p: Option<String>,
|
||||||
if let Some(org) = meta.org_name {
|
pub sp: Option<String>,
|
||||||
summary += &format!("<b>Reporter:</b> {}<br>", org);
|
pub pct: Option<String>,
|
||||||
}
|
}
|
||||||
if let Some(email) = meta.email {
|
#[derive(Debug, serde::Deserialize)]
|
||||||
summary += &format!("<b>Contact:</b> {}<br>", email);
|
pub struct Record {
|
||||||
}
|
pub row: Option<Row>,
|
||||||
if let Some(rid) = meta.report_id {
|
pub identifiers: Option<Identifiers>,
|
||||||
summary += &format!("<b>Report ID:</b> {}<br>", rid);
|
pub auth_results: Option<AuthResults>,
|
||||||
}
|
}
|
||||||
if let Some(dr) = meta.date_range {
|
#[derive(Debug, serde::Deserialize)]
|
||||||
if let (Some(begin), Some(end)) = (dr.begin, dr.end) {
|
pub struct Row {
|
||||||
use chrono::{NaiveDateTime, TimeZone, Utc};
|
pub source_ip: Option<String>,
|
||||||
let begin_dt = Utc.timestamp_opt(begin as i64, 0).single();
|
pub count: Option<u64>,
|
||||||
let end_dt = Utc.timestamp_opt(end as i64, 0).single();
|
pub policy_evaluated: Option<PolicyEvaluated>,
|
||||||
summary += &format!("<b>Date range:</b> {} to {}<br>",
|
}
|
||||||
begin_dt.map(|d| d.format("%Y-%m-%d").to_string()).unwrap_or(begin.to_string()),
|
#[derive(Debug, serde::Deserialize)]
|
||||||
end_dt.map(|d| d.format("%Y-%m-%d").to_string()).unwrap_or(end.to_string())
|
pub struct PolicyEvaluated {
|
||||||
);
|
pub disposition: Option<String>,
|
||||||
}
|
pub dkim: Option<String>,
|
||||||
}
|
pub spf: Option<String>,
|
||||||
}
|
pub reason: Option<Vec<Reason>>,
|
||||||
if let Some(pol) = feedback.policy_published {
|
}
|
||||||
summary += "<b>Policy Published:</b><ul>";
|
#[derive(Debug, serde::Deserialize)]
|
||||||
if let Some(domain) = pol.domain {
|
pub struct Reason {
|
||||||
summary += &format!("<li>Domain: {}</li>", domain);
|
#[serde(rename = "type")]
|
||||||
}
|
pub reason_type: Option<String>,
|
||||||
if let Some(adkim) = pol.adkim {
|
pub comment: Option<String>,
|
||||||
summary += &format!("<li>ADKIM: {}</li>", adkim);
|
}
|
||||||
}
|
#[derive(Debug, serde::Deserialize)]
|
||||||
if let Some(aspf) = pol.aspf {
|
pub struct Identifiers {
|
||||||
summary += &format!("<li>ASPF: {}</li>", aspf);
|
pub header_from: Option<String>,
|
||||||
}
|
}
|
||||||
if let Some(p) = pol.p {
|
#[derive(Debug, serde::Deserialize)]
|
||||||
summary += &format!("<li>Policy: {}</li>", p);
|
pub struct AuthResults {
|
||||||
}
|
pub dkim: Option<Vec<AuthDKIM>>,
|
||||||
if let Some(sp) = pol.sp {
|
pub spf: Option<Vec<AuthSPF>>,
|
||||||
summary += &format!("<li>Subdomain Policy: {}</li>", sp);
|
}
|
||||||
}
|
#[derive(Debug, serde::Deserialize)]
|
||||||
if let Some(pct) = pol.pct {
|
pub struct AuthDKIM {
|
||||||
summary += &format!("<li>Percent: {}</li>", pct);
|
pub domain: Option<String>,
|
||||||
}
|
pub result: Option<String>,
|
||||||
summary += "</ul>";
|
pub selector: Option<String>,
|
||||||
}
|
}
|
||||||
if let Some(records) = feedback.record {
|
#[derive(Debug, serde::Deserialize)]
|
||||||
summary += "<b>Records:</b><table style=\"border-collapse:collapse;width:100%;font-size:0.95em;\"><thead><tr style=\"background:#f0f0f0;\"><th style=\"border:1px solid #bbb;padding:4px 8px;\">Source IP</th><th style=\"border:1px solid #bbb;padding:4px 8px;\">Count</th><th style=\"border:1px solid #bbb;padding:4px 8px;\">Header From</th><th style=\"border:1px solid #bbb;padding:4px 8px;\">Disposition</th><th style=\"border:1px solid #bbb;padding:4px 8px;\">DKIM</th><th style=\"border:1px solid #bbb;padding:4px 8px;\">SPF</th><th style=\"border:1px solid #bbb;padding:4px 8px;\">Auth Results</th></tr></thead><tbody>";
|
pub struct AuthSPF {
|
||||||
for rec in records {
|
pub domain: Option<String>,
|
||||||
let mut row_html = String::new();
|
pub result: Option<String>,
|
||||||
let mut source_ip = String::new();
|
pub scope: Option<String>,
|
||||||
let mut count = String::new();
|
}
|
||||||
let mut header_from = String::new();
|
|
||||||
let mut disposition = String::new();
|
#[derive(Template)]
|
||||||
let mut dkim = String::new();
|
#[template(path = "dmarc_report.html")]
|
||||||
let mut spf = String::new();
|
pub struct DmarcReportTemplate<'a> {
|
||||||
if let Some(r) = &rec.row {
|
pub report: &'a FormattedFeedback,
|
||||||
if let Some(ref s) = r.source_ip {
|
}
|
||||||
source_ip = s.clone();
|
|
||||||
}
|
// Add this helper function to parse the DMARC XML and summarize it.
|
||||||
if let Some(c) = r.count {
|
pub fn parse_dmarc_report(xml: &str) -> Result<String, ServerError> {
|
||||||
count = c.to_string();
|
let feedback: Feedback = xml_from_str(xml)
|
||||||
}
|
.map_err(|e| ServerError::StringError(format!("DMARC XML parse error: {}", e)))?;
|
||||||
if let Some(ref pe) = r.policy_evaluated {
|
|
||||||
if let Some(ref disp) = pe.disposition {
|
let formatted_report_metadata = feedback.report_metadata.map(|meta| {
|
||||||
disposition = disp.clone();
|
let date_range = meta.date_range.map(|dr| FormattedDateRange {
|
||||||
}
|
begin: match Utc.timestamp_opt(dr.begin.unwrap_or(0) as i64, 0) {
|
||||||
if let Some(ref d) = pe.dkim {
|
chrono::LocalResult::Single(d) => Some(d),
|
||||||
dkim = d.clone();
|
_ => None,
|
||||||
}
|
}
|
||||||
if let Some(ref s) = pe.spf {
|
.map(|d| d.format("%Y-%m-%d").to_string())
|
||||||
spf = s.clone();
|
.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,
|
||||||
if let Some(ids) = &rec.identifiers {
|
}
|
||||||
if let Some(ref hf) = ids.header_from {
|
.map(|d| d.format("%Y-%m-%d").to_string())
|
||||||
header_from = hf.clone();
|
.unwrap_or_else(|| "".to_string()),
|
||||||
}
|
});
|
||||||
}
|
FormattedReportMetadata {
|
||||||
row_html += &format!("<tr><td style=\"border:1px solid #bbb;padding:4px 8px;\">{}</td><td style=\"border:1px solid #bbb;padding:4px 8px;\">{}</td><td style=\"border:1px solid #bbb;padding:4px 8px;\">{}</td><td style=\"border:1px solid #bbb;padding:4px 8px;\">{}</td><td style=\"border:1px solid #bbb;padding:4px 8px;\">{}</td><td style=\"border:1px solid #bbb;padding:4px 8px;\">{}</td><td style=\"border:1px solid #bbb;padding:4px 8px;\">",
|
org_name: meta.org_name.unwrap_or_else(|| "".to_string()),
|
||||||
source_ip, count, header_from, disposition, dkim, spf);
|
email: meta.email.unwrap_or_else(|| "".to_string()),
|
||||||
// Auth Results
|
report_id: meta.report_id.unwrap_or_else(|| "".to_string()),
|
||||||
let mut auths = String::new();
|
date_range,
|
||||||
if let Some(auth) = &rec.auth_results {
|
}
|
||||||
if let Some(dkims) = &auth.dkim {
|
});
|
||||||
for dkimres in dkims {
|
|
||||||
auths += &format!("<span style=\"white-space:nowrap;\">DKIM: domain=<b>{}</b> selector=<b>{}</b> result=<b>{}</b></span><br>",
|
let formatted_record = feedback.record.map(|records| {
|
||||||
dkimres.domain.as_deref().unwrap_or(""),
|
records
|
||||||
dkimres.selector.as_deref().unwrap_or(""),
|
.into_iter()
|
||||||
dkimres.result.as_deref().unwrap_or("")
|
.map(|rec| {
|
||||||
);
|
let auth_results = rec.auth_results.map(|auth| {
|
||||||
}
|
let dkim = auth
|
||||||
}
|
.dkim
|
||||||
if let Some(spfs) = &auth.spf {
|
.map(|dkims| {
|
||||||
for spfres in spfs {
|
dkims
|
||||||
auths += &format!("<span style=\"white-space:nowrap;\">SPF: domain=<b>{}</b> scope=<b>{}</b> result=<b>{}</b></span><br>",
|
.into_iter()
|
||||||
spfres.domain.as_deref().unwrap_or(""),
|
.map(|d| FormattedAuthDKIM {
|
||||||
spfres.scope.as_deref().unwrap_or(""),
|
domain: d.domain.unwrap_or_else(|| "".to_string()),
|
||||||
spfres.result.as_deref().unwrap_or("")
|
result: d.result.unwrap_or_else(|| "".to_string()),
|
||||||
);
|
selector: d.selector.unwrap_or_else(|| "".to_string()),
|
||||||
}
|
})
|
||||||
}
|
.collect()
|
||||||
}
|
})
|
||||||
row_html += &auths;
|
.unwrap_or_else(|| Vec::new());
|
||||||
row_html += "</td></tr>";
|
|
||||||
summary += &row_html;
|
let spf = auth
|
||||||
}
|
.spf
|
||||||
summary += "</tbody></table>";
|
.map(|spfs| {
|
||||||
}
|
spfs.into_iter()
|
||||||
if summary.is_empty() {
|
.map(|s| FormattedAuthSPF {
|
||||||
summary = "No DMARC summary found.".to_string();
|
domain: s.domain.unwrap_or_else(|| "".to_string()),
|
||||||
}
|
result: s.result.unwrap_or_else(|| "".to_string()),
|
||||||
Ok(summary)
|
scope: s.scope.unwrap_or_else(|| "".to_string()),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| Vec::new());
|
||||||
|
|
||||||
|
FormattedAuthResults { dkim, spf }
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
|
|
||||||
|
let formatted_policy_published =
|
||||||
|
feedback
|
||||||
|
.policy_published
|
||||||
|
.map(|pol| FormattedPolicyPublished {
|
||||||
|
domain: pol.domain.unwrap_or_else(|| "".to_string()),
|
||||||
|
adkim: pol.adkim.unwrap_or_else(|| "".to_string()),
|
||||||
|
aspf: pol.aspf.unwrap_or_else(|| "".to_string()),
|
||||||
|
p: pol.p.unwrap_or_else(|| "".to_string()),
|
||||||
|
sp: pol.sp.unwrap_or_else(|| "".to_string()),
|
||||||
|
pct: pol.pct.unwrap_or_else(|| "".to_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
let formatted_feedback = FormattedFeedback {
|
||||||
|
report_metadata: formatted_report_metadata,
|
||||||
|
policy_published: formatted_policy_published,
|
||||||
|
record: formatted_record,
|
||||||
|
};
|
||||||
|
|
||||||
|
let template = DmarcReportTemplate {
|
||||||
|
report: &formatted_feedback,
|
||||||
|
};
|
||||||
|
let html = template.render()?;
|
||||||
|
Ok(html)
|
||||||
}
|
}
|
||||||
|
|||||||
7
server/src/templates.rs
Normal file
7
server/src/templates.rs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
use askama::Template;
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "dmarc_report.html")]
|
||||||
|
pub struct DmarcReportTemplate<'a> {
|
||||||
|
pub feedback: &'a crate::nm::Feedback,
|
||||||
|
}
|
||||||
89
server/templates/dmarc_report.html
Normal file
89
server/templates/dmarc_report.html
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>DMARC Report</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% if report.report_metadata.is_some() %}
|
||||||
|
{% let meta = report.report_metadata.as_ref().unwrap() %}
|
||||||
|
<b>Reporter:</b> {{ meta.org_name }}<br>
|
||||||
|
<b>Contact:</b> {{ meta.email }}<br>
|
||||||
|
<b>Report ID:</b> {{ meta.report_id }}<br>
|
||||||
|
{% if meta.date_range.is_some() %}
|
||||||
|
{% let dr = meta.date_range.as_ref().unwrap() %}
|
||||||
|
<b>Date range:</b>
|
||||||
|
{{ dr.begin }}
|
||||||
|
to
|
||||||
|
{{ dr.end }}
|
||||||
|
<br>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if report.policy_published.is_some() %}
|
||||||
|
{% let pol = report.policy_published.as_ref().unwrap() %}
|
||||||
|
<b>Policy Published:</b>
|
||||||
|
<ul>
|
||||||
|
<li>Domain: {{ pol.domain }}</li>
|
||||||
|
<li>ADKIM: {{ pol.adkim }}</li>
|
||||||
|
<li>ASPF: {{ pol.aspf }}</li>
|
||||||
|
<li>Policy: {{ pol.p }}</li>
|
||||||
|
<li>Subdomain Policy: {{ pol.sp }}</li>
|
||||||
|
<li>Percent: {{ pol.pct }}</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if report.record.is_some() %}
|
||||||
|
<b>Records:</b>
|
||||||
|
<table style="border-collapse:collapse;width:100%;font-size:0.95em;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background:#f0f0f0;">
|
||||||
|
<th style="border:1px solid #bbb;padding:4px 8px;">Source IP</th>
|
||||||
|
<th style="border:1px solid #bbb;padding:4px 8px;">Count</th>
|
||||||
|
<th style="border:1px solid #bbb;padding:4px 8px;">Header From</th>
|
||||||
|
<th style="border:1px solid #bbb;padding:4px 8px;">Disposition</th>
|
||||||
|
<th style="border:1px solid #bbb;padding:4px 8px;">DKIM</th>
|
||||||
|
<th style="border:1px solid #bbb;padding:4px 8px;">SPF</th>
|
||||||
|
<th style="border:1px solid #bbb;padding:4px 8px;">Auth Results</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for rec in report.record.as_ref().unwrap() %}
|
||||||
|
<tr>
|
||||||
|
<td style="border:1px solid #bbb;padding:4px 8px;">{{ rec.source_ip }}</td>
|
||||||
|
<td style="border:1px solid #bbb;padding:4px 8px;">{{ rec.count }}</td>
|
||||||
|
<td style="border:1px solid #bbb;padding:4px 8px;">{{ rec.header_from }}</td>
|
||||||
|
<td style="border:1px solid #bbb;padding:4px 8px;">{{ rec.disposition }}</td>
|
||||||
|
<td style="border:1px solid #bbb;padding:4px 8px;">{{ rec.dkim }}</td>
|
||||||
|
<td style="border:1px solid #bbb;padding:4px 8px;">{{ rec.spf }}</td>
|
||||||
|
<td style="border:1px solid #bbb;padding:4px 8px;">
|
||||||
|
{% if rec.auth_results.is_some() %}
|
||||||
|
{% let auth = rec.auth_results.as_ref().unwrap() %}
|
||||||
|
{% for dkimres in auth.dkim %}
|
||||||
|
<span style="white-space:nowrap;">
|
||||||
|
DKIM: domain=<b>{{ dkimres.domain }}</b>
|
||||||
|
selector=<b>{{ dkimres.selector }}</b>
|
||||||
|
result=<b>{{ dkimres.result }}</b>
|
||||||
|
</span><br>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% for spfres in auth.spf %}
|
||||||
|
<span style="white-space:nowrap;">
|
||||||
|
SPF: domain=<b>{{ spfres.domain }}</b>
|
||||||
|
scope=<b>{{ spfres.scope }}</b>
|
||||||
|
result=<b>{{ spfres.result }}</b>
|
||||||
|
</span><br>
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if report.report_metadata.is_none() && report.policy_published.is_none() && report.record.is_none() %}
|
||||||
|
<p>No DMARC summary found.</p>
|
||||||
|
{% endif %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user