a
This commit is contained in:
@@ -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}}
|
||||
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user