use dmarc_report_parser::{DkimResult, DmarcResult, Report, SpfResult};
use super::{alignment_label, dkim_pass, dmarc_pass, format_timestamp, spf_pass};
pub fn render(report: &Report) -> String {
let meta = &report.report_metadata;
let pol = &report.policy_published;
let total_messages: u64 = report.records.iter().map(|r| r.row.count).sum();
let mut html = String::from(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DMARC Report</title>
<style>
:root {
--pass: #16a34a;
--fail: #dc2626;
--bg: #f8fafc;
--card: #ffffff;
--border: #e2e8f0;
--text: #1e293b;
--muted: #64748b;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); padding: 2rem; line-height: 1.6; }
h1 { font-size: 1.5rem; margin-bottom: 1.5rem; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; margin-bottom: 1.25rem; }
.card h2 { font-size: 1.1rem; margin-bottom: 0.75rem; color: var(--muted); }
dl { display: grid; grid-template-columns: 10rem 1fr; gap: 0.25rem 1rem; }
dt { font-weight: 600; color: var(--muted); }
dd { margin: 0; }
table { width: 100%; border-collapse: collapse; margin-top: 0.75rem; font-size: 0.9rem; }
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); }
th { background: var(--bg); font-weight: 600; }
.pass { color: var(--pass); font-weight: 700; }
.fail { color: var(--fail); font-weight: 700; }
.badge { display: inline-block; padding: 0.1em 0.5em; border-radius: 4px; font-size: 0.85em; font-weight: 600; }
.badge-pass { background: #dcfce7; color: var(--pass); }
.badge-fail { background: #fee2e2; color: var(--fail); }
.summary { display: flex; gap: 2rem; flex-wrap: wrap; margin-top: 0.5rem; }
.summary-item { text-align: center; }
.summary-item .value { font-size: 1.5rem; font-weight: 700; }
.summary-item .label { font-size: 0.8rem; color: var(--muted); }
.auth-detail { font-size: 0.85rem; color: var(--muted); }
</style>
</head>
<body>
"#,
);
html.push_str(&format!(
"<h1>DMARC Aggregate Report — {}</h1>\n",
escape(&pol.domain)
));
html.push_str("<div class=\"card\">\n<h2>Report Source</h2>\n<dl>\n");
dl_row(&mut html, "Organization", &meta.org_name);
dl_row(&mut html, "Email", &meta.email);
if let Some(ref info) = meta.extra_contact_info {
dl_row(&mut html, "Contact Info", info);
}
dl_row(&mut html, "Report ID", &meta.report_id);
dl_row(
&mut html,
"Period",
&format!(
"{} → {}",
format_timestamp(meta.date_range.begin),
format_timestamp(meta.date_range.end)
),
);
if !meta.errors.is_empty() {
dl_row(&mut html, "Errors", &meta.errors.join(", "));
}
html.push_str("</dl>\n</div>\n");
html.push_str("<div class=\"card\">\n<h2>Published Policy</h2>\n<dl>\n");
dl_row(&mut html, "Domain", &pol.domain);
dl_row(&mut html, "Policy (p)", &pol.p.to_string());
dl_row(&mut html, "Sub-policy (sp)", &pol.sp.to_string());
dl_row(&mut html, "DKIM Alignment", alignment_label(&pol.adkim));
dl_row(&mut html, "SPF Alignment", alignment_label(&pol.aspf));
dl_row(&mut html, "Percentage", &format!("{}%", pol.pct));
if let Some(ref fo) = pol.fo {
dl_row(&mut html, "Failure Options", fo);
}
html.push_str("</dl>\n</div>\n");
html.push_str("<div class=\"card\">\n<h2>Summary</h2>\n<div class=\"summary\">\n");
summary_item(&mut html, &report.records.len().to_string(), "Records");
summary_item(&mut html, &total_messages.to_string(), "Messages");
let pass_count: u64 = report
.records
.iter()
.filter(|r| {
dmarc_pass(r.row.policy_evaluated.dkim) && dmarc_pass(r.row.policy_evaluated.spf)
})
.map(|r| r.row.count)
.sum();
summary_item(&mut html, &pass_count.to_string(), "Fully Passing");
html.push_str("</div>\n</div>\n");
html.push_str("<div class=\"card\">\n<h2>Records</h2>\n");
html.push_str("<table>\n<thead>\n<tr>");
for hdr in &[
"Source IP",
"Count",
"Disposition",
"DKIM",
"SPF",
"Header From",
"Envelope From",
"Auth Details",
] {
html.push_str(&format!("<th>{hdr}</th>"));
}
html.push_str("</tr>\n</thead>\n<tbody>\n");
for record in &report.records {
let row = &record.row;
let ident = &record.identifiers;
let auth = &record.auth_results;
html.push_str("<tr>");
html.push_str(&format!("<td>{}</td>", escape(&row.source_ip)));
html.push_str(&format!("<td>{}</td>", row.count));
html.push_str(&format!("<td>{}</td>", row.policy_evaluated.disposition));
html.push_str(&format!(
"<td>{}</td>",
badge_dmarc(row.policy_evaluated.dkim)
));
html.push_str(&format!(
"<td>{}</td>",
badge_dmarc(row.policy_evaluated.spf)
));
html.push_str(&format!("<td>{}</td>", escape(&ident.header_from)));
html.push_str(&format!(
"<td>{}</td>",
ident
.envelope_from
.as_deref()
.map(escape)
.unwrap_or_default()
));
html.push_str("<td class=\"auth-detail\">");
for dkim in &auth.dkim {
html.push_str(&format!(
"DKIM: {} <em>{}</em>",
badge_dkim(dkim.result),
escape(&dkim.domain)
));
if let Some(ref sel) = dkim.selector {
html.push_str(&format!(" (sel={})", escape(sel)));
}
html.push_str("<br>");
}
for spf in &auth.spf {
html.push_str(&format!(
"SPF: {} <em>{}</em>",
badge_spf(spf.result),
escape(&spf.domain)
));
if let Some(ref scope) = spf.scope {
html.push_str(&format!(" ({})", scope));
}
html.push_str("<br>");
}
html.push_str("</td>");
html.push_str("</tr>\n");
}
html.push_str("</tbody>\n</table>\n</div>\n");
html.push_str("</body>\n</html>\n");
html
}
fn dl_row(html: &mut String, label: &str, value: &str) {
html.push_str(&format!("<dt>{label}</dt><dd>{value}</dd>\n"));
}
fn summary_item(html: &mut String, value: &str, label: &str) {
html.push_str(&format!(
"<div class=\"summary-item\"><div class=\"value\">{value}</div><div class=\"label\">{label}</div></div>\n"
));
}
fn badge_dmarc(result: DmarcResult) -> String {
let cls = if dmarc_pass(result) {
"badge badge-pass"
} else {
"badge badge-fail"
};
format!("<span class=\"{cls}\">{result}</span>")
}
fn badge_dkim(result: DkimResult) -> String {
let cls = if dkim_pass(result) {
"badge badge-pass"
} else {
"badge badge-fail"
};
format!("<span class=\"{cls}\">{result}</span>")
}
fn badge_spf(result: SpfResult) -> String {
let cls = if spf_pass(result) {
"badge badge-pass"
} else {
"badge badge-fail"
};
format!("<span class=\"{cls}\">{result}</span>")
}
fn escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}