If your app sends email — transactional or marketing — three DNS records decide whether it lands in the inbox or the spam folder: SPF, DKIM, and DMARC. Here's how to look them up and sanity-check them in Python, no third-party API required.
Install the one dependency:
pip install dnspython
SPF: who is allowed to send
SPF lives in a TXT record on the domain itself and starts with v=spf1.
import dns.resolver
def get_spf(domain):
for rec in dns.resolver.resolve(domain, "TXT"):
txt = b"".join(rec.strings).decode()
if txt.startswith("v=spf1"):
return txt
return None
print(get_spf("github.com"))
# v=spf1 ip4:... include:_spf.google.com ~all
A quick gotcha worth checking: SPF allows at most 10 DNS-querying mechanisms (include, a, mx, ptr, exists, redirect). Go over and receivers return permerror, which quietly breaks authentication:
def spf_lookup_count(spf):
return sum(spf.count(m) for m in ("include:", "a:", "mx:", "ptr", "exists:", "redirect="))
spf = get_spf("example.com")
if spf and spf_lookup_count(spf) > 10:
print("⚠️ SPF exceeds the 10-lookup limit")
DMARC: the policy that ties it together
DMARC is a TXT record on the _dmarc. subdomain and starts with v=DMARC1.
def get_dmarc(domain):
try:
for rec in dns.resolver.resolve(f"_dmarc.{domain}", "TXT"):
txt = b"".join(rec.strings).decode()
if txt.startswith("v=DMARC1"):
return dict(
kv.strip().split("=", 1)
for kv in txt.split(";") if "=" in kv
)
except dns.resolver.NXDOMAIN:
return None
print(get_dmarc("github.com"))
# {'v': 'DMARC1', 'p': 'reject', 'rua': 'mailto:...'}
The key field is p: none (monitor only), quarantine (spam folder), or reject (bounce). If a domain sends real mail but has p=none, it's not protected against spoofing yet.
DKIM: the signature key
DKIM is trickier because you need the selector — a label chosen by the sender that lives at SELECTOR._domainkey.DOMAIN. There's no way to enumerate selectors from DNS, so you try common ones:
COMMON_SELECTORS = ["google", "default", "s1", "s2", "k1", "selector1", "selector2", "dkim"]
def find_dkim(domain):
found = {}
for sel in COMMON_SELECTORS:
try:
for rec in dns.resolver.resolve(f"{sel}._domainkey.{domain}", "TXT"):
txt = b"".join(rec.strings).decode()
if "v=DKIM1" in txt or "p=" in txt:
found[sel] = txt
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
continue
return found
print(find_dkim("github.com").keys())
If none of the common selectors resolve, the domain may use a custom one — you'll need to read it from an actual signed message's DKIM-Signature: header (s= tag).
Putting it together
def audit(domain):
spf = get_spf(domain)
dmarc = get_dmarc(domain)
dkim = find_dkim(domain)
print(f"{domain}")
print(f" SPF : {'✓' if spf else '✗ missing'}")
print(f" DMARC : {dmarc.get('p') if dmarc else '✗ missing'}")
print(f" DKIM : {', '.join(dkim) if dkim else '✗ none of the common selectors'}")
audit("your-domain.com")
Run that against your sending domain before you ship a campaign. Missing DKIM or a p=none DMARC are the two most common reasons legitimate mail gets filtered after Gmail and Yahoo tightened bulk-sender rules in 2024.
When you need this at scale
The snippets above are perfect for one domain in a script. If you're validating whole lists, warming a new sending domain, or want the checks plus blacklist and deliverability scoring in one pass, I built these into Bulko's free email tools — same logic, no code to run. But for a quick programmatic check, dnspython is all you need.
Top comments (0)