Spaces:
Running
Running
Download index.html from getreadystack/bulk-sender-lint: direct link, hf CLI and curl.
- Browser
- Download file 48.8 kB
-
https://huggingface.co/spaces/getreadystack/bulk-sender-lint/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/bulk-sender-lint/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/bulk-sender-lint/resolve/main/index.html
48.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Spf dkim dmarc check — check email sending setup and dns records</title> | |
| <style> | |
| :root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512} | |
| .wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px} | |
| textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box} | |
| .bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer} | |
| input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px} | |
| table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65} | |
| .sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600} | |
| .note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa} | |
| </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/bulk-sender-lint"></head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>Spf dkim dmarc check — check email sending setup and dns records</h1> | |
| <p class="lede">Checks mail-sending code and DNS records against the Gmail/Yahoo bulk sender rules and Microsoft Outlook.com's: SPF, DKIM, DMARC, RFC 8058 one-click unsubscribe, TLS - each with the date it took effect. Runs entirely in your browser — nothing is uploaded.</p> | |
| <textarea id="src" spellcheck="false">// mail/digest.js - nightly product digest, ~42,000 recipients (mostly gmail.com and outlook.com) | |
| 'use strict'; | |
| const nodemailer = require('nodemailer'); | |
| const app = require('../app'); | |
| const { requireAuth } = require('../auth'); | |
| // DNS for the sending domain - paste into Route 53 when the new sender goes live | |
| const DNS = { | |
| spf: 'v=spf1 include:_spf.google.com include:sendgrid.net include:mailgun.org include:amazonses.com include:servers.mcsv.net include:spf.protection.outlook.com include:_spf.salesforce.com include:mail.zendesk.com include:shops.shopify.com include:stspg-customer.com include:helpscout.net ?all', | |
| dkim: 'v=DKIM1; k=rsa; t=y; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7Kd1JqTn4fY2mQ0Z', | |
| dmarc: 'v=DMARC1; p=none; pct=20; sp=none', | |
| bimi: 'v=BIMI1; l=https://cdn.acme.example/logo.svg;' | |
| }; | |
| const transport = nodemailer.createTransport({ | |
| host: 'smtp.acme.example', port: 25, secure: false, tls: { rejectUnauthorized: false }, | |
| auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASS } | |
| }); | |
| async function sendDigest(recipients, html) { | |
| for (const to of recipients) { | |
| await transport.sendMail({ | |
| from: 'Acme Digest <acme.digest@gmail.com>', | |
| to, | |
| subject: 'Your weekly Acme digest', | |
| html, | |
| headers: { 'List-Unsubscribe': '<mailto:unsub@acme.example>' } | |
| }); | |
| } | |
| } | |
| app.get('/unsubscribe', requireAuth, (req, res) => res.render('unsubscribe', { user: req.user })); | |
| module.exports = { sendDigest, DNS };</textarea> | |
| <div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div> | |
| <div id="out" aria-live="polite"></div> | |
| <div id="paid"><strong>Free here:</strong> Check the file you have open - every check, with the rule it comes from, the date it took effect and the line. <strong>In the editor:</strong> Sweep every file in the workspace and write a dated evidence report you keep - $29 once, one licence key per person or team seat; a deliverability consultant bills $150-$250 an hour — that part asks for a licence key.<p style="margin:10px 0 0"><a href="https://getreadystack.com/api/buy/cl/polar_cl_wVUwTqi8qqkeHlhoZHE3VCSgjwBGHxsQL9rYt24DtQP">Get the workspace sweep and the dated report — $29 once</a></p></div> | |
| <p class="note">Same engine as the VS Code extension, byte for byte.</p> | |
| </div> | |
| <script>var BULK_RULES = [ | |
| {"id":"spf_missing","sev":"error","kind":"require_when","when":"v=DMARC1|v=DKIM1|_dmarc|_domainkey","need":"v=spf1","since":"2024-02-01", | |
| "msg":"Mail DNS is configured here but there is no SPF record (v=spf1). Gmail and Yahoo require bulk senders - 5,000+ messages a day to their users, counted per From domain - to publish SPF and DKIM and to pass DMARC alignment.", | |
| "cite":"Google Email sender guidelines (bulk senders); Yahoo Sender Requirements"}, | |
| {"id":"spf_all_neutral","sev":"error","kind":"forbid","bad":"v=spf1[^\"'\\n]*[?+]all","since":"2024-02-01", | |
| "msg":"SPF ends in ?all or +all, so every server on the internet passes SPF for this domain. A pass that anyone can get is not a pass a receiver will align on. End the record with -all, or ~all while you are still finding senders.", | |
| "cite":"RFC 7208 section 8; Google Email sender guidelines"}, | |
| {"id":"spf_lookup_limit","sev":"error","kind":"fn:spf_lookups","since":"2024-02-01", | |
| "msg":"SPF needs more than 10 DNS lookups to evaluate. Over the limit the receiver returns PermError, which DMARC treats as an SPF failure - the record stops working without changing.", | |
| "cite":"RFC 7208 section 4.6.4"}, | |
| {"id":"spf_multiple","sev":"error","kind":"fn:spf_multiple","since":"2024-02-01", | |
| "msg":"More than one SPF record is published for the same name. Two v=spf1 records is a PermError, not a merge: neither record is used.", | |
| "cite":"RFC 7208 section 3.2"}, | |
| {"id":"dmarc_missing","sev":"error","kind":"require_when","when":"v=spf1|v=DKIM1|_domainkey","need":"v=DMARC1","since":"2024-02-01", | |
| "msg":"No DMARC record (v=DMARC1) for the sending domain. Gmail and Yahoo have required one from bulk senders since 2024-02-01, and Microsoft Outlook.com since 2025-05-05; p=none is the accepted minimum.", | |
| "cite":"Google Email sender guidelines; Microsoft Outlook.com sender requirements (2025-05-05)"}, | |
| {"id":"dmarc_p_none","sev":"warning","kind":"forbid","bad":"v=DMARC1[^\"'\\n]*p=none","since":"2024-02-01", | |
| "msg":"DMARC policy is p=none. That clears the Gmail, Yahoo and Outlook.com minimum but instructs receivers to do nothing about mail that fails, so anyone can still send as your domain. Move to quarantine once your reports are clean.", | |
| "cite":"RFC 7489 section 6.3; Google Email sender guidelines"}, | |
| {"id":"dmarc_rua_missing","sev":"warning","kind":"require_when","when":"v=DMARC1","need":"rua=mailto:", | |
| "msg":"DMARC record has no rua= aggregate report address, so nothing tells you which of your senders fail alignment. Without rua you cannot safely raise the policy later.", | |
| "cite":"RFC 7489 section 7.1"}, | |
| {"id":"dmarc_pct_partial","sev":"warning","kind":"fn:dmarc_pct", | |
| "msg":"DMARC pct= is below 100, so the policy is applied to only part of your mail and the rest is treated as p=none. Leave pct at 100 unless you are mid-rollout.", | |
| "cite":"RFC 7489 section 6.3"}, | |
| {"id":"dmarc_sp_none","sev":"warning","kind":"forbid","bad":"v=DMARC1[^\"'\\n]*sp=none", | |
| "msg":"sp=none exempts every subdomain from the policy. Look-alike subdomains are the usual path for spoofed invoices; set sp to match p.", | |
| "cite":"RFC 7489 section 6.3"}, | |
| {"id":"dkim_missing","sev":"error","kind":"require_when","when":"v=spf1|v=DMARC1|_dmarc","need":"v=DKIM1","since":"2024-02-01", | |
| "msg":"No DKIM record (v=DKIM1) for the sending domain. SPF alone breaks on every forward and mailing list; Gmail and Yahoo ask bulk senders for both SPF and DKIM.", | |
| "cite":"RFC 6376; Google Email sender guidelines"}, | |
| {"id":"dkim_key_short","sev":"warning","kind":"fn:dkim_key", | |
| "msg":"The DKIM public key is short enough to be a 1024-bit key. 1024 is the floor; Google's guidelines say use a 2048-bit key where your DNS host allows the longer record.", | |
| "cite":"RFC 6376 section 3.3.3; Google Email sender guidelines"}, | |
| {"id":"dkim_testmode","sev":"error","kind":"forbid","bad":"v=DKIM1[^\"'\\n]*[;\\s]t=y", | |
| "msg":"DKIM record carries t=y, testing mode. Receivers are told to treat a signature failure as if the message were unsigned, so the signature buys you nothing - and the domain still looks configured.", | |
| "cite":"RFC 6376 section 3.6.1"}, | |
| {"id":"bimi_unenforced","sev":"error","kind":"fn:bimi", | |
| "msg":"A BIMI record is published while DMARC is missing or at p=none. BIMI is only honoured at an enforcement policy (quarantine or reject), so the logo will never show.", | |
| "cite":"BIMI draft specification, DMARC enforcement requirement"}, | |
| {"id":"unsub_missing","sev":"error","kind":"require_when","when":"createTransport|sendMail|sgMail|mailgun|SendEmail|sesv2|resend\\.emails|smtplib|send_message","need":"List-Unsubscribe","since":"2024-02-01", | |
| "msg":"This code sends mail but sets no List-Unsubscribe header. Bulk senders must offer one-click unsubscribe in commercial and subscribed messages, and act on it within two days.", | |
| "cite":"RFC 8058; Google Email sender guidelines"}, | |
| {"id":"unsub_post_missing","sev":"error","kind":"require_when","when":"List-Unsubscribe","need":"List-Unsubscribe-Post","since":"2024-02-01", | |
| "msg":"List-Unsubscribe is set but List-Unsubscribe-Post is not, so this is the old two-step header, not one-click. Gmail and Yahoo require the RFC 8058 pair on bulk mail.", | |
| "cite":"RFC 8058 section 3.1"}, | |
| {"id":"unsub_post_value","sev":"error","kind":"fn:unsub_post_value","since":"2024-02-01", | |
| "msg":"List-Unsubscribe-Post must carry exactly the value List-Unsubscribe=One-Click. Any other value and the mail client will not show the one-click control.", | |
| "cite":"RFC 8058 section 3.1"}, | |
| {"id":"unsub_mailto_only","sev":"error","kind":"fn:unsub_mailto", | |
| "msg":"List-Unsubscribe offers only a mailto: address. One-click unsubscribe is an HTTPS POST, so a mailto-only header cannot satisfy it - add an https:// URI to the header.", | |
| "cite":"RFC 8058 section 1; Google Email sender guidelines"}, | |
| {"id":"unsub_route_get_only","sev":"error","kind":"fn:unsub_route", | |
| "msg":"The unsubscribe endpoint answers GET but not POST. The mail client sends a POST with the body List-Unsubscribe=One-Click; a GET-only route returns 404 or 405 and the unsubscribe silently fails.", | |
| "cite":"RFC 8058 section 3.2"}, | |
| {"id":"unsub_auth_required","sev":"error","kind":"forbid","bad":"(?:get|post|route)\\s*\\(\\s*['\"][^'\"]*unsubscribe[^'\"]*['\"]\\s*,\\s*(?:requireAuth|isAuthenticated|ensureLoggedIn|authMiddleware|login_required)", | |
| "msg":"The unsubscribe route is behind a login. One-click unsubscribe arrives as an unauthenticated POST from the mail client, and an unsubscribe that needs a sign-in counts as an unsubscribe that does not work.", | |
| "cite":"RFC 8058 section 3.2; Google Email sender guidelines"}, | |
| {"id":"smtp_tls_off","sev":"error","kind":"forbid","bad":"secure\\s*:\\s*false|ignoreTLS\\s*:\\s*true|rejectUnauthorized\\s*:\\s*false|port\\s*[:=]\\s*25\\b","since":"2024-02-01", | |
| "msg":"This transport disables TLS, disables certificate checking, or connects on port 25. Bulk senders are required to transmit over a TLS connection.", | |
| "cite":"Google Email sender guidelines (TLS requirement)"}, | |
| {"id":"from_consumer_domain","sev":"error","kind":"forbid","bad":"(?:from|From|sender)[^\\n]{0,40}@(?:gmail|yahoo|hotmail|outlook|aol)\\.com","since":"2024-02-01", | |
| "msg":"Bulk mail is being sent with a consumer mailbox domain in From. Those domains publish DMARC p=reject for their own users, so mail you send from your servers as that address fails DMARC and is rejected outright.", | |
| "cite":"Google Email sender guidelines (do not send from gmail.com); RFC 7489 section 6.6.2"} | |
| ] | |
| ;</script> | |
| <script>// Bulk Sender Lint - the brain. Same file runs in Node (VS Code extension) and in the browser (free web check). | |
| ; | |
| var RULES = (typeof module !== 'undefined' && module.exports) ? require('./rules.json') : window.BULK_RULES; | |
| function lineOf(text, index) { return text.slice(0, index).split('\n').length; } | |
| function rx(p, extra) { return new RegExp(p, 'i' + (extra || '')); } | |
| function days(fromISO, toISO) { | |
| var a = Date.parse(fromISO + 'T00:00:00Z'), b = Date.parse(toISO + 'T00:00:00Z'); | |
| if (isNaN(a) || isNaN(b)) return null; | |
| return Math.round((b - a) / 86400000); | |
| } | |
| function dated(rule, today) { | |
| if (!rule.since) return rule.msg + ' [' + rule.cite + ']'; | |
| var d = days(rule.since, today), when; | |
| if (d === null) when = 'in force since ' + rule.since; | |
| else if (d >= 0) when = 'in force since ' + rule.since + ', ' + d + ' days ago'; | |
| else when = 'takes effect ' + rule.since + ', in ' + (-d) + ' days'; | |
| return rule.msg + ' (' + when + ') [' + rule.cite + ']'; | |
| } | |
| // --- the SPF / DKIM / DMARC records and the unsubscribe headers found in the text --- | |
| function records(text, re) { | |
| var out = [], m, g = new RegExp(re, 'gi'); | |
| while ((m = g.exec(text))) { out.push({ s: m[0], line: lineOf(text, m.index) }); if (m.index === g.lastIndex) g.lastIndex++; } | |
| return out; | |
| } | |
| var SPF = "v=spf1[^\"'\\n]*", DMARC = "v=DMARC1[^\"'\\n]*", DKIM = "v=DKIM1[^\"'\\n]*"; | |
| var LOOKUP = /^[+\-~?]?(?:(?:include|exists|ptr|a|mx):\S+|redirect=\S+|a|mx|ptr)$/i; | |
| var FN = { | |
| spf_lookups: function (text) { | |
| var hits = []; | |
| records(text, SPF).forEach(function (r) { | |
| var n = r.s.split(/\s+/).filter(function (t) { return LOOKUP.test(t); }).length; | |
| if (n > 10) hits.push({ line: r.line, extra: n + ' lookups in the record, limit is 10' }); | |
| }); | |
| return hits; | |
| }, | |
| spf_multiple: function (text) { | |
| var r = records(text, SPF); | |
| return r.length > 1 ? [{ line: r[1].line, extra: r.length + ' v=spf1 records in this file' }] : []; | |
| }, | |
| dmarc_pct: function (text) { | |
| var hits = []; | |
| records(text, DMARC).forEach(function (r) { | |
| var m = /pct=(\d+)/i.exec(r.s); | |
| if (m && parseInt(m[1], 10) < 100) hits.push({ line: r.line, extra: 'pct=' + m[1] + ', so ' + (100 - parseInt(m[1], 10)) + '% of failing mail is let through' }); | |
| }); | |
| return hits; | |
| }, | |
| dkim_key: function (text) { | |
| var hits = []; | |
| records(text, DKIM).forEach(function (r) { | |
| var m = /p=([A-Za-z0-9+/=.]+)/.exec(r.s); | |
| if (m && m[1].length < 300) hits.push({ line: r.line, extra: m[1].length + ' base64 characters, a 2048-bit key is about 392' }); | |
| }); | |
| return hits; | |
| }, | |
| bimi: function (text) { | |
| var b = records(text, "v=BIMI1[^\"'\\n]*"); | |
| if (!b.length) return []; | |
| var d = records(text, DMARC); | |
| var enforced = d.some(function (r) { return /p=(quarantine|reject)/i.test(r.s); }); | |
| return enforced ? [] : [{ line: b[0].line, extra: d.length ? 'DMARC is at p=none' : 'no DMARC record here' }]; | |
| }, | |
| unsub_post_value: function (text) { | |
| var hits = []; | |
| records(text, "List-Unsubscribe-Post[^\\n]*").forEach(function (r) { | |
| if (!/List-Unsubscribe\s*=\s*One-Click/i.test(r.s)) hits.push({ line: r.line, extra: 'value found: ' + r.s.slice(0, 80) }); | |
| }); | |
| return hits; | |
| }, | |
| unsub_mailto: function (text) { | |
| var hits = []; | |
| records(text, "List-Unsubscribe(?!-Post)[\"']?\\s*[:=,][^\\n]*").forEach(function (r) { | |
| if (/mailto:/i.test(r.s) && !/https:\/\//i.test(r.s)) hits.push({ line: r.line, extra: 'header has mailto: and no https:// URI' }); | |
| }); | |
| return hits; | |
| }, | |
| unsub_route: function (text) { | |
| var getJs = /\.get\s*\(\s*['"][^'"]*unsubscribe/i.exec(text); | |
| var postJs = /\.post\s*\(\s*['"][^'"]*unsubscribe/i.exec(text); | |
| if (getJs && !postJs) return [{ line: lineOf(text, getJs.index), extra: 'GET route found, no matching POST route' }]; | |
| var py = /route\s*\(\s*['"][^'"]*unsubscribe[^'"]*['"][^)]*methods\s*=\s*\[[^\]]*\]/i.exec(text); | |
| if (py && !/POST/i.test(py[0])) return [{ line: lineOf(text, py.index), extra: 'route declares methods without POST' }]; | |
| return []; | |
| } | |
| }; | |
| function check(text, opts) { | |
| text = String(text == null ? '' : text); | |
| opts = opts || {}; | |
| var today = opts.today || new Date().toISOString().slice(0, 10); | |
| var findings = []; | |
| function add(rule, line, extra) { | |
| findings.push({ check: rule.id, sev: rule.sev, line: line || 1, msg: (extra ? extra + '. ' : '') + dated(rule, today) }); | |
| } | |
| RULES.forEach(function (rule) { | |
| if (rule.kind === 'forbid') { | |
| var seen = {}, m, g = new RegExp(rule.bad, 'gi'), n = 0; | |
| while ((m = g.exec(text)) && n < 10) { | |
| var ln = lineOf(text, m.index); | |
| if (!seen[ln]) { seen[ln] = 1; n++; add(rule, ln, null); } | |
| if (m.index === g.lastIndex) g.lastIndex++; | |
| } | |
| } else if (rule.kind === 'require_when') { | |
| var w = rx(rule.when).exec(text); | |
| if (w && !rx(rule.need).test(text)) add(rule, lineOf(text, w.index), null); | |
| } else if (rule.kind.indexOf('fn:') === 0) { | |
| var fn = FN[rule.kind.slice(3)]; | |
| if (fn) fn(text).forEach(function (h) { add(rule, h.line, h.extra); }); | |
| } | |
| }); | |
| findings.sort(function (a, b) { return a.line - b.line; }); | |
| return { findings: findings, checked: RULES.length, today: today }; | |
| } | |
| var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = API; | |
| if (typeof window !== 'undefined') window.BULKENGINE = API; | |
| </script> | |
| <script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다. | |
| ; | |
| function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } | |
| function summaryLine(res) { | |
| const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length; | |
| return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.'; | |
| } | |
| function toText(res, label) { | |
| const f = (res && res.findings) || []; | |
| const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)]; | |
| f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); }); | |
| return lines.join('\n'); | |
| } | |
| function toHtml(res) { | |
| const f = (res && res.findings) || []; | |
| if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>'; | |
| const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join(''); | |
| return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>'; | |
| } | |
| var BULKREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = BULKREPORT; | |
| else window.BULKREPORT = BULKREPORT; | |
| </script> | |
| <script> | |
| (function(){ | |
| var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today'); | |
| today.value=new Date().toISOString().slice(0,10); | |
| function run(){ try { var res=window.BULKENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.BULKREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } } | |
| go.onclick=run; if (src.value.trim()) 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.bulk-sender-lint" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'bulk-sender-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://www.npmjs.com/package/@readystack/bulk-sender-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'bulk-sender-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/bulk-sender-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'bulk-sender-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/bulk-sender-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'bulk-sender-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:'bulk-sender-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.bulk-sender-lint | |
| npx @readystack/bulk-sender-lint <file> | |
| npx @readystack/bulk-sender-lint --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/team" 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 21 rules Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, checked in your mailer code 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:"bulk-sender-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">21 dated checks over SPF, DKIM, DMARC, RFC 8058 one-click unsubscribe and TLS, in VS Code and in the browser</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A deliverability consultant bills $150 to $250 an hour</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_wVUwTqi8qqkeHlhoZHE3VCSgjwBGHxsQL9rYt24DtQP" 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><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><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">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)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(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{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-12">Updated 2026-09-12</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-12","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>Fifteen findings, ten of them errors, out of 21 checks - that is what one 42,000-recipient nightly digest module produces, the kind of mailer file a backend developer at a small SaaS company writes once and touches twice a year. It passes lint, tests and code review. It also breaks four separate requirements that Gmail, Yahoo and Microsoft Outlook.com have been enforcing on bulk senders for over two years.</p> | |
| <p>The dates are the part people get wrong. Google and Yahoo's bulk sender requirements took effect on 2024-02-01 and apply to anyone sending more than 5,000 messages a day to their users, counted per From domain. Microsoft Outlook.com started enforcing the same authentication set on 2025-05-05, routing non-compliant bulk mail to the Junk folder. Nothing warns you on the day you cross the threshold.</p> | |
| <p>Here is what the fixture actually contains, line by line.</p> | |
| <p>Line 9 is an SPF record with eleven include: tokens. RFC 7208 section 4.6.4 caps SPF evaluation at ten DNS lookups; past that the receiver returns PermError, which DMARC treats as an SPF failure. The record was fine when it had six includes. Nobody changed it - the vendors it points at added their own includes. The same line ends in ?all, so any server passes SPF anyway.</p> | |
| <p>Line 10 is a DKIM record carrying t=y, testing mode, which tells receivers to treat a signature failure exactly as if the message were unsigned. The key on that line is 55 base64 characters, where a 2048-bit key is about 392.</p> | |
| <p>Line 11 is v=DMARC1; p=none; pct=20; sp=none - a policy that instructs receivers to do nothing, applied to a fifth of the mail, with every subdomain exempted. It satisfies the Gmail and Outlook.com minimum and protects nothing.</p> | |
| <p>Line 16 sets port 25, secure: false and rejectUnauthorized: false on the transport, against a requirement to transmit over TLS. Line 23 sends the whole campaign From acme.digest@gmail.com, a domain that publishes p=reject for its own users - so the mail fails DMARC at the receiver by construction.</p> | |
| <p>Then the unsubscribe, which is where the code-side findings cluster. Line 27 sets List-Unsubscribe to a mailto: address and no https:// URI, and sets no List-Unsubscribe-Post at all. One-click unsubscribe under RFC 8058 is an HTTPS POST carrying the body List-Unsubscribe=One-Click; a mailto-only header cannot serve it, and without the paired Post header the mail client shows the old two-step link instead. Line 32 declares the unsubscribe route with app.get and wraps it in requireAuth - so even when a client does send the POST, it arrives at a route that answers GET only, behind a login the mail client has never seen.</p> | |
| <p>That is the part an online SPF or DMARC checker structurally cannot tell you. Those tools read published DNS for a domain that already exists. Eight of these 21 checks live in code - the headers, the TLS flags, the From line, the unsubscribe route - and two more read records still sitting in a Terraform file.</p> | |
| <p>Bulk Sender Lint runs the 21 checks over the file you have open and prints the rule, the line, the clause (RFC 7208, 6376, 7489, 8058) and how many days ago that requirement took effect. That part is free, in the editor and in the browser, with nothing uploaded. The paid part is a different axis: sweep every matching file in the workspace and write a dated report you keep - the artefact you hand to a deliverability review or a client. $29 once, one licence key per person or team seat, 7-day full refund. A deliverability consultant bills $150 to $250 an hour.</p> | |
| <p>Run it on the fixed version of the same file and it says: clean against all 21 checks.</p></div><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 Bulk Sender 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 files where sending is configured - mailer code, DNS records, the unsubscribe route - and applies 21 checks drawn from the Gmail, Yahoo and Microsoft Outlook.com bulk sender requirements and from RFC 7208, 6376, 7489 and 8058. Each finding names the line, the clause it comes from, and the date that requirement took effect.</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">Backend and platform developers who own transactional or marketing email inside a product: the person paged when a 40,000-recipient digest lands in Junk. It is written for the repository rather than the marketing dashboard, so it fits teams sending through Amazon SES, SendGrid, Mailgun, Postmark or their own SMTP relay.</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 SPF and DMARC checker?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Online checkers read DNS for a domain you have already published. They cannot see the mailer code, where the List-Unsubscribe-Post header, the TLS flag and the unsubscribe route live, and they cannot read a record still sitting in a Terraform file. Eight of the 21 checks are code-side and invisible to them.</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 is paid?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free: checking the file you have open, all 21 checks, every finding with its clause and its line - and the same engine runs free in the browser with nothing uploaded. Paid, $29 once: sweeping every matching file in the workspace and writing a dated report you keep as the evidence artefact.</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 if a person did it?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A deliverability consultant bills $150 to $250 an hour, and reviewing one sending stack is rarely a single hour. The extension is $29 once, one licence key per person or team seat,. The single-file check and the browser check stay free either way.</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. Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, c checks the file you open against 21 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 Bulk Sender Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads the files where sending is configured - mailer code, DNS records, the unsubscribe route - and applies 21 checks drawn from the Gmail, Yahoo and Microsoft Outlook.com bulk sender requirements and from RFC 7208, 6376, 7489 and 8058. Each finding names the line, the clause it comes from, and the date that requirement took effect."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Backend and platform developers who own transactional or marketing email inside a product: the person paged when a 40,000-recipient digest lands in Junk. It is written for the repository rather than the marketing dashboard, so it fits teams sending through Amazon SES, SendGrid, Mailgun, Postmark or their own SMTP relay."}}, {"@type": "Question", "name": "Why not just use a free SPF and DMARC checker?", "acceptedAnswer": {"@type": "Answer", "text": "Online checkers read DNS for a domain you have already published. They cannot see the mailer code, where the List-Unsubscribe-Post header, the TLS flag and the unsubscribe route live, and they cannot read a record still sitting in a Terraform file. Eight of the 21 checks are code-side and invisible to them."}}, {"@type": "Question", "name": "What is free and what is paid?", "acceptedAnswer": {"@type": "Answer", "text": "Free: checking the file you have open, all 21 checks, every finding with its clause and its line - and the same engine runs free in the browser with nothing uploaded. Paid, $29 once: sweeping every matching file in the workspace and writing a dated report you keep as the evidence artefact."}}, {"@type": "Question", "name": "What would this cost if a person did it?", "acceptedAnswer": {"@type": "Answer", "text": "A deliverability consultant bills $150 to $250 an hour, and reviewing one sending stack is rarely a single hour. The extension is $29 once, one licence key per person or team seat,. The single-file check and the browser check stay free either way."}}, {"@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. Bulk Sender Lint - the Gmail, Yahoo and Outlook.com rules, c checks the file you open against 21 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:38017,slug:"bulk-sender-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/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/en301549-lint"><span class="yj-rel-t">EN 301 549 Lint — European Accessibility Act rules, with the clause number, in</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/spf-dmarc-record-lint"><span class="yj-rel-t">SPF & DMARC Record Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/coverage-gate-lint"><span class="yj-rel-t">Coverage Gate Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/devicetree-config-language-pack"><span class="yj-rel-t">DeviceTree Config Audit: 23 dtc-Style Rules, 50 Snippets</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> | |