Download index.html from getreadystack/spf-dmarc-record-lint: direct link, hf CLI and curl.
- Browser
- Download file 49.9 kB
-
https://huggingface.co/spaces/getreadystack/spf-dmarc-record-lint/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/spf-dmarc-record-lint/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/spf-dmarc-record-lint/resolve/main/index.html
49.9 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |
| <title>Dkim Record check — check your DNS records free</title> | |
| <meta name="description" content="Paste a zone file, Terraform block or YAML. 19 offline checks on every v=spf1, v=DMARC1 and v=DKIM1 string, including the May 2026 DMARCbis changes (pct removed, np=, t=). Runs in your browser; nothing is uploaded."> | |
| <style> | |
| :root{--bg:#0f1115;--panel:#171a21;--line:#262b36;--ink:#e8ecf4;--dim:#9aa4b8;--err:#ff6b6b;--warn:#ffc65c;--ok:#4ade80;--acc:#6ea8fe} | |
| *{box-sizing:border-box} | |
| body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.55 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif} | |
| .wrap{max-width:1180px;margin:0 auto;padding:28px 20px 72px} | |
| h1{font-size:26px;margin:0 0 6px;letter-spacing:-.01em} | |
| .lede{color:var(--dim);margin:0 0 22px;max-width:74ch} | |
| .lede b{color:var(--ink);font-weight:600} | |
| .grid{display:grid;grid-template-columns:1.15fr .85fr;gap:18px;align-items:start} | |
| @media(max-width:880px){.grid{grid-template-columns:1fr}} | |
| .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px} | |
| label{display:block;font-size:13px;color:var(--dim);margin:0 0 8px} | |
| textarea{width:100%;min-height:270px;background:#0c0e13;color:#dfe6f2;border:1px solid var(--line);border-radius:8px;padding:12px;font:13px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;resize:vertical} | |
| .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:12px} | |
| button{background:var(--acc);color:#0b1020;border:0;border-radius:8px;padding:10px 16px;font-weight:650;font-size:14px;cursor:pointer} | |
| button.ghost{background:transparent;color:var(--dim);border:1px solid var(--line)} | |
| .summary{display:flex;gap:14px;flex-wrap:wrap;align-items:baseline;border-bottom:1px solid var(--line);padding-bottom:10px;margin-bottom:12px} | |
| .big{font-size:30px;font-weight:700;letter-spacing:-.02em} | |
| .tag{font-size:12px;padding:2px 8px;border-radius:999px;border:1px solid var(--line);color:var(--dim)} | |
| ul.findings{list-style:none;margin:0;padding:0;max-height:430px;overflow:auto} | |
| ul.findings li{border-left:3px solid var(--line);padding:9px 0 9px 12px;margin-bottom:10px} | |
| li.error{border-left-color:var(--err)} li.warning{border-left-color:var(--warn)} | |
| .meta{font:12px/1.4 ui-monospace,Menlo,Consolas,monospace;color:var(--dim);margin-bottom:3px} | |
| .meta .sev.error{color:var(--err)} .meta .sev.warning{color:var(--warn)} | |
| .msg{font-size:14px} | |
| .clean{color:var(--ok);font-size:15px} | |
| .buy{margin-top:16px;border:1px solid #33405e;background:linear-gradient(180deg,#1b2231,#161b26);border-radius:12px;padding:16px} | |
| .buy h3{margin:0 0 6px;font-size:15px} | |
| .buy p{margin:0 0 10px;color:var(--dim);font-size:13.5px} | |
| .buy a{display:inline-block;background:#ffd166;color:#22180a;text-decoration:none;font-weight:700;border-radius:8px;padding:10px 15px;font-size:14px} | |
| .buy small{display:block;margin-top:8px;color:var(--dim);font-size:12px} | |
| .foot{color:var(--dim);font-size:13px;margin-top:26px;border-top:1px solid var(--line);padding-top:14px} | |
| .foot a{color:var(--acc)} | |
| code{background:#0c0e13;border:1px solid var(--line);border-radius:4px;padding:1px 5px;font-size:12.5px} | |
| </style> | |
| <script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script> | |
| <!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/spf-dmarc-record-lint"></head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>Dkim Record check — check your DNS records free</h1> | |
| <p class="lede">Paste the records as you wrote them — BIND zone file, Terraform <code>cloudflare_record</code>, Kubernetes YAML, a <code>.env</code> line. <b>19 checks</b> run on every <code>v=spf1</code>, <code>v=DMARC1</code> and <code>v=DKIM1</code> string, including the <b>May 2026 DMARCbis</b> changes (RFC 9989 / RFC 9990 obsoleted RFC 7489: <code>pct=</code> removed, <code>np=</code> and <code>t=</code> added). No DNS query, no upload, no account — the domain does not have to resolve yet.</p> | |
| <div class="grid"> | |
| <div class="card"> | |
| <label for="src">Records — one per line (comments starting with ; or # are skipped)</label> | |
| <textarea id="src" spellcheck="false">; example.com — the state a mid-size domain is usually in after four years of adding senders | |
| $TTL 3600 | |
| example.com. 3600 IN TXT "v=spf1 include:_spf.google.com include:sendgrid.net include:mailgun.org include:servers.mcsv.net include:spf.protection.outlook.com include:_spf.salesforce.com include:helpscout.net include:_spf.atlassian.net a mx ptr ~all" | |
| example.com. 3600 IN TXT "v=spf1 include:mail.zendesk.com +all" | |
| _dmarc.example.com. 3600 IN TXT "v=DMARC1; p=reject; pct=10; t=y; sp=none; np=none; rua=mailto:reports@dmarc-vendor.example" | |
| sel1._domainkey.example.com. 3600 IN TXT "v=DKIM1; k=rsa; t=y; p=" | |
| </textarea> | |
| <div class="row"> | |
| <button id="run">Check records</button> | |
| <button class="ghost" id="clear">Clear</button> | |
| <span class="tag" id="rulecount"></span> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="card" id="result" aria-live="polite"> | |
| <div class="summary"><span class="big">—</span><span class="tag">press Check records</span></div> | |
| </div> | |
| <div class="buy"> | |
| <h3>Every file in the repo, in one dated report</h3> | |
| <p>This page checks what you paste. The licensed extension sweeps the whole workspace — every <code>*.zone</code>, <code>*.tf</code>, <code>*.yaml</code> — in one pass and writes a dated Markdown audit report you keep, attach to the change ticket and file as the evidence that the policy was enforced on that date.</p> | |
| <a href="https://getreadystack.com/api/buy/cl/polar_cl_lTz1ceauyoB7vrNgatSTcYZd8Oyia1VfFLlxg3fcI1A">Get the full version — $29</a> | |
| <small>$29 once · one licence key per person or CI seat · A freelance email-deliverability specialist bills $50–$150 an hour, and the first hour goes on reading the records you already have.</small> | |
| </div> | |
| </div> | |
| </div> | |
| <p class="foot">Same engine as the VS Code extension, byte for byte — the file below is <code>ext/engine.js</code> and <code>ext/rules.json</code> from the published extension. Everything runs in your browser; nothing is sent anywhere. Sources for every finding: RFC 7208, RFC 1035, RFC 6376, RFC 8301, and RFC 9989 / RFC 9990 (IETF, May 2026). Extension: <a href="https://getreadystack.com/tools/spf-dmarc-record-lint">getreadystack.com/tools/spf-dmarc-record-lint</a></p> | |
| </div> | |
| <script>window.SPF_RULES = [ | |
| {"id": "spf_lookup_limit", "sev": "error", "msg": "SPF record needs {n} DNS lookups — the limit is 10. Receivers return PermError and treat the domain as having no SPF at all. Flatten or remove includes. [RFC 7208 4.6.4, IETF, April 2014]", "ref": "RFC 7208 4.6.4"}, | |
| {"id": "spf_lookup_near", "sev": "warning", "msg": "SPF record already spends {n} of the 10 permitted DNS lookups in the record itself; every include: resolves to a record that can spend more. Count the nested ones before you publish. [RFC 7208 4.6.4, IETF, April 2014]", "ref": "RFC 7208 4.6.4"}, | |
| {"id": "spf_pass_all", "sev": "error", "msg": "'+all' (or a bare 'all') passes SPF for every sender on the internet, which is the same as publishing no SPF. End the record with '-all', or '~all' while you migrate. [RFC 7208 5.1, IETF, April 2014]", "ref": "RFC 7208 5.1"}, | |
| {"id": "spf_ptr", "sev": "error", "msg": "The 'ptr' mechanism is deprecated and must not be published — receivers may ignore it, and it costs a DNS lookup per candidate host. Replace it with ip4:/ip6: or include:. [RFC 7208 5.5, IETF, April 2014: 'its use is discouraged']", "ref": "RFC 7208 5.5"}, | |
| {"id": "spf_no_all", "sev": "warning", "msg": "SPF record has no 'all' mechanism and no 'redirect=' modifier, so everything it does not list gets Neutral — the same result as no policy. Add '-all' or '~all' at the end. [RFC 7208 4.7, IETF, April 2014]", "ref": "RFC 7208 4.7"}, | |
| {"id": "spf_duplicate", "sev": "error", "msg": "Second 'v=spf1' record published on the same name. More than one SPF record for a domain is a PermError — merge them into one record. [RFC 7208 4.5, IETF, April 2014]", "ref": "RFC 7208 4.5"}, | |
| {"id": "spf_string_255", "sev": "error", "msg": "A single quoted TXT string is {n} characters; the DNS wire format caps one character-string at 255. Split it into several quoted strings on the same record — they are concatenated with no separator. [RFC 1035 3.3.14, IETF, November 1987]", "ref": "RFC 1035 3.3.14"}, | |
| {"id": "dmarc_missing_p", "sev": "warning", "msg": "DMARC record has no 'p=' tag. Under DMARCbis this no longer voids the record — it is now read as 'p=none', so the domain looks protected in an audit and blocks nothing. Publish 'p=' explicitly. [RFC 9989 4.7, IETF, May 2026: 'If this tag is not present in an otherwise syntactically valid DMARC Policy Record, then the record is treated as if it included p=none' — this REPLACES the RFC 7489 behaviour of discarding the record]", "ref": "RFC 9989 4.7"}, | |
| {"id": "dmarc_p_none", "sev": "warning", "msg": "'p=none' only asks for reports; receivers still deliver mail that fails alignment, so nothing is blocked. It satisfies 'a DMARC record exists' and nothing else — move to quarantine, then reject. [RFC 9989 4.7, IETF, May 2026]", "ref": "RFC 9989 4.7"}, | |
| {"id": "dmarc_pct", "sev": "error", "msg": "'pct={n}' was removed from DMARC in May 2026. A receiver following the current spec ignores it, so a record you published as a {n}% staged rollout now applies your full policy to 100% of your mail. Delete 'pct=' and stage with 't=y' instead. [RFC 9989 Appendix A.6 'Removal of the pct tag', IETF, 21 May 2026]", "ref": "RFC 9989 A.6"}, | |
| {"id": "dmarc_t_test", "sev": "warning", "msg": "'t=y' is DMARC test mode: receivers apply the policy one level below the one you published, so 'p=reject' behaves as quarantine and 'p=quarantine' as none. Correct while you stage a rollout, silent failure if it is left on. [RFC 9989 4.7, IETF, May 2026: 'the policy applied to any failing messages will be one level below the specified policy']", "ref": "RFC 9989 4.7"}, | |
| {"id": "dmarc_np_none", "sev": "error", "msg": "'np=none' switches off the policy for subdomains that do not exist — the names a spoofer picks precisely because nobody has claimed them. Set 'np=reject', or drop the tag so the domain policy is inherited. [RFC 9989 4.7, IETF, May 2026: np = policy 'for non-existent subdomains of the Organizational Domain queried']", "ref": "RFC 9989 4.7"}, | |
| {"id": "dmarc_no_rua", "sev": "warning", "msg": "No 'rua=' tag, so no aggregate reports are ever sent: you cannot see which of your senders fail alignment, and you hold no evidence that the policy was enforced on any given date. Add rua=mailto: before you tighten the policy. [RFC 9990 3.4, IETF, May 2026: 'The presence of the rua tag specifies where to send feedback']", "ref": "RFC 9990 3.4"}, | |
| {"id": "dmarc_rua_external", "sev": "warning", "msg": "'rua=' sends aggregate reports to {n}, which is outside this domain. That destination must publish an authorisation record before any receiver will send to it; without it the reports are silently dropped and your evidence trail is empty. [RFC 9990 4, IETF, May 2026: verification requires a TXT record at '_report._dmarc' prepended to the reporting destination, e.g. yourdomain._report._dmarc.<destination>]", "ref": "RFC 9990 4"}, | |
| {"id": "dmarc_owner", "sev": "error", "msg": "A DMARC record is only read at the name '_dmarc.<domain>'. This record is published somewhere else, so no receiver will ever look it up. [RFC 9989 4.5, IETF, May 2026: policy records are published at '_dmarc prepended to domain name']", "ref": "RFC 9989 4.5"}, | |
| {"id": "dmarc_sp_none", "sev": "error", "msg": "'sp=none' overrides the policy for every subdomain, so mail from a subdomain nobody uses is unprotected while the parent domain is enforced. Drop 'sp=' to inherit, or set sp=reject. [RFC 9989 4.7, IETF, May 2026 — note the organizational domain is now found by the DNS Tree Walk (4.10), not the public suffix list]", "ref": "RFC 9989 4.7"}, | |
| {"id": "dkim_p_empty", "sev": "error", "msg": "Empty 'p=' in a DKIM record means the key is revoked — every signature made with this selector fails, and mail signed only by it loses DKIM. Publish the public key or delete the selector. [RFC 6376 3.6.1, IETF, September 2011]", "ref": "RFC 6376 3.6.1"}, | |
| {"id": "dkim_key_short", "sev": "warning", "msg": "The 'p=' key blob is {n} base64 characters, which is about a 1024-bit RSA key or smaller. Signers must use at least 1024 bits and should use 2048 — rotate the selector. [RFC 8301 3.2, IETF, January 2018]", "ref": "RFC 8301 3.2"}, | |
| {"id": "dkim_test_mode", "sev": "warning", "msg": "'t=y' puts the selector in test mode, which tells receivers to treat a failed signature as if the message were unsigned. Remove it once the selector is live. [RFC 6376 3.6.1, IETF, September 2011]", "ref": "RFC 6376 3.6.1"} | |
| ];</script> | |
| <script>// SPF & DMARC Record Lint — one brain. Same file runs in Node (the extension) and in the browser (the free web page). | |
| ; | |
| var RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.SPF_RULES; | |
| function rule(id) { | |
| for (var i = 0; i < RULES.length; i++) if (RULES[i].id === id) return RULES[i]; | |
| return { id: id, sev: 'error', msg: id }; | |
| } | |
| function push(out, id, line, n) { | |
| var r = rule(id); | |
| out.push({ check: r.id, sev: r.sev, msg: String(r.msg).replace('{n}', String(n)), line: line }); | |
| } | |
| function strings(raw) { | |
| var m = raw.match(/"[^"]*"/g); | |
| return m ? m.map(function (s) { return s.slice(1, -1); }) : []; | |
| } | |
| function tags(rec) { | |
| var t = {}; | |
| rec.split(';').forEach(function (part) { | |
| var kv = part.split('='); | |
| if (kv.length < 2) return; | |
| var k = kv[0].trim().toLowerCase(); | |
| if (k) t[k] = kv.slice(1).join('=').trim(); | |
| }); | |
| return t; | |
| } | |
| function owner(raw) { | |
| if (!/^\S/.test(raw)) return null; | |
| if (!/\bTXT\b/i.test(raw)) return null; | |
| var first = raw.split(/\s+/)[0]; | |
| return first ? first.toLowerCase().replace(/\.$/, '') : null; | |
| } | |
| function checkSpf(rec, line, out) { | |
| var terms = rec.split(/\s+/).slice(1); | |
| var lookups = 0, hasAll = false, passAll = false, hasPtr = false, hasRedirect = false; | |
| for (var i = 0; i < terms.length; i++) { | |
| var t = terms[i].replace(/[",;)]+$/, ''); | |
| if (!t) continue; | |
| var q = ''; | |
| if (/^[+\-~?]/.test(t)) { q = t.charAt(0); t = t.slice(1); } | |
| var name = t.split(/[:\/=]/)[0].toLowerCase(); | |
| if (name === 'include' || name === 'a' || name === 'mx' || name === 'ptr' || name === 'exists') lookups++; | |
| if (name === 'redirect' && /^redirect=/i.test(t)) { lookups++; hasRedirect = true; } | |
| if (name === 'ptr') hasPtr = true; | |
| if (name === 'all') { hasAll = true; if (q === '' || q === '+') passAll = true; } | |
| } | |
| if (lookups > 10) push(out, 'spf_lookup_limit', line, lookups); | |
| else if (lookups >= 7) push(out, 'spf_lookup_near', line, lookups); | |
| if (passAll) push(out, 'spf_pass_all', line); | |
| if (hasPtr) push(out, 'spf_ptr', line); | |
| if (!hasAll && !hasRedirect) push(out, 'spf_no_all', line); | |
| return lookups; | |
| } | |
| function checkDmarc(rec, raw, prev, line, out) { | |
| var t = tags(rec); | |
| if (!t.p) push(out, 'dmarc_missing_p', line); | |
| else if (t.p.toLowerCase() === 'none') push(out, 'dmarc_p_none', line); | |
| if (!t.rua) push(out, 'dmarc_no_rua', line); | |
| if (t.sp && t.sp.toLowerCase() === 'none' && t.p && t.p.toLowerCase() !== 'none') push(out, 'dmarc_sp_none', line); | |
| if (t.pct) push(out, 'dmarc_pct', line, t.pct); // RFC 9989 A.6 (2026-05) — pct removed | |
| if (t.t && /^y/i.test(t.t)) push(out, 'dmarc_t_test', line); // RFC 9989 4.7 — test mode | |
| if (t.np && t.np.toLowerCase() === 'none') push(out, 'dmarc_np_none', line); // RFC 9989 4.7 — non-existent subdomains | |
| if (t.rua) { | |
| var own = (owner(raw) || '').replace(/^_dmarc\./, ''); | |
| var hosts = []; | |
| (t.rua.match(/mailto:[^\s,;!]+/gi) || []).forEach(function (u) { | |
| var h = u.split('@')[1]; | |
| if (!h) return; | |
| h = h.toLowerCase().replace(/\.$/, ''); | |
| if (own && (h === own || h.slice(-(own.length + 1)) === '.' + own)) return; // same organizational domain — no authorisation record needed | |
| if (hosts.indexOf(h) < 0) hosts.push(h); | |
| }); | |
| if (own && hosts.length) push(out, 'dmarc_rua_external', line, hosts.join(', ')); // RFC 9990 4 — external destination verification | |
| } | |
| if (!/_dmarc/i.test(raw + '\n' + prev)) push(out, 'dmarc_owner', line); | |
| } | |
| function checkDkim(rec, line, out) { | |
| var t = tags(rec); | |
| var p = (t.p || '').replace(/\s+/g, ''); | |
| if (!p) push(out, 'dkim_p_empty', line); | |
| else if (p.length < 250) push(out, "dkim_key_short", line, p.length); | |
| if (t.t && /(^|:)y(:|$)/i.test(t.t)) push(out, 'dkim_test_mode', line); | |
| } | |
| function check(text, opts) { | |
| opts = opts || {}; | |
| var lines = String(text == null ? '' : text).split(/\r?\n/); | |
| var out = [], seen = {}, records = 0; | |
| for (var i = 0; i < lines.length; i++) { | |
| var raw = lines[i]; | |
| if (/^\s*[;#]/.test(raw)) continue; | |
| var parts = strings(raw); | |
| var joined = parts.length ? parts.join('') : raw; | |
| var isSpf = /v=spf1\b/i.test(joined), isDmarc = /v=DMARC1\b/i.test(joined), isDkim = /v=DKIM1\b/i.test(joined); | |
| if (!isSpf && !isDmarc && !isDkim) continue; | |
| records++; | |
| var line = i + 1; | |
| for (var s = 0; s < parts.length; s++) if (parts[s].length > 255) push(out, 'spf_string_255', line, parts[s].length); | |
| if (isSpf) { | |
| var own = owner(raw); | |
| if (own) { if (seen[own]) push(out, 'spf_duplicate', line); seen[own] = true; } | |
| checkSpf(joined.slice(joined.search(/v=spf1\b/i)).trim(), line, out); | |
| } | |
| if (isDmarc) { | |
| var prev = lines.slice(Math.max(0, i - 4), i).join('\n'); | |
| checkDmarc(joined.slice(joined.search(/v=DMARC1\b/i)).trim(), raw, prev, line, out); | |
| } | |
| if (isDkim) checkDkim(joined.slice(joined.search(/v=DKIM1\b/i)).trim(), line, out); | |
| } | |
| out.sort(function (a, b) { return (a.line || 0) - (b.line || 0); }); | |
| return { findings: out, records: records, rules: RULES.length, today: opts.today || '' }; | |
| } | |
| var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length }; | |
| if (typeof module !== 'undefined') module.exports = API; | |
| if (typeof window !== 'undefined') window.SPFENGINE = API; | |
| </script> | |
| <script> | |
| (function () { | |
| var E = window.SPFENGINE, $ = function (id) { return document.getElementById(id); }; | |
| $('rulecount').textContent = E.RULE_COUNT + ' checks · offline'; | |
| function esc(s) { return String(s).replace(/[&<>]/g, function (c) { return { '&': '&', '<': '<', '>': '>' }[c]; }); } | |
| function run() { | |
| var res = E.engine.check($('src').value, { today: new Date().toISOString().slice(0, 10) }); | |
| var f = res.findings, errs = 0; | |
| for (var i = 0; i < f.length; i++) if (String(f[i].sev).indexOf('err') === 0) errs++; | |
| var warns = f.length - errs; | |
| var head = '<div class="summary"><span class="big">' + f.length + '</span>' | |
| + '<span class="tag">' + errs + ' error' + (errs === 1 ? '' : 's') + ' · ' + warns + ' warning' + (warns === 1 ? '' : 's') + '</span>' | |
| + '<span class="tag">' + res.records + ' record' + (res.records === 1 ? '' : 's') + ' read</span>' | |
| + '<span class="tag">' + res.rules + ' rules</span></div>'; | |
| var body = f.length | |
| ? '<ul class="findings">' + f.map(function (x) { | |
| var sev = String(x.sev).indexOf('err') === 0 ? 'error' : 'warning'; | |
| return '<li class="' + sev + '"><div class="meta">line ' + x.line + ' · <span class="sev ' + sev + '">' + sev + '</span> · ' + esc(x.check) + '</div><div class="msg">' + esc(x.msg) + '</div></li>'; | |
| }).join('') + '</ul>' | |
| : (res.records ? '<p class="clean">Clean against all ' + res.rules + ' checks.</p>' | |
| : '<p class="clean">No v=spf1, v=DMARC1 or v=DKIM1 string found in that text.</p>'); | |
| $('result').innerHTML = head + body; | |
| } | |
| $('run').addEventListener('click', run); | |
| $('clear').addEventListener('click', function () { $('src').value = ''; $('src').focus(); run(); }); | |
| run(); | |
| })(); | |
| </script> | |
| <section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.spf-dmarc-record-lint" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'spf-dmarc-record-lint',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/spf-dmarc-record-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'spf-dmarc-record-lint',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/spf-dmarc-record-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'spf-dmarc-record-lint',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/spf-dmarc-record-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'spf-dmarc-record-lint',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/spf-dmarc-record-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'spf-dmarc-record-lint',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><a href="https://apify.com/readystack/seo-compliance-audit" rel="noopener" data-yj-install-door="apify" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'spf-dmarc-record-lint',src:'apify',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Apify (audit live URLs)</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.spf-dmarc-record-lint | |
| npx @readystack/spf-dmarc-record-lint <file> | |
| npx @readystack/spf-dmarc-record-lint --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section> | |
| <section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 19 rules SPF & DMARC Record Lint checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"spf-dmarc-record-lint",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section> | |
| <section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">19 offline checks on the SPF, DMARC and DKIM records in your branch — including the DMARC rewrite of May 2026</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A freelance email-deliverability specialist bills $50-$150 an hour, and the first hour goes on reading the records you already have.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_lTz1ceauyoB7vrNgatSTcYZd8Oyia1VfFLlxg3fcI1A" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section> | |
| <!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button></div><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div> | |
| <script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script> | |
| <script data-yj="yj-use-beacon v2">(function(){try{var r=document.querySelector('main')||document.body;var p=location.pathname,k='yju:'+p,sent=false,armed=false;function ours(el){return !!(el&&el.closest&&el.closest('[data-yj],[data-yj-cta],.yj-email-gate,.yj-find,.yj-related,#yj-ask,#yjgo,#yjr'));}function fire(){if(sent||!armed||sessionStorage.getItem(k))return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}r.addEventListener('click',function(e){var b=e.target&&e.target.closest?e.target.closest('button,input[type=submit],[role=button]'):null;if(!b||ours(b))return;armed=true;setTimeout(fire,400);},true);r.addEventListener('input',function(e){if(!ours(e.target))armed=true;},true);var mo=new MutationObserver(function(){if(armed){fire();if(sent)mo.disconnect();}});mo.observe(r,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script> | |
| <section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-17">Updated 2026-09-17</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-17","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku66280_result_card.jpg" alt="Six email-auth faults in one zone file" loading="lazy" style="max-width:100%;border-radius:6px"></p> | |
| <p>Six findings in one four-record zone file — for the platform engineer who keeps DNS in git and adds a sending vendor every quarter. The result first, the explanation afterwards:</p> | |
| <pre><code>_fixtures/dirty.zone — 15 checks — 6 findings (4 errors, 2 warnings) | |
| ERROR L5 spf_lookup_limit SPF record needs 11 DNS lookups — the limit is 10 (RFC 7208 §4.6.4). Receivers return PermError and treat the domain as having no SPF at all. | |
| ERROR L5 spf_ptr The 'ptr' mechanism is deprecated and must not be published (RFC 7208 §5.5). | |
| ERROR L6 spf_pass_all '+all' passes SPF for every sender on the internet — the same as publishing no SPF. | |
| WARN L7 dmarc_p_none 'p=none' only asks for reports; nothing that fails alignment is blocked. | |
| WARN L7 dmarc_no_rua No 'rua=' tag, so no aggregate reports are ever sent. | |
| ERROR L8 dkim_p_empty Empty 'p=' means the key is revoked — every signature from this selector fails.</code></pre> | |
| <p>The zone is plain: one SPF record for the mail host, one for a legacy sender, one DMARC record, one DKIM selector. Every line is valid. BIND loads it, <code>dig</code> prints it back exactly as written, and a language model asked to review it will say it looks fine.</p> | |
| <p><strong>Line 5 is the expensive one.</strong> RFC 7208 §4.6.4 caps SPF evaluation at ten DNS lookups — <code>include</code>, <code>a</code>, <code>mx</code>, <code>ptr</code>, <code>exists</code> and <code>redirect=</code> each cost at least one, and every <code>include:</code> resolves to a record that can spend more. At lookup eleven the receiver stops and returns PermError, and most receivers treat PermError like a domain with no SPF record. If DMARC was passing on SPF alignment alone, DMARC fails with it. Nothing in the editor or the deploy log turns red, and the record stays live until somebody notices invoices landing in spam.</p> | |
| <p>Eight includes, an <code>a:</code>, an <code>mx</code> and a <code>ptr:</code> is eleven. Nobody adds eleven at once: you add the fourth vendor, then the sixth, then the eighth, and the record crosses the line on an ordinary Tuesday. The <code>ptr:</code> should not be there either — §5.5 deprecated it, receivers may ignore it, and it costs a lookup per candidate host.</p> | |
| <p>Line 6 authorises every host on the internet to send as the domain: <code>+all</code> is the protection of publishing nothing with the paperwork of publishing something. Line 7 is monitoring with the monitor unplugged — <code>p=none</code> blocks nothing, and with no <code>rua=</code> no aggregate report is ever sent, so there is no data to move off it with. Line 8 is a DKIM selector whose public key has been revoked to an empty <code>p=</code>: every signature made with it fails.</p> | |
| <p>Fix those six and the same zone returns zero findings.</p> | |
| <h3>Why the free checkers do not catch it</h3> | |
| <p>Because they resolve. Every online SPF tool takes a domain name, queries DNS, and answers about the record already live — which is exactly the record already costing you mail. The one you want checked is in a pull request, on a domain that does not resolve yet, behind a Terraform variable. A chatbot will count your include: chain by guessing.</p> | |
| <p>SPF & DMARC Record Lint reads <code>v=spf1</code>, <code>v=DMARC1</code> and <code>v=DKIM1</code> strings wherever you actually write them — BIND zone files, <code>aws_route53_record</code> and <code>cloudflare_record</code> blocks, Kubernetes and Ansible YAML, <code>.env</code> files, docs — and runs 15 checks on each one: seven for SPF, five for DMARC, three for DKIM. Findings land on the line in the Problems panel with the RFC section and the edit that fixes them. No DNS queries, no network, no account, no telemetry.</p> | |
| <p>The file in front of you is checked in full, by all 15 rules, free and forever, with nothing timed or watermarked. The licence key is a different axis: sweeping every zone, Terraform and YAML file in the workspace in one pass, and keeping one dated Markdown audit report you can attach to the change ticket. $29 once, one key per person or CI seat — against the $50–$150 an hour a freelance deliverability specialist bills, most of the first hour going on reading the records you already have.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku66280_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku66280_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does SPF & DMARC Record Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads the SPF, DMARC and DKIM records where you write them — BIND zone files, Terraform cloudflare_record blocks, Kubernetes YAML, .env files — and runs 19 checks on every v=spf1, v=DMARC1 and v=DKIM1 string. It counts SPF DNS lookups itself, so you see the number before the record is published, with no DNS query and no network call.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Sysadmins and platform engineers who own a zone file, and deliverability consultants who audit other people's. If you have ever added one more include: to an SPF record, or published p=reject with pct=10 and assumed it was a ten percent experiment, this is written for you. It also fits a compliance owner who has to show that enforcement was on.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just use a free online DMARC checker?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Online checkers resolve live DNS, so they can only judge a record after you publish it — the damage is already out. This runs on the text in your branch, before merge, on a domain that need not resolve. Most were also written against RFC 7489 and still call pct= valid, which RFC 9989 removed in May 2026.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The file you have open is checked in full, free, forever: all 19 rules, every finding, nothing watermarked or timed. The licence covers the workspace sweep — every zone, Terraform and YAML file in the folder in one pass — and the dated Markdown audit report it writes out for you to keep and attach to a ticket.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What would this cost me otherwise?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A freelance email-deliverability specialist bills 50 to 150 dollars an hour, and the first hour goes on reading the records you already have. The licence is 29 dollars once, one key per person or CI seat,. One rejected invoice run costs more than either.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. SPF & DMARC Record Lint checks the file you open against 19 rules, each naming its legal or official basis, from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does SPF & DMARC Record Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads the SPF, DMARC and DKIM records where you write them — BIND zone files, Terraform cloudflare_record blocks, Kubernetes YAML, .env files — and runs 19 checks on every v=spf1, v=DMARC1 and v=DKIM1 string. It counts SPF DNS lookups itself, so you see the number before the record is published, with no DNS query and no network call."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Sysadmins and platform engineers who own a zone file, and deliverability consultants who audit other people's. If you have ever added one more include: to an SPF record, or published p=reject with pct=10 and assumed it was a ten percent experiment, this is written for you. It also fits a compliance owner who has to show that enforcement was on."}}, {"@type": "Question", "name": "Why not just use a free online DMARC checker?", "acceptedAnswer": {"@type": "Answer", "text": "Online checkers resolve live DNS, so they can only judge a record after you publish it — the damage is already out. This runs on the text in your branch, before merge, on a domain that need not resolve. Most were also written against RFC 7489 and still call pct= valid, which RFC 9989 removed in May 2026."}}, {"@type": "Question", "name": "What is free and what needs a licence?", "acceptedAnswer": {"@type": "Answer", "text": "The file you have open is checked in full, free, forever: all 19 rules, every finding, nothing watermarked or timed. The licence covers the workspace sweep — every zone, Terraform and YAML file in the folder in one pass — and the dated Markdown audit report it writes out for you to keep and attach to a ticket."}}, {"@type": "Question", "name": "What would this cost me otherwise?", "acceptedAnswer": {"@type": "Answer", "text": "A freelance email-deliverability specialist bills 50 to 150 dollars an hour, and the first hour goes on reading the records you already have. The licence is 29 dollars once, one key per person or CI seat,. One rejected invoice run costs more than either."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. SPF & DMARC Record Lint checks the file you open against 19 rules, each naming its legal or official basis, from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:66280,slug:"spf-dmarc-record-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/billable-hours-tracker-firefox"><span class="yj-rel-t">Billable Tab Timer for Hourly Freelancers & Consultants — Firefox extension th</span><span class="yj-rel-p">$9</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/rust-sepa-iban-pruefung-offline"><span class="yj-rel-t">sepa_pruefkit — offline IBAN, BIC, Gläubiger-ID and USt-IdNr checks for German</span><span class="yj-rel-p">$20</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/autohotkey-toml-devicetree-snippets"><span class="yj-rel-t">AutoHotkey v2, TOML and DeviceTree checks for VS Code</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cloud-cost-landmine-lint"><span class="yj-rel-t">Cloud Cost Landmine Lint for Terraform, CloudFormation and Kubernetes</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/consent-proof-record-lint"><span class="yj-rel-t">Consent Proof Record Lint (GDPR Art.7)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/eu-data-residency-lint"><span class="yj-rel-t">EU Data Residency Lint (Terraform, GDPR Ch. V)</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section></body> | |
| </html> | |