<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Mail::DMARC</title>
<link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/2.3.7/css/dataTables.dataTables.min.css" />
<script type="text/javascript" src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="https://cdn.datatables.net/2.3.7/js/dataTables.min.js"></script>
<style type="text/css">
html, body {
margin: 0;
padding: 0;
font-size: 80%;
font-family: sans-serif;
}
td.dt-control {
cursor: pointer;
}
td.dt-control::before {
content: '\25B6';
color: #888;
}
tr.shown td.dt-control::before {
content: '\25BC';
}
.dmarc-pass, .dmarc-none { background-color: #CCFFCC; }
.dmarc-fail, .dmarc-reject { background-color: #FFCCCC; }
.dmarc-quarantine { background-color: #FFFFCC; }
span.dmarc-cell {
display: block;
padding: 2px 4px;
}
table.child-detail {
width: 100%;
border-collapse: collapse;
margin: 4px 0;
}
table.child-detail th, table.child-detail td {
border: 1px solid #ccc;
padding: 4px 6px;
}
.child-loading { padding: 4px; }
thead input {
width: 100%;
box-sizing: border-box;
padding: 2px 4px;
font-size: 100%;
}
</style>
<script type="text/javascript">
const dmarcCell = (value) => {
if (!value) return '';
return `<span class="dmarc-cell dmarc-${value.toLowerCase()}">${value}</span>`;
};
const escAttr = (s) => String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
const formatChildRows = (rows) => {
if (!rows || !rows.length) return '<p>No detail rows found.</p>';
const body = rows.map(r => `<tr>
<td>${r.header_from || ''}</td>
<td>${r.source_ip || ''}</td>
<td>${r.count || ''}</td>
<td>${dmarcCell(r.disposition)}</td>
<td>${dmarcCell(r.spf)}</td>
<td>${dmarcCell(r.dkim)}</td>
<td>${r.envelope_to || ''}</td>
<td>${r.envelope_from || ''}</td>
</tr>`).join('');
return `<table class="child-detail"><thead><tr>
<th>Header From</th><th>IP</th><th>#</th><th>Disposition</th>
<th>SPF</th><th>DKIM</th><th>Envelope To</th><th>Envelope From</th>
</tr></thead><tbody>${body}</tbody></table>`;
};
jQuery(document).ready(() => {
let filterDomain = '';
let filterAuthor = '';
const table = jQuery('#grid').DataTable({
processing: true,
serverSide: true,
rowId: 'rid',
ajax: (data, callback) => {
const orderCol = data.order?.[0];
const sortCol = orderCol ? (data.columns[orderCol.column].name || 'r.id') : 'r.id';
const sortDir = orderCol?.dir || 'desc';
const params = {
start: data.start,
length: data.length,
sort_col: sortCol,
sort_dir: sortDir,
};
if (filterDomain) params.search_domain = filterDomain;
if (filterAuthor) params.search_author = filterAuthor;
fetch('/dmarc/json/report?' + new URLSearchParams(params))
.then(r => r.json())
.then(json => callback({
draw: data.draw,
recordsTotal: json.recordsTotal,
recordsFiltered: json.recordsFiltered || json.recordsTotal,
data: json.data || [],
}))
.catch(() => callback({
draw: data.draw,
recordsTotal: 0,
recordsFiltered: 0,
data: [],
}));
},
columns: [
{ data: 'rid', name: null, title: '', className: 'dt-control', orderable: false,
render: () => '' },
{ data: 'rid', name: 'r.id', title: 'Id',
render: (data, type, row) => type === 'display' ? `<span title="${escAttr(row.uuid)}">${data}</span>` : data },
{ data: 'from_domain', name: 'fd.domain', title: 'Sender/From' },
{ data: 'author', name: 'a.org_name', title: 'Org Name' },
{ data: 'begin', name: 'r.begin', title: 'Begin' },
{ data: 'end', name: 'r.end', title: 'End' },
],
order: [[1, 'desc']],
pageLength: 100,
lengthMenu: [50, 100, 500],
searching: false,
initComplete: function() {
const api = this.api();
const searchTr = jQuery('<tr>');
api.columns().every(function(i) {
const th = jQuery('<th>');
if (i === 2 || i === 3) {
const placeholder = i === 2 ? 'Filter sender\u2026' : 'Filter org\u2026';
let timer;
jQuery('<input type="text" />')
.attr('placeholder', placeholder)
.on('input', function() {
clearTimeout(timer);
const val = this.value;
timer = setTimeout(() => {
if (i === 2) filterDomain = val;
else filterAuthor = val;
api.draw();
}, 300);
})
.appendTo(th);
}
searchTr.append(th);
});
jQuery('#grid thead').append(searchTr);
},
});
jQuery('#grid tbody').on('click', 'td.dt-control', function() {
const tr = jQuery(this).closest('tr');
const rid = tr[0].id;
if (!rid) return;
const row = table.row(tr);
if (row.child.isShown()) {
row.child.hide();
tr.removeClass('shown');
return;
}
row.child('<p class="child-loading">Loading\u2026</p>').show();
tr.addClass('shown');
fetch(`/dmarc/json/row?rid=${rid}`)
.then(r => {
if (!r.ok) throw new Error(`${r.status} ${r.statusText}`);
return r.json();
})
.then(json => {
table.row(tr[0]).child(formatChildRows(json.data)).show();
})
.catch(() => {
table.row(tr[0]).child.hide();
tr.removeClass('shown');
});
});
});
</script>
</head>
<body>
<table id="grid" class="display" style="width:100%"><caption>DMARC Reports</caption></table>
<p>by <a href="https://metacpan.org/dist/Mail-DMARC">Mail::DMARC</a>.</p>
</body>
</html>