This commit is contained in:
2026-05-24 17:15:48 +00:00
parent 329d5c665a
commit 063b3b643f
22 changed files with 1348 additions and 92 deletions
+91
View File
@@ -0,0 +1,91 @@
{{define "title"}}DMARC Reports — {{.Domain.Name}}{{end}}
{{define "content"}}
<div class="flex items-center gap-3 mb-6">
<a href="/admin/domains" class="text-gray-400 text-sm hover:text-white">Domains</a>
<span class="text-gray-600">/</span>
<a href="/admin/domains/{{.Domain.ID}}" class="text-gray-400 text-sm hover:text-white">{{.Domain.Name}}</a>
<span class="text-gray-600">/</span>
<h1 class="text-xl font-bold text-white">DMARC Reports</h1>
</div>
{{if not .Domain.DMARCRua}}
<div class="card text-center" style="max-width:32rem;margin:0 auto">
<div class="text-sm text-gray-400 mb-3">DMARC monitoring is not enabled for this domain.</div>
<a href="/admin/domains/{{.Domain.ID}}" class="btn btn-primary btn-sm">Back to domain</a>
</div>
{{else if eq (len .Reports) 0}}
<div class="card text-center" style="max-width:32rem;margin:0 auto">
<div class="text-sm font-semibold text-gray-300 mb-2">No reports yet</div>
<div class="text-xs text-gray-500 mb-1">Monitoring address: <span class="text-blue-400">{{.Domain.DMARCRua}}</span></div>
<div class="text-xs text-gray-500 mb-4">Reports arrive after external senders process your DMARC policy. This can take 24-48 hours after DNS propagation.</div>
<a href="/admin/domains/{{.Domain.ID}}" class="btn btn-primary btn-sm">Back to domain</a>
</div>
{{else}}
<div class="flex items-center justify-between mb-4">
<div class="text-xs text-gray-500">Monitoring: <span class="text-blue-400">{{.Domain.DMARCRua}}</span> — {{len .Reports}} reports</div>
<a href="/admin/domains/{{.Domain.ID}}" class="btn btn-primary btn-sm">Back to domain</a>
</div>
{{range .Reports}}
<div class="card" style="margin-bottom:1.25rem">
<!-- Report header -->
<div class="flex items-start justify-between mb-3">
<div>
<div class="text-sm font-semibold text-white">{{.OrgName}}</div>
<div class="text-xs text-gray-400">{{.OrgEmail}}</div>
</div>
<div class="text-right">
<div class="text-xs text-gray-400">Report ID: <span class="text-gray-300">{{.ReportID}}</span></div>
<div class="text-xs text-gray-500">{{shortTime .ReceivedAt}}</div>
</div>
</div>
<!-- Date range + policy -->
<div style="display:flex;gap:2rem;margin-bottom:.75rem;font-size:.75rem;color:#9ca3af">
<div>Period: <span class="text-gray-300">{{unixDate .DateBegin}} to {{unixDate .DateEnd}}</span></div>
<div>Policy: <span class="text-gray-300">{{.PolicyP}}</span>
{{if .PolicyADKIM}}<span style="margin-left:.5rem">adkim=<span class="text-gray-300">{{.PolicyADKIM}}</span></span>{{end}}
{{if .PolicyASPF}}<span style="margin-left:.5rem">aspf=<span class="text-gray-300">{{.PolicyASPF}}</span></span>{{end}}
{{if .PolicyPct}}<span style="margin-left:.5rem">pct=<span class="text-gray-300">{{.PolicyPct}}%</span></span>{{end}}
</div>
</div>
<!-- IP records table -->
{{if .Records}}
<div style="overflow:auto">
<table style="font-size:.75rem;width:100%">
<thead>
<tr style="color:#6b7280;text-align:left;border-bottom:1px solid #374151">
<th style="padding:.375rem .5rem;white-space:nowrap">Source IP</th>
<th style="padding:.375rem .5rem;text-align:center">Count</th>
<th style="padding:.375rem .5rem;text-align:center">Disposition</th>
<th style="padding:.375rem .5rem;text-align:center">DKIM</th>
<th style="padding:.375rem .5rem;text-align:center">SPF</th>
<th style="padding:.375rem .5rem">From</th>
<th style="padding:.375rem .5rem">DKIM domain</th>
<th style="padding:.375rem .5rem">SPF domain</th>
</tr>
</thead>
<tbody>
{{range .Records}}
<tr style="border-bottom:1px solid #1f2937">
<td style="padding:.375rem .5rem;font-family:monospace;color:#93c5fd">{{.SourceIP}}</td>
<td style="padding:.375rem .5rem;text-align:center;color:#d1d5db">{{.Count}}</td>
<td style="padding:.375rem .5rem;text-align:center">{{dispositionBadge .Disposition}}</td>
<td style="padding:.375rem .5rem;text-align:center">{{passBadge .DKIMResult}}</td>
<td style="padding:.375rem .5rem;text-align:center">{{passBadge .SPFResult}}</td>
<td style="padding:.375rem .5rem;color:#9ca3af">{{.HeaderFrom}}</td>
<td style="padding:.375rem .5rem;color:#9ca3af;font-family:monospace">{{if .DKIMDomain}}{{.DKIMDomain}}{{if .DKIMSelector}}/{{.DKIMSelector}}{{end}}{{end}}</td>
<td style="padding:.375rem .5rem;color:#9ca3af;font-family:monospace">{{.SPFDomain}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="text-xs text-gray-500">No IP records in this report.</div>
{{end}}
</div>
{{end}}
{{end}}
{{end}}
+142 -8
View File
@@ -57,16 +57,14 @@
<!-- Right column -->
<div>
<!-- DKIM -->
<!-- DKIM key management -->
<div class="card">
<div class="text-sm font-semibold text-gray-300 mb-3">DKIM key</div>
{{if .Domain.DKIMPublic}}
<div class="text-xs text-gray-400 mb-1">
<div class="text-xs text-gray-400 mb-2">
Selector: <span class="text-white">{{.Domain.DKIMSelector}}</span> /
Algorithm: <span class="text-white">{{.Domain.DKIMAlgo}}</span>
</div>
<div class="text-xs text-gray-300 mb-3 font-semibold">DNS TXT record:</div>
<div style="background:#111827;border-radius:.375rem;padding:.625rem;font-size:.7rem;color:#6ee7b7;word-break:break-all;margin-bottom:.875rem;line-height:1.6">{{.DKIMRecord}}</div>
{{else}}
<div class="text-xs text-yellow-400 mb-3">No DKIM key generated yet.</div>
{{end}}
@@ -86,13 +84,84 @@
</form>
</div>
<!-- DNS hints -->
<!-- DMARC Monitoring -->
<div class="card">
<div class="text-sm font-semibold text-gray-300 mb-3">Recommended DNS records</div>
<div class="text-sm font-semibold text-gray-300 mb-3">DMARC Monitoring</div>
{{if .Domain.DMARCRua}}
<div class="text-xs text-gray-400 mb-1">Monitoring address</div>
<div style="font-family:monospace;font-size:.75rem;color:#60a5fa;word-break:break-all;margin-bottom:.75rem">{{.Domain.DMARCRua}}</div>
<div class="text-xs text-gray-500 mb-3">
Add this address to your DMARC DNS record as <span style="font-family:monospace;color:#9ca3af">rua=mailto:{{.Domain.DMARCRua}}</span>.
External mail servers will send aggregate reports here every 24 hours.
</div>
<div class="flex items-center gap-2">
<a href="/admin/domains/{{.Domain.ID}}/dmarc" class="btn btn-primary btn-sm">
View reports{{if .DMARCReportCount}} ({{.DMARCReportCount}}){{end}}
</a>
<form method="POST" action="/admin/domains/{{.Domain.ID}}/dmarc/disable"
onsubmit="return confirm('Disable DMARC monitoring? Incoming reports will no longer be processed.')">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<button type="submit" class="btn btn-danger btn-sm">Disable</button>
</form>
</div>
{{else}}
<div class="text-xs text-gray-400 mb-3">
Enable monitoring to receive DMARC aggregate reports from external mail servers.
A unique email address will be generated for this domain and reports delivered to it will be parsed and stored.
</div>
<form method="POST" action="/admin/domains/{{.Domain.ID}}/dmarc/enable">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
<button type="submit" class="btn btn-primary btn-sm">Enable DMARC monitoring</button>
</form>
{{end}}
</div>
<!-- DNS Records -->
<div class="card">
<div class="flex items-center justify-between mb-3">
<div class="text-sm font-semibold text-gray-300">DNS Records</div>
<button onclick="checkDNS({{.Domain.ID}}, this)" class="btn btn-primary btn-sm">Check DNS</button>
</div>
<div class="text-xs font-semibold text-gray-400 mb-2" style="text-transform:uppercase;letter-spacing:.05em">Required</div>
<div class="text-xs text-gray-400 mb-1">MX</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.MXRecord}}</div>
<div class="text-xs text-gray-400 mb-1">SPF</div>
<div style="background:#111827;border-radius:.375rem;padding:.5rem;font-size:.7rem;color:#93c5fd;word-break:break-all;margin-bottom:.75rem">{{.SPFHint}}</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.SPFHint}}</div>
{{if .DKIMRecord}}
<div class="text-xs text-gray-400 mb-1">DKIM (selector: {{.Domain.DKIMSelector}})</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.DKIMRecord}}</div>
{{else}}
<div class="text-xs text-yellow-400 mb-3">Generate a DKIM key above to get the DKIM DNS record.</div>
{{end}}
<div class="text-xs text-gray-400 mb-1">DMARC</div>
<div style="background:#111827;border-radius:.375rem;padding:.5rem;font-size:.7rem;color:#93c5fd;word-break:break-all">{{.DMARCHint}}</div>
<div class="dns-record" style="margin-bottom:.625rem">{{.DMARCHint}}</div>
{{if .Domain.DMARCRua}}
<div class="text-xs text-gray-500 mb-1">Monitoring address: <span style="color:#60a5fa;font-family:monospace">{{.Domain.DMARCRua}}</span></div>
{{end}}
<div style="margin-bottom:1rem"></div>
<div class="text-xs font-semibold text-gray-400 mb-2" style="text-transform:uppercase;letter-spacing:.05em">Optional (autoconfiguration)</div>
<div class="text-xs text-gray-500 mb-2">Allows mail clients (Thunderbird, Outlook) to auto-discover server settings.</div>
<div class="text-xs text-gray-400 mb-1">Thunderbird autoconfig (CNAME)</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.AutoconfigCNAME}}</div>
<div class="text-xs text-gray-400 mb-1">Outlook autodiscover (CNAME)</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.AutodiscoverCNAME}}</div>
<div class="text-xs text-gray-400 mb-1">SMTP submission (SRV, RFC 6186)</div>
<div class="dns-record" style="margin-bottom:.75rem">{{.SMTPSRVRecord}}</div>
<div class="text-xs text-gray-400 mb-1">IMAP SSL (SRV, RFC 6186)</div>
<div class="dns-record">{{.IMAPSRVRecord}}</div>
<!-- DNS check results injected here -->
<div id="dns-check-{{.Domain.ID}}" style="display:none;margin-top:1.25rem;padding-top:1rem;border-top:1px solid #374151"></div>
</div>
</div>
</div>
@@ -143,4 +212,69 @@
{{end}}
</div>
</div>
<style>
.dns-record {
background: #111827;
border-radius: .375rem;
padding: .5rem .625rem;
font-size: .7rem;
color: #93c5fd;
word-break: break-all;
line-height: 1.6;
font-family: monospace;
}
</style>
<script>
function checkDNS(id, btn) {
var orig = btn.textContent;
btn.disabled = true;
btn.textContent = 'Checking...';
var div = document.getElementById('dns-check-' + id);
div.style.display = 'block';
div.innerHTML = '<div style="font-size:.75rem;color:#9ca3af">Performing DNS lookups...</div>';
fetch('/admin/domains/' + id + '/dnscheck')
.then(function(r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(function(data) {
var html = '<div style="font-size:.75rem;font-weight:600;color:#d1d5db;margin-bottom:.625rem">DNS Check Results</div>';
html += dnsRow('MX', data.mx);
html += dnsRow('SPF', data.spf);
html += dnsRow('DKIM', data.dkim);
html += dnsRow('DMARC', data.dmarc);
div.innerHTML = html;
})
.catch(function(e) {
div.innerHTML = '<div style="font-size:.75rem;color:#f87171">DNS check failed: ' + esc(e.message) + '</div>';
})
.finally(function() {
btn.disabled = false;
btn.textContent = orig;
});
}
function dnsRow(name, rec) {
var colors = {ok:'#6ee7b7', warn:'#fbbf24', error:'#f87171', missing:'#9ca3af'};
var labels = {ok:'OK', warn:'WARN', error:'FAIL', missing:'MISSING'};
var c = colors[rec.status] || '#9ca3af';
var l = labels[rec.status] || rec.status.toUpperCase();
var h = '<div style="margin-bottom:.5rem;font-size:.75rem">';
h += '<span style="color:' + c + ';font-weight:700;font-family:monospace">[' + l + ']</span> ';
h += '<span style="color:#d1d5db;font-weight:600">' + esc(name) + '</span> ';
h += '<span style="color:#9ca3af">' + esc(rec.message) + '</span>';
if (rec.found) {
h += '<div style="color:#6b7280;margin-top:.25rem;word-break:break-all;padding-left:1rem;font-size:.7rem;font-family:monospace">' + esc(rec.found) + '</div>';
}
h += '</div>';
return h;
}
function esc(s) {
return (s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
{{end}}