DMARC-Berichte liefern wichtige Informationen über die Authentizität und Sicherheit von E-Mails. Das Problem: Diese Reports werden standardmäßig als komplexe XML-Dateien bereitgestellt, die für Menschen kaum lesbar sind. Genau hier setzt unser Tool an: Der DMARC XML to Human Converter übersetzt rohe DMARC-Feedback-Dateien in eine klar strukturierte, leicht verständliche Darstellung.
Funktionen:
- Drag & Drop Upload: Einfach DMARC XML-Dateien, ZIP-Archive oder GZIP-Dateien hochladen.
- Automatische Entpackung & Parsing: Das Tool erkennt das Dateiformat, entpackt es bei Bedarf und liest die enthaltenen XML-Daten.
- Menschenlesbare Darstellung: Wichtige Informationen wie Absender-Domain, Richtlinien, Auswertungen und Authentifizierungsergebnisse (SPF, DKIM) werden übersichtlich angezeigt.
- Übersicht der Quellen: Zeigt IP-Adressen, Absenderdomains und deren Bewertung nach den DMARC-Regeln.
- Exportfunktionen: Möglichkeit, die Ergebnisse als JSON herunterzuladen oder zu kopieren.
- Lokale Verarbeitung: Alle Daten werden ausschließlich im Browser verarbeitet – es erfolgt keine Weitergabe an Dritte.
- Optionaler Server-Upload: Für Unternehmen besteht die Möglichkeit, Reports per AJAX automatisch an einen definierten Server zu übertragen.
Vorteile:
- Kein mühsames Durchsuchen von XML-Dateien mehr.
- Schnelles Erkennen von Problemen bei SPF- oder DKIM-Prüfungen.
- Ideal für Administratoren, Sicherheitsverantwortliche und Unternehmen, die DMARC-Monitoring einsetzen.
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Dreamcodes - DMARC XML to Human Converter</title>
<!-- Externe Bibliotheken per CDN -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js" integrity="sha512-+sX8QfZyZQ7Q7r2b/3t1p3kJ6s6pJ3h8h2eQqGv5k7b8XbFf0oZkY9y0L6n5f2qHqv8h8+Yq5sHkq8Y1p6mMw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pako/2.1.0/pako.min.js" integrity="sha512-4lFj5qON7bXn14G1+Zl0E0wP9Q+o5j2b6g2Q3qZ2p7sY8c3uK6q9r1e7o4p8u9v1w2Y3z5m7n8p9q2z1o7k3g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<style>
:root{
--bg:#f6f7fb;
--card:#ffffff;
--accent:#1766a6;
--muted:#6b7280;
--success:#0f9d58;
}
body{
margin:0;
font-family:Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
background:var(--bg);
color:#0f172a;
-webkit-font-smoothing:antialiased;
}
header{
background:linear-gradient(90deg,#0e5fa4, #0b72b9);
color:white;
padding:28px 20px;
display:flex;
align-items:center;
gap:16px;
}
header h1{ margin:0; font-size:20px; font-weight:600; }
header p{ margin:0; opacity:0.9; font-size:13px; }
.container{ max-width:1100px; margin:28px auto; padding:0 20px; }
.drop{
background:linear-gradient(180deg, rgba(255,255,255,0.8), rgba(255,255,255,0.6));
border:2px dashed rgba(23,102,166,0.15);
border-radius:10px;
padding:28px;
display:flex;
gap:18px;
align-items:center;
justify-content:space-between;
box-shadow: 0 6px 18px rgba(16,24,40,0.04);
}
.drop-left{ display:flex; gap:18px; align-items:center; }
.drop .info{ max-width:720px; }
.drop h2{ margin:0; font-size:18px; color:var(--accent); }
.drop p{ margin:6px 0 0; color:var(--muted); font-size:13px; line-height:1.4; }
.actions{ display:flex; gap:10px; align-items:center; }
.btn{
background:var(--accent);
color:white;
border:none;
padding:10px 14px;
border-radius:8px;
cursor:pointer;
font-weight:600;
}
.btn.secondary{
background:#fff;
color:var(--accent);
border:1px solid rgba(23,102,166,0.12);
}
input[type=file]{ display:none; }
.list{ margin-top:18px; display:grid; gap:12px; }
.card{
background:var(--card);
border-radius:10px;
padding:16px;
box-shadow: 0 6px 18px rgba(16,24,40,0.04);
}
.meta{ display:flex; gap:12px; flex-wrap:wrap; font-size:13px; color:var(--muted); }
.key{ color:var(--accent); font-weight:700; margin-right:6px; }
.section-title{ font-size:15px; margin:12px 0 8px; color:#0f172a; }
table{ width:100%; border-collapse:collapse; font-size:13px; }
th, td{ text-align:left; padding:8px; border-bottom:1px solid #f1f5f9; }
th{ color:var(--muted); font-weight:600; font-size:12px; }
pre{ background:#0b1720; color:#e6eef6; padding:12px; border-radius:8px; overflow:auto; font-size:13px; }
.small{ font-size:12px; color:var(--muted); }
.controls{ display:flex; gap:10px; margin-top:12px; flex-wrap:wrap; }
.badge{ background:#eef6fb; color:var(--accent); padding:6px 8px; border-radius:999px; font-weight:700; font-size:12px; }
.footer-note{ font-size:13px; color:var(--muted); margin-top:12px; }
.row{ display:flex; gap:12px; align-items:center; flex-wrap:wrap; }
.export-btn{ background:var(--success); border:none; color:white; padding:8px 12px; border-radius:8px; cursor:pointer; }
.danger{ background:#ef4444; color:white; padding:8px 12px; border-radius:8px; border:none; cursor:pointer; }
.toggle{ display:inline-flex; gap:6px; align-items:center; background:#fff; padding:6px 8px; border-radius:8px; border:1px solid #e6eef6; }
.empty{ color:var(--muted); padding:24px; text-align:center; border-radius:10px; background:linear-gradient(180deg, #fff, #fbfdff); border:1px dashed #edf2f7; }
</style>
</head>
<body>
<header>
<div style="display:flex;flex-direction:column;">
<h1>Dreamcodes - DMARC XML to Human Converter</h1>
<p>DMARC Reports lesbar machen, Fehler verstehen und Maßnahmen ableiten</p>
</div>
</header>
<div class="container">
<div class="drop" id="dropzone">
<div class="drop-left">
<div style="width:64px;height:64px;border-radius:12px;background:#eaf6ff;display:flex;align-items:center;justify-content:center">
<svg width="34" height="34" viewBox="0 0 24 24" fill="none" aria-hidden><path d="M12 2v13" stroke="#0369a1" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M5 10l7-7 7 7" stroke="#0369a1" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
<div class="info">
<h2>Dreamcodes - Drag und Drop DMARC XML Dateien hierher</h2>
<p>Unterstützte Formate, unkomprimiertes XML, gzip oder zip. Du kannst beliebig viele Dateien hochladen. Die Dateien werden lokal im Browser verarbeitet.</p>
</div>
</div>
<div class="actions">
<label for="fileInput" class="btn" title="Dateien wählen">Dateien wählen</label>
<input id="fileInput" type="file" multiple />
<button id="clearAll" class="btn secondary">Alles entfernen</button>
<div style="font-size:12px;color:#fff;background:rgba(255,255,255,0.06);padding:6px 8px;border-radius:8px" class="small">Lokal im Browser</div>
</div>
</div>
<div id="output" class="list" style="margin-top:16px;">
<div class="card empty" id="emptyState">
Ziehe DMARC XML Dateien hierher oder wähle Dateien aus, um sie lesbar aufzubereiten
</div>
</div>
<div style="margin-top:18px;display:flex;align-items:center;gap:12px;flex-wrap:wrap;">
<div class="toggle" title="Optionen">
<input type="checkbox" id="showRaw" /> <label for="showRaw" style="font-size:13px;color:#0f172a">Rohdaten anzeigen</label>
</div>
<button id="exportAll" class="export-btn">Alle als JSON exportieren</button>
<button id="uploadServer" class="btn secondary">Optionalen Upload per AJAX</button>
</div>
<p class="footer-note">
Hinweis: Die Verarbeitung geschieht ausschließlich im Browser.
Die Dateien werden nicht ohne dein Zutun an einen Server gesendet.
Mehr Infos findest du bei
<a href="http://www.dreamcodes.net" target="_blank" rel="noopener noreferrer">Dreamcodes</a>.
</p>
</div>
<script>
const $ = id => document.getElementById(id);
const fileInput = $('fileInput');
const dropzone = $('dropzone');
const output = $('output');
const empty = $('emptyState');
const clearAllBtn = $('clearAll');
const exportAllBtn = $('exportAll');
const uploadServerBtn = $('uploadServer');
const showRaw = $('showRaw');
let processedReports = []; // Array mit Objekten { filename, xmlString, parsed }
function setEmptyState(show){
empty.style.display = show ? 'block' : 'none';
}
function createCard(fileName, parsed, rawXml){
const card = document.createElement('div');
card.className='card';
const header = document.createElement('div');
header.className='row';
const title = document.createElement('div');
title.innerHTML = `<div style="font-weight:700">${escapeHtml(fileName)}</div><div class="small">DMARC Report Übersicht</div>`;
header.appendChild(title);
const controls = document.createElement('div');
controls.style.marginLeft='auto';
controls.className='row';
const exportBtn = document.createElement('button');
exportBtn.className='export-btn';
exportBtn.textContent='JSON herunterladen';
exportBtn.onclick = () => downloadJSON(parsed, fileName + '.json');
const copyBtn = document.createElement('button');
copyBtn.className='btn secondary';
copyBtn.textContent='In Zwischenablage kopieren';
copyBtn.onclick = async () => {
await navigator.clipboard.writeText(JSON.stringify(parsed, null, 2));
alert('JSON in die Zwischenablage kopiert');
};
const toggleRawBtn = document.createElement('button');
toggleRawBtn.className='btn secondary';
toggleRawBtn.textContent='Roh anzeigen';
toggleRawBtn.onclick = () => {
const pre = card.querySelector('pre');
pre.style.display = pre.style.display === 'none' ? 'block' : 'none';
};
controls.appendChild(exportBtn);
controls.appendChild(copyBtn);
controls.appendChild(toggleRawBtn);
header.appendChild(controls);
card.appendChild(header);
const meta = document.createElement('div');
meta.className='meta';
const m = parsed.report_metadata || {};
meta.innerHTML = `
<div><span class="key">Organisation</span> ${escapeHtml(m.org_name || 'unbekannt')}</div>
<div><span class="key">Report ID</span> ${escapeHtml(m.report_id || 'n/a')}</div>
<div><span class="key">Datum von</span> ${formatEpoch(m.begin)} bis ${formatEpoch(m.end)}</div>
`;
card.appendChild(meta);
if(parsed.policy_published){
const s = document.createElement('div');
s.innerHTML = `<div class="section-title">Richtlinie</div>
<div class="small">Domain: <strong>${escapeHtml(parsed.policy_published.domain || '')}</strong>, Policy: <strong>${escapeHtml(parsed.policy_published.p || '')}</strong>, Subdomain Policy: <strong>${escapeHtml(parsed.policy_published.sp||'n/a')}</strong>, Tengrade: <strong>${escapeHtml(parsed.policy_published.pct||'100')}</strong></div>`;
card.appendChild(s);
}
if(parsed.records && parsed.records.length){
const tableTitle = document.createElement('div');
tableTitle.className='section-title';
tableTitle.textContent = `Beobachtete Quellen ${parsed.records.length} Einträge`;
card.appendChild(tableTitle);
const table = document.createElement('table');
const thead = document.createElement('thead');
thead.innerHTML = '<tr><th>IP</th><th>Count</th><th>Resultat</th><th>Header From</th><th>SPF</th><th>DKIM</th></tr>';
table.appendChild(thead);
const tbody = document.createElement('tbody');
parsed.records.forEach(r => {
const tr = document.createElement('tr');
const ip = r.row && r.row.source_ip ? r.row.source_ip : '';
const count = r.row && r.row.count ? r.row.count : '';
const res = r.row && r.row.policy_evaluated ? `${r.row.policy_evaluated.disposition || ''} / dkim:${r.row.policy_evaluated.dkim || ''} / spf:${r.row.policy_evaluated.spf || ''}` : '';
const headerFrom = r.identifiers && r.identifiers.header_from ? r.identifiers.header_from : '';
const spf = r.auth_results && r.auth_results.spf ? summarizeAuth(r.auth_results.spf) : '';
const dkim = r.auth_results && r.auth_results.dkim ? summarizeAuth(r.auth_results.dkim) : '';
tr.innerHTML = `<td>${escapeHtml(ip)}</td><td>${escapeHtml(String(count))}</td><td>${escapeHtml(res)}</td><td>${escapeHtml(headerFrom)}</td><td>${escapeHtml(spf)}</td><td>${escapeHtml(dkim)}</td>`;
tbody.appendChild(tr);
});
table.appendChild(tbody);
card.appendChild(table);
} else {
const noRec = document.createElement('div');
noRec.className='small';
noRec.textContent = 'Keine Einträge im Report gefunden';
card.appendChild(noRec);
}
const pre = document.createElement('pre');
pre.style.display = showRaw && showRaw.checked ? 'block' : 'none';
pre.textContent = rawXml || '';
card.appendChild(pre);
return card;
}
function escapeHtml(input){
if(!input && input !== 0) return '';
return String(input).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('"','"');
}
function formatEpoch(epoch){
if(!epoch) return 'n/a';
try {
const ms = Number(epoch) * 1000;
if(isNaN(ms)) return 'n/a';
const d = new Date(ms);
return d.toISOString().replace('T',' ').replace('.000Z',' UTC');
} catch(e){ return 'n/a'; }
}
function summarizeAuth(arr){
if(!arr) return '';
if(Array.isArray(arr)){
return arr.map(a => `${a.domain||''}:${a.result||''}`).join(', ');
} else if(typeof arr === 'object'){
return `${arr.domain||''}:${arr.result||''}`;
}
return String(arr);
}
function downloadJSON(obj, filename){
const blob = new Blob([JSON.stringify(obj, null, 2)], {type:'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href=url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
function parseDmarcXml(xmlString){
const parser = new DOMParser();
const doc = parser.parseFromString(xmlString, 'application/xml');
const parseError = doc.querySelector('parsererror');
if(parseError) {
// Falls kein gültiges XML
throw new Error('Ungültiges XML');
}
const getText = (parent, tag) => {
const el = parent.querySelector(tag);
return el ? el.textContent.trim() : null;
};
const root = doc.documentElement;
// Report metadata
const report_metadata = {};
const rm = root.querySelector('report_metadata');
if(rm){
report_metadata.org_name = getText(rm, 'org_name');
report_metadata.report_id = getText(rm, 'report_id');
const dateRange = rm.querySelector('date_range');
if(dateRange){
report_metadata.begin = getText(dateRange, 'begin');
report_metadata.end = getText(dateRange, 'end');
}
}
// Policy published
const policy_published = {};
const pp = root.querySelector('policy_published');
if(pp){
policy_published.domain = getText(pp, 'domain');
policy_published.adkim = getText(pp, 'adkim');
policy_published.aspf = getText(pp, 'aspf');
policy_published.p = getText(pp, 'p');
policy_published.sp = getText(pp, 'sp');
policy_published.pct = getText(pp, 'pct');
}
// Records
const records = [];
const recEls = root.querySelectorAll('record');
recEls.forEach(rEl => {
const row = {};
const rowEl = rEl.querySelector('row');
if(rowEl){
row.source_ip = getText(rowEl, 'source_ip');
row.count = getText(rowEl, 'count');
const pol = rowEl.querySelector('policy_evaluated');
if(pol){
row.policy_evaluated = {
disposition: getText(pol, 'disposition'),
dkim: getText(pol, 'dkim'),
spf: getText(pol, 'spf')
};
}
}
const identifiers = {};
const idEl = rEl.querySelector('identifiers');
if(idEl){
identifiers.header_from = getText(idEl, 'header_from');
identifiers.envelope_to = getText(idEl, 'envelope_to');
identifiers.envelope_from = getText(idEl, 'envelope_from');
}
const auth_results = {};
const arEl = rEl.querySelector('auth_results');
if(arEl){
// dkim may be multiple
const dkimEls = arEl.querySelectorAll('dkim');
if(dkimEls.length){
auth_results.dkim = Array.from(dkimEls).map(dk=>({
domain: getText(dk, 'domain'),
selector: getText(dk, 'selector'),
result: getText(dk, 'result')
}));
}
const spfEls = arEl.querySelectorAll('spf');
if(spfEls.length){
auth_results.spf = Array.from(spfEls).map(spf=>({
domain: getText(spf, 'domain'),
scope: getText(spf, 'scope'),
result: getText(spf, 'result')
}));
}
}
records.push({row, identifiers, auth_results});
});
return {
report_metadata,
policy_published,
records
};
}
async function handleFile(file){
const name = file.name || 'unknown';
const arrBuf = await file.arrayBuffer();
const bytes = new Uint8Array(arrBuf);
const isGzip = bytes[0] === 0x1f && bytes[1] === 0x8b;
const isZip = bytes[0] === 0x50 && bytes[1] === 0x4b && bytes[2] === 0x03 && bytes[3] === 0x04;
if(isGzip){
try {
const decompressed = pako.ungzip(bytes, { to: 'string' });
processXmlString(name.replace(/\.gz$/i, ''), decompressed);
} catch(e){
console.error('GZIP Fehler', e);
alert('Fehler beim Entpacken von gzip Datei ' + name);
}
} else if(isZip){
try {
const jszip = await JSZip.loadAsync(bytes);
let found = 0;
await Promise.all(
Object.keys(jszip.files).map(async key => {
const f = jszip.files[key];
if(!f.dir && /(\.xml)$/i.test(f.name)){
found++;
const content = await f.async('string');
processXmlString(f.name, content);
} else if(!f.dir && /\.(xml|xml\.txt)$/i.test(f.name)){
found++;
const content = await f.async('string');
processXmlString(f.name, content);
} else if(!f.dir && /\.(gz)$/i.test(f.name)){
// handle nested gz in zip
const data = await f.async('uint8array');
try {
const dec = pako.ungzip(data, { to: 'string' });
processXmlString(f.name.replace(/\.gz$/i,''), dec);
found++;
} catch(e){}
}
})
);
if(found === 0){
alert('Kein XML in der ZIP Datei gefunden: ' + name);
}
} catch(e){
console.error('ZIP Fehler', e);
alert('Fehler beim Auslesen der ZIP Datei ' + name);
}
} else {
// Assume plain XML text
const text = new TextDecoder().decode(bytes);
// If file extension .xml or content starts with <?xml or <feedback
const trimmed = text.trim();
if(trimmed.startsWith('<') && trimmed.length > 20){
processXmlString(name, text);
} else {
alert('Unbekanntes Format: ' + name);
}
}
}
function processXmlString(filename, xmlString){
try {
const parsed = parseDmarcXml(xmlString);
processedReports.push({ filename, xmlString, parsed });
renderAll();
} catch(e){
console.error('Parse Fehler', e);
alert('Fehler beim Parsen der Datei ' + filename + '. Datei ist möglicherweise kein gültiger DMARC XML Bericht.');
}
}
function renderAll(){
// clear
output.innerHTML = '';
if(processedReports.length === 0){
setEmptyState(true);
output.appendChild(empty);
return;
}
setEmptyState(false);
processedReports.forEach(r => {
const card = createCard(r.filename, r.parsed, r.xmlString);
output.appendChild(card);
});
}
// Drag and drop events
['dragenter','dragover'].forEach(evt => {
dropzone.addEventListener(evt, e => {
e.preventDefault();
dropzone.style.borderColor = 'rgba(23,102,166,0.4)';
});
});
['dragleave','drop'].forEach(evt => {
dropzone.addEventListener(evt, e => {
e.preventDefault();
dropzone.style.borderColor = 'rgba(23,102,166,0.15)';
});
});
dropzone.addEventListener('drop', async e => {
const dt = e.dataTransfer;
const files = Array.from(dt.files || []);
if(files.length === 0) return;
for(const f of files) await handleFile(f);
});
// File input
fileInput.addEventListener('change', async e => {
const files = Array.from(e.target.files || []);
for(const f of files) await handleFile(f);
fileInput.value = '';
});
// Clear all
clearAllBtn.addEventListener('click', () => {
if(!confirm('Alle verarbeiteten Reports entfernen?')) return;
processedReports = [];
renderAll();
});
// Export all JSON
exportAllBtn.addEventListener('click', () => {
if(processedReports.length === 0){ alert('Keine Reports zum Exportieren'); return; }
const payload = processedReports.map(p => ({ filename: p.filename, parsed: p.parsed }));
downloadJSON(payload, 'dmarc_reports.json');
});
// Optionaler AJAX Upload
uploadServerBtn.addEventListener('click', async () => {
if(processedReports.length === 0){ alert('Keine Reports vorhanden'); return; }
const url = prompt('Ziel URL für Upload eingeben, z. B. https://example.com/api/dmarc', '');
if(!url) return;
try {
// Beispiel: POST JSON
const payload = processedReports.map(p => ({ filename: p.filename, parsed: p.parsed }));
const resp = await fetch(url, {
method:'POST',
headers:{ 'Content-Type':'application/json' },
body: JSON.stringify({ reports: payload })
});
if(!resp.ok) throw new Error('Upload fehlgeschlagen ' + resp.status);
alert('Upload erfolgreich');
} catch(err){
console.error(err);
alert('Fehler beim Upload: ' + err.message);
}
});
// init
setEmptyState(true);
renderAll();
</script>
</body>
</html>