How to bulk-check DMARC, SPF and DKIM for a list of domains (free Python script)
Every few weeks someone on Reddit or a sysadmin forum asks some version of: "I have 300 client/prospect domains. How do I check which ones have no DMARC, or a weak SPF, without pasting them one by one into MXToolbox?" H
Every few weeks someone on Reddit or a sysadmin forum asks some version of: "I have 300 client/prospect domains. How do I check which ones have no DMARC, or a weak SPF, without pasting them one by one into MXToolbox?"
Here's a small, dependency-light Python script that does exactly that from public DNS, plus the gotchas we hit doing this on real small-business domains.
The script
pip install dnspython
# bulk_dmarc.py - usage: python bulk_dmarc.py domains.txt > results.csv
import csv, re, sys
import dns.resolver
R = dns.resolver.Resolver()
R.lifetime = 5
DKIM_SELECTORS = ["google", "selector1", "selector2", "default", "k1", "k2",
"s1", "s2", "mail", "dkim", "smtp", "mxvault", "zoho"]
def txt(name):
try:
return [b"".join(r.strings).decode(errors="ignore") for r in R.resolve(name, "TXT")]
except Exception:
return []
def has_mx(domain):
try:
return len(R.resolve(domain, "MX")) > 0
except Exception:
return False
def check(domain):
spf = next((t for t in txt(domain) if t.lower().startswith("v=spf1")), "")
dmarc = next((t for t in txt("_dmarc." + domain) if t.lower().startswith("v=dmarc1")), "")
policy = ""
for part in dmarc.replace(" ", "").split(";"):
if part.lower().startswith("p="):
policy = part[2:].lower()
# a DKIM record with an empty p= is a revoked key, so require a value
dkim = [s for s in DKIM_SELECTORS
if any(re.search(r"(^|;)\s*p=[A-Za-z0-9+/]", t) for t in txt(f"{s}._domainkey.{domain}"))]
mx = has_mx(domain)
spoofable = mx and policy in ("", "none")
return {
"domain": domain, "has_mx": mx, "spf": spf or "MISSING",
"spf_all": (spf.split()[-1] if spf else ""),
"dmarc_policy": policy or "MISSING", "dkim_selectors_found": " ".join(dkim),
"spoofable": spoofable,
}
domains = [l.strip().lower() for l in open(sys.argv[1]) if l.strip()]
w = csv.DictWriter(sys.stdout, fieldnames=list(check("example.com").keys()))
w.writeheader()
for d in domains:
w.writerow(check(d))
That's the whole thing. For a few hundred domains it runs in a couple of minutes.
Gotchas worth knowing
1. Only count "spoofable" when the domain actually receives mail. A parked domain with no MX and no DMARC is noise. Filter on has_mx first, or you'll overstate the problem (and look silly if you're using this for outreach).
2. p=none is not protection. It's monitoring. Treat p=none the same as no DMARC when you're asking "can someone send email as this domain?" In our sample of 51 small accountants, law firms and dental practices, 30 of the ones with mailboxes were spoofable, and 13 of those had p=none.
3. DKIM can't be enumerated. DNS has no way to list selectors, so you can only guess common ones. "No DKIM found" is a low-confidence result. Never lead a conversation with it. Also, a record like v=DKIM1; p= (empty key) means the key was revoked, so don't count it as "DKIM present". example.com publishes exactly that on every selector.
4. SPF ending in ?all or +all is effectively open. ~all (softfail) is common and fine alongside DMARC; ?all and +all aren't.
5. Watch the 10-lookup SPF limit. Long include: chains silently break SPF (permerror). If you're auditing clients, add a recursive lookup counter. That's a nice follow-up to the script above.
6. Resolver rate limits. Hammering a public resolver with thousands of TXT queries gets you SERVFAILs that look like "missing records". Add retries, or spread across resolvers, before trusting a MISSING.
If you're doing this for prospecting
If you're an MSP or web agency checking prospects rather than your own clients, a raw pass/fail CSV isn't enough: you want to know which companies to call first and what to say. We packaged a hosted version that also checks SSL expiry, HTTP-only sites, outdated WordPress/PHP and domain expiry, then ranks every company with a 0–100 score, the exact evidence and a one-line opener. It's pay-per-result on Apify ($0.015 per company, invalid domains free):
- Security & Website Pain Prospect Finder (full prospect scoring)
- Email Domain Security Auditor (just the DMARC/SPF/DKIM part, in bulk)
But for most people the script above is all you need. Questions about edge cases (subdomain policies, sp=, pct=, third-party senders) are welcome in the comments.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.