getreadystack commited on
Commit
cbd78ab
·
verified ·
1 Parent(s): 346a918

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +276 -17
index.html CHANGED
@@ -1,19 +1,278 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>SPF &amp; DMARC Record Lint — free online checker (no DNS lookup needed)</title>
7
+ <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.">
8
+ <script defer data-auto-init src="https://cdn.jsdelivr.net/npm/@polar-sh/checkout@latest/dist/embed.global.js"></script>
9
+ <style>
10
+ :root{--bg:#0f1115;--panel:#171a21;--line:#262b36;--ink:#e8ecf4;--dim:#9aa4b8;--err:#ff6b6b;--warn:#ffc65c;--ok:#4ade80;--acc:#6ea8fe}
11
+ *{box-sizing:border-box}
12
+ 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}
13
+ .wrap{max-width:1180px;margin:0 auto;padding:28px 20px 72px}
14
+ h1{font-size:26px;margin:0 0 6px;letter-spacing:-.01em}
15
+ .lede{color:var(--dim);margin:0 0 22px;max-width:74ch}
16
+ .lede b{color:var(--ink);font-weight:600}
17
+ .grid{display:grid;grid-template-columns:1.15fr .85fr;gap:18px;align-items:start}
18
+ @media(max-width:880px){.grid{grid-template-columns:1fr}}
19
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:16px}
20
+ label{display:block;font-size:13px;color:var(--dim);margin:0 0 8px}
21
+ 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}
22
+ .row{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-top:12px}
23
+ button{background:var(--acc);color:#0b1020;border:0;border-radius:8px;padding:10px 16px;font-weight:650;font-size:14px;cursor:pointer}
24
+ button.ghost{background:transparent;color:var(--dim);border:1px solid var(--line)}
25
+ .summary{display:flex;gap:14px;flex-wrap:wrap;align-items:baseline;border-bottom:1px solid var(--line);padding-bottom:10px;margin-bottom:12px}
26
+ .big{font-size:30px;font-weight:700;letter-spacing:-.02em}
27
+ .tag{font-size:12px;padding:2px 8px;border-radius:999px;border:1px solid var(--line);color:var(--dim)}
28
+ ul.findings{list-style:none;margin:0;padding:0;max-height:430px;overflow:auto}
29
+ ul.findings li{border-left:3px solid var(--line);padding:9px 0 9px 12px;margin-bottom:10px}
30
+ li.error{border-left-color:var(--err)} li.warning{border-left-color:var(--warn)}
31
+ .meta{font:12px/1.4 ui-monospace,Menlo,Consolas,monospace;color:var(--dim);margin-bottom:3px}
32
+ .meta .sev.error{color:var(--err)} .meta .sev.warning{color:var(--warn)}
33
+ .msg{font-size:14px}
34
+ .clean{color:var(--ok);font-size:15px}
35
+ .buy{margin-top:16px;border:1px solid #33405e;background:linear-gradient(180deg,#1b2231,#161b26);border-radius:12px;padding:16px}
36
+ .buy h3{margin:0 0 6px;font-size:15px}
37
+ .buy p{margin:0 0 10px;color:var(--dim);font-size:13.5px}
38
+ .buy a{display:inline-block;background:#ffd166;color:#22180a;text-decoration:none;font-weight:700;border-radius:8px;padding:10px 15px;font-size:14px}
39
+ .buy small{display:block;margin-top:8px;color:var(--dim);font-size:12px}
40
+ .foot{color:var(--dim);font-size:13px;margin-top:26px;border-top:1px solid var(--line);padding-top:14px}
41
+ .foot a{color:var(--acc)}
42
+ code{background:#0c0e13;border:1px solid var(--line);border-radius:4px;padding:1px 5px;font-size:12.5px}
43
+ </style>
44
+ <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>
45
+ <!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/spf-dmarc-record-lint"></head>
46
+ <body>
47
+ <div class="wrap">
48
+ <h1>SPF &amp; DMARC Record Lint</h1>
49
+ <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>
50
+
51
+ <div class="grid">
52
+ <div class="card">
53
+ <label for="src">Records — one per line (comments starting with ; or # are skipped)</label>
54
+ <textarea id="src" spellcheck="false">; example.com — the state a mid-size domain is usually in after four years of adding senders
55
+ $TTL 3600
56
+ 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"
57
+ example.com. 3600 IN TXT "v=spf1 include:mail.zendesk.com +all"
58
+ _dmarc.example.com. 3600 IN TXT "v=DMARC1; p=reject; pct=10; t=y; sp=none; np=none; rua=mailto:reports@dmarc-vendor.example"
59
+ sel1._domainkey.example.com. 3600 IN TXT "v=DKIM1; k=rsa; t=y; p="
60
+ </textarea>
61
+ <div class="row">
62
+ <button id="run">Check records</button>
63
+ <button class="ghost" id="clear">Clear</button>
64
+ <span class="tag" id="rulecount"></span>
65
+ </div>
66
+ </div>
67
+
68
+ <div>
69
+ <div class="card" id="result" aria-live="polite">
70
+ <div class="summary"><span class="big">—</span><span class="tag">press Check records</span></div>
71
+ </div>
72
+
73
+ <div class="buy">
74
+ <h3>Every file in the repo, in one dated report</h3>
75
+ <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>
76
+ <a href="https://buy.polar.sh/polar_cl_lTz1ceauyoB7vrNgatSTcYZd8Oyia1VfFLlxg3fcI1A" data-polar-checkout data-polar-checkout-theme="light">Get the full version — $29</a>
77
+ <small>$29 once · one licence key per person or CI seat · 7-day full refund. A freelance email-deliverability specialist bills $50–$150 an hour, and the first hour goes on reading the records you already have.</small>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ <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>
83
+ </div>
84
+
85
+ <script>window.SPF_RULES = [
86
+ {"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"},
87
+ {"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"},
88
+ {"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"},
89
+ {"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"},
90
+ {"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"},
91
+ {"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"},
92
+ {"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"},
93
+ {"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"},
94
+ {"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"},
95
+ {"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"},
96
+ {"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"},
97
+ {"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"},
98
+ {"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"},
99
+ {"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"},
100
+ {"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"},
101
+ {"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"},
102
+ {"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"},
103
+ {"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"},
104
+ {"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"}
105
+ ];</script>
106
+ <script>// SPF & DMARC Record Lint — one brain. Same file runs in Node (the extension) and in the browser (the free web page).
107
+ 'use strict';
108
+ var RULES = (typeof module !== 'undefined') ? require('./rules.json') : window.SPF_RULES;
109
+
110
+ function rule(id) {
111
+ for (var i = 0; i < RULES.length; i++) if (RULES[i].id === id) return RULES[i];
112
+ return { id: id, sev: 'error', msg: id };
113
+ }
114
+ function push(out, id, line, n) {
115
+ var r = rule(id);
116
+ out.push({ check: r.id, sev: r.sev, msg: String(r.msg).replace('{n}', String(n)), line: line });
117
+ }
118
+ function strings(raw) {
119
+ var m = raw.match(/"[^"]*"/g);
120
+ return m ? m.map(function (s) { return s.slice(1, -1); }) : [];
121
+ }
122
+ function tags(rec) {
123
+ var t = {};
124
+ rec.split(';').forEach(function (part) {
125
+ var kv = part.split('=');
126
+ if (kv.length < 2) return;
127
+ var k = kv[0].trim().toLowerCase();
128
+ if (k) t[k] = kv.slice(1).join('=').trim();
129
+ });
130
+ return t;
131
+ }
132
+ function owner(raw) {
133
+ if (!/^\S/.test(raw)) return null;
134
+ if (!/\bTXT\b/i.test(raw)) return null;
135
+ var first = raw.split(/\s+/)[0];
136
+ return first ? first.toLowerCase().replace(/\.$/, '') : null;
137
+ }
138
+
139
+ function checkSpf(rec, line, out) {
140
+ var terms = rec.split(/\s+/).slice(1);
141
+ var lookups = 0, hasAll = false, passAll = false, hasPtr = false, hasRedirect = false;
142
+ for (var i = 0; i < terms.length; i++) {
143
+ var t = terms[i].replace(/[",;)]+$/, '');
144
+ if (!t) continue;
145
+ var q = '';
146
+ if (/^[+\-~?]/.test(t)) { q = t.charAt(0); t = t.slice(1); }
147
+ var name = t.split(/[:\/=]/)[0].toLowerCase();
148
+ if (name === 'include' || name === 'a' || name === 'mx' || name === 'ptr' || name === 'exists') lookups++;
149
+ if (name === 'redirect' && /^redirect=/i.test(t)) { lookups++; hasRedirect = true; }
150
+ if (name === 'ptr') hasPtr = true;
151
+ if (name === 'all') { hasAll = true; if (q === '' || q === '+') passAll = true; }
152
+ }
153
+ if (lookups > 10) push(out, 'spf_lookup_limit', line, lookups);
154
+ else if (lookups >= 7) push(out, 'spf_lookup_near', line, lookups);
155
+ if (passAll) push(out, 'spf_pass_all', line);
156
+ if (hasPtr) push(out, 'spf_ptr', line);
157
+ if (!hasAll && !hasRedirect) push(out, 'spf_no_all', line);
158
+ return lookups;
159
+ }
160
+ function checkDmarc(rec, raw, prev, line, out) {
161
+ var t = tags(rec);
162
+ if (!t.p) push(out, 'dmarc_missing_p', line);
163
+ else if (t.p.toLowerCase() === 'none') push(out, 'dmarc_p_none', line);
164
+ if (!t.rua) push(out, 'dmarc_no_rua', line);
165
+ if (t.sp && t.sp.toLowerCase() === 'none' && t.p && t.p.toLowerCase() !== 'none') push(out, 'dmarc_sp_none', line);
166
+ if (t.pct) push(out, 'dmarc_pct', line, t.pct); // RFC 9989 A.6 (2026-05) — pct removed
167
+ if (t.t && /^y/i.test(t.t)) push(out, 'dmarc_t_test', line); // RFC 9989 4.7 — test mode
168
+ if (t.np && t.np.toLowerCase() === 'none') push(out, 'dmarc_np_none', line); // RFC 9989 4.7 — non-existent subdomains
169
+ if (t.rua) {
170
+ var own = (owner(raw) || '').replace(/^_dmarc\./, '');
171
+ var hosts = [];
172
+ (t.rua.match(/mailto:[^\s,;!]+/gi) || []).forEach(function (u) {
173
+ var h = u.split('@')[1];
174
+ if (!h) return;
175
+ h = h.toLowerCase().replace(/\.$/, '');
176
+ if (own && (h === own || h.slice(-(own.length + 1)) === '.' + own)) return; // same organizational domain — no authorisation record needed
177
+ if (hosts.indexOf(h) < 0) hosts.push(h);
178
+ });
179
+ if (own && hosts.length) push(out, 'dmarc_rua_external', line, hosts.join(', ')); // RFC 9990 4 — external destination verification
180
+ }
181
+ if (!/_dmarc/i.test(raw + '\n' + prev)) push(out, 'dmarc_owner', line);
182
+ }
183
+ function checkDkim(rec, line, out) {
184
+ var t = tags(rec);
185
+ var p = (t.p || '').replace(/\s+/g, '');
186
+ if (!p) push(out, 'dkim_p_empty', line);
187
+ else if (p.length < 250) push(out, "dkim_key_short", line, p.length);
188
+ if (t.t && /(^|:)y(:|$)/i.test(t.t)) push(out, 'dkim_test_mode', line);
189
+ }
190
+
191
+ function check(text, opts) {
192
+ opts = opts || {};
193
+ var lines = String(text == null ? '' : text).split(/\r?\n/);
194
+ var out = [], seen = {}, records = 0;
195
+ for (var i = 0; i < lines.length; i++) {
196
+ var raw = lines[i];
197
+ if (/^\s*[;#]/.test(raw)) continue;
198
+ var parts = strings(raw);
199
+ var joined = parts.length ? parts.join('') : raw;
200
+ var isSpf = /v=spf1\b/i.test(joined), isDmarc = /v=DMARC1\b/i.test(joined), isDkim = /v=DKIM1\b/i.test(joined);
201
+ if (!isSpf && !isDmarc && !isDkim) continue;
202
+ records++;
203
+ var line = i + 1;
204
+ for (var s = 0; s < parts.length; s++) if (parts[s].length > 255) push(out, 'spf_string_255', line, parts[s].length);
205
+ if (isSpf) {
206
+ var own = owner(raw);
207
+ if (own) { if (seen[own]) push(out, 'spf_duplicate', line); seen[own] = true; }
208
+ checkSpf(joined.slice(joined.search(/v=spf1\b/i)).trim(), line, out);
209
+ }
210
+ if (isDmarc) {
211
+ var prev = lines.slice(Math.max(0, i - 4), i).join('\n');
212
+ checkDmarc(joined.slice(joined.search(/v=DMARC1\b/i)).trim(), raw, prev, line, out);
213
+ }
214
+ if (isDkim) checkDkim(joined.slice(joined.search(/v=DKIM1\b/i)).trim(), line, out);
215
+ }
216
+ out.sort(function (a, b) { return (a.line || 0) - (b.line || 0); });
217
+ return { findings: out, records: records, rules: RULES.length, today: opts.today || '' };
218
+ }
219
+
220
+ var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
221
+ if (typeof module !== 'undefined') module.exports = API;
222
+ if (typeof window !== 'undefined') window.SPFENGINE = API;
223
+ </script>
224
+ <script>
225
+ (function () {
226
+ var E = window.SPFENGINE, $ = function (id) { return document.getElementById(id); };
227
+ $('rulecount').textContent = E.RULE_COUNT + ' checks · offline';
228
+ function esc(s) { return String(s).replace(/[&<>]/g, function (c) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]; }); }
229
+ function run() {
230
+ var res = E.engine.check($('src').value, { today: new Date().toISOString().slice(0, 10) });
231
+ var f = res.findings, errs = 0;
232
+ for (var i = 0; i < f.length; i++) if (String(f[i].sev).indexOf('err') === 0) errs++;
233
+ var warns = f.length - errs;
234
+ var head = '<div class="summary"><span class="big">' + f.length + '</span>'
235
+ + '<span class="tag">' + errs + ' error' + (errs === 1 ? '' : 's') + ' · ' + warns + ' warning' + (warns === 1 ? '' : 's') + '</span>'
236
+ + '<span class="tag">' + res.records + ' record' + (res.records === 1 ? '' : 's') + ' read</span>'
237
+ + '<span class="tag">' + res.rules + ' rules</span></div>';
238
+ var body = f.length
239
+ ? '<ul class="findings">' + f.map(function (x) {
240
+ var sev = String(x.sev).indexOf('err') === 0 ? 'error' : 'warning';
241
+ 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>';
242
+ }).join('') + '</ul>'
243
+ : (res.records ? '<p class="clean">Clean against all ' + res.rules + ' checks.</p>'
244
+ : '<p class="clean">No v=spf1, v=DMARC1 or v=DKIM1 string found in that text.</p>');
245
+ $('result').innerHTML = head + body;
246
+ }
247
+ $('run').addEventListener('click', run);
248
+ $('clear').addEventListener('click', function () { $('src').value = ''; $('src').focus(); run(); });
249
+ run();
250
+ })();
251
+ </script>
252
+
253
+ <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 one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind SPF &amp; DMARC Record Lint every day. When a rule changes, you get a single email with what changed and the updated check. 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">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</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_alert"})}).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></section>
254
+
255
+
256
+ <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://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://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><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">npx @readystack/spf-dmarc-record-lint &lt;file&gt;
257
+ docker run --rm -v &quot;$PWD:/w&quot; getreadystack/spf-dmarc-record-lint /w</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://buy.polar.sh/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>
258
+ <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://buy.polar.sh/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>
259
+ <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-14">Updated 2026-09-14</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-14","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="SPF &amp; DMARC Record Lint" loading="lazy" style="max-width:100%;border-radius:6px"></p>
260
+ <p>Six findings in one four-record zone file — the kind a platform engineer keeps in git and edits twice a year, on the afternoon a seventh sending vendor is added.</p>
261
+ <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. Run 15 checks over it and six things are wrong: four errors and two warnings.</p>
262
+ <p><strong>The SPF record needs 11 DNS lookups.</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 <strong>PermError</strong>. Most receivers treat PermError the way they treat a domain with no SPF record at all. If your DMARC was passing on SPF alignment alone, DMARC fails with it. Nothing in the editor turns red, nothing in the deploy log turns red, and the record stays live until somebody notices invoices landing in spam.</p>
263
+ <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.</p>
264
+ <p><strong>The <code>ptr</code> mechanism is still in there.</strong> RFC 7208 §5.5 says it is deprecated and must not be published: receivers are free to ignore it, and it costs a lookup per candidate host on the way.</p>
265
+ <p><strong>A legacy sender ends in <code>+all</code>.</strong> That authorises every host on the internet to send as the domain — the same protection as publishing nothing, with the paperwork of publishing something.</p>
266
+ <p><strong>The DMARC record is <code>p=none</code> with no <code>rua=</code>.</strong> <code>p=none</code> satisfies the sentence &quot;the domain has a DMARC record&quot; and blocks nothing at all. Without <code>rua=</code>, no aggregate reports are ever sent anywhere, so there is no data to move off <code>p=none</code> with. It is monitoring with the monitor unplugged.</p>
267
+ <p><strong>The DKIM selector has an empty <code>p=</code>.</strong> An empty public key in a DKIM record means the key is revoked: every signature made with that selector fails, and mail signed only by it has no DKIM at all.</p>
268
+ <p>Fix those six and the same zone returns zero findings.</p>
269
+ <h3>Why the free checkers do not catch it</h3>
270
+ <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>
271
+ <p>SPF &amp; 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>
272
+ <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, 7-day full refund — 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 &amp; 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&#x27;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, with a 7-day full refund. One rejected invoice run costs more than either.</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, with a 7-day full refund. One rejected invoice run costs more than either."}}]}</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, 7-day refund.</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 &amp; 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/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><a class="yj-rel-c" href="https://getreadystack.com/tools/email-footer-law-lint"><span class="yj-rel-t">Email Footer Law Lint</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><!--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 {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[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>
273
+
274
+ <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>
275
+
276
+ <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>
277
+ </body>
278
  </html>