Guide · Backend
Email Deliverability Explained: SPF, DKIM, DMARC and Sender Reputation
23 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Email deliverability is whether your mail reaches the inbox, not just the receiving server. Today that rests on SPF, DKIM and DMARC plus a clean sending history.
- SPF (Sender Policy Framework) is a list of servers allowed to send for your domain, published in DNS, the internet's public name directory. DKIM (DomainKeys Identified Mail) adds a digital signature that proves the signed headers and body were not changed.
- DMARC (Domain-based Message Authentication, Reporting and Conformance) passes only when SPF or DKIM passes for a domain that matches the From line the reader sees. It also tells receivers what to do with failures.
- As of 24 October 2026, Gmail and Yahoo require SPF, DKIM and a DMARC record from bulk senders, plus one-click unsubscribe on marketing mail. Bounces, spam complaints and spam traps then decide your reputation.
- Beginners can stop after the DMARC section. Engineers get the SPF 10-lookup trap tested on pyspf, DKIM tamper tests on dkimpy, the 2026 DMARC update (RFC 9989), and warm-up and bounce rules.
What goes wrong when your app's emails land in spam?
Your users never see the OTP (one-time password), the invoice or the password reset, so they think your app is broken. The mail left your server fine, but the receiving mailbox did not trust it. That is an email deliverability problem, and it usually starts in DNS.
Picture a common setup. A small team sends sign-up OTPs and order receipts from [email protected] through an email provider, and it works for months. Then marketing sends a large campaign from the same domain through a new tool nobody set up in DNS. Those messages bounce with 550 5.7.26, complaints pile up, and soon the OTPs land in spam too. The setup is made up, but the code is real: Gmail uses it for unauthenticated mail and for mail blocked by DMARC.
Email deliverability is the share of your mail that reaches the inbox. It depends on two things. First, can the receiver prove the mail really came from your domain? Second, does your domain have a good record of sending mail people want? This guide covers both, on this one running example.
Which words do you need before reading about email deliverability?
Email has two sender addresses, and most confusion starts here. One is on the envelope that servers use, and one is on the letter that people read.
| Term | Plain meaning | In our example |
|---|---|---|
| Envelope sender (MAIL FROM) | The address servers use for delivery and for sending bounces back. People rarely see it. | [email protected], owned by the email provider |
| Header From | The From line the reader sees in the inbox. DMARC protects this one. | [email protected] |
| DNS TXT record | A short text value anyone can look up for a domain name. SPF, DKIM and DMARC all live in these. | example.com publishes three of them |
| Receiver | The mailbox provider that accepts or rejects your mail, such as Gmail or Yahoo Mail. | The user's Gmail account |
| Bounce | A message saying delivery failed. A hard bounce is permanent; a soft bounce is temporary. | 550 5.1.1 for a mailbox that does not exist |
Servers pass mail to each other with SMTP (Simple Mail Transfer Protocol). Every SMTP reply carries a three-digit code, such as 250 for accepted or 550 for refused. Keep the envelope sender and the header From apart in your head. SPF checks the first. Readers only see the second. DMARC is the rule that ties them together.
How do SPF, DKIM and DMARC work together for email deliverability?
For email deliverability, the three checks answer three questions. SPF says which servers may send for a domain. DKIM proves a domain signed the message and nobody changed it. DMARC checks that one of those passes for the From domain.
What a receiver checks when your mail arrives
Drag to exploreNotice that SPF and DKIM can each pass for some domain, but only the DMARC box asks whether that domain matches the From line. Receivers may run the SPF and DKIM checks in either order.
Diagram connections:
- Your sending server to Receiving mail server (SMTP connection)
- Receiving mail server to SPF check (Check envelope sender)
- SPF check to DKIM check (Check signature)
- DKIM check to DMARC alignment (Compare with From)
- DMARC alignment to Inbox (Yes: aligned pass)
- DMARC alignment to Apply the DMARC policy (No: fail)
- Your sending server: Sends from 192.0.2.10, signs as example.com
- Receiving mail server: Gets the message over SMTP
- SPF check: Is 192.0.2.10 in the MAIL FROM domain's list?
- DKIM check: Does the signature verify with the DNS key?
- DMARC alignment: Did SPF or DKIM pass for the From domain?
- Inbox: Then reputation decides inbox or spam
- Apply the DMARC policy: none, quarantine or reject
An everyday picture: a parcel at a gated society
Think of the security guard at a gated housing society receiving a parcel labelled with a sender's name. SPF is the list of delivery agents that sender registered with the gate. DKIM is a tamper-evident seal stamped with the sender's own mark. DMARC is the sender's standing note to the guard. It says: accept the parcel only if the agent or the seal matches the name on the label. Otherwise hold it or send it back, and send me a daily log.
Where the picture breaks: the guard checks the seal with maths, not by eye, and anyone can read the agent list because it sits in public DNS. Forwarding is like a neighbour handing the parcel on. The agent changes, so SPF fails, but the seal survives if nobody opens the box.
How does SPF decide which servers may send for your domain?
SPF is one TXT record on your domain that lists the IP addresses and services allowed to send mail for it. The receiver looks up the record for the envelope sender's domain and checks the connecting IP against it.
example.com. TXT "v=spf1 ip4:192.0.2.10 include:_spf.mailer.example.com -all"Read it left to right. ip4 allows one address. include pulls in another domain's SPF record, which is how you allow an email provider. The final all says what happens to everyone else.
| Ending | Result for an unlisted IP | What receivers do |
|---|---|---|
| -all | fail | An explicit statement that the server is not allowed. |
| ~all | softfail | Probably not allowed. RFC 7208 says receivers should not reject on this alone. |
| ?all | neutral | No opinion. The RFC says treat it exactly like having no record. |
| Two SPF records on one name | permerror | A broken setup. Publish exactly one record per name. |
SPF never looks at the From line the reader sees. RFC 7208 checks the envelope sender and the HELO name, which is the name the sending server gives when it connects. That gap is why SPF alone cannot stop someone faking your From line.
The 10-lookup limit that breaks quietly
RFC 7208 caps SPF at 10 DNS lookups. The include, a, mx, ptr and exists terms count, and so does the redirect modifier. The ip4, ip6 and all terms are free. Going over the limit gives permerror, which DMARC treats as no SPF pass.
Each include also counts the lookups inside the record it pulls in. Our test record allowed the main server with a and mx, plus three vendors whose records each held two more includes. That is 2 plus 3 times 3, which makes 11 lookups.
| Mail sent from | Lookups counted | Result |
|---|---|---|
| 192.0.2.10, the main server (matched by a) | 1 | pass |
| 192.0.2.31, the second vendor | 8 | pass |
| 192.0.2.41, the last vendor | 11 | permerror: Too many DNS lookups |
| 192.0.2.41 after swapping a and mx for ip4 | 9 | pass |
What just happened? pyspf counts lookups while it evaluates, left to right, and stops at the first match. So the broken record looked fine for your main server and failed only for the vendor listed last. RFC 7208 also evaluates mechanisms left to right and applies the limit during evaluation. So the bug hides from your main server's mail. Treat any record over 10 as broken.
The usual fix is to remove includes for services you no longer use and to replace a and mx with fixed ip4 entries. Copying a vendor's IPs straight into your record also works, but it breaks silently when the vendor changes its addresses.
How does DKIM prove a message was not changed?
DKIM adds a signature header to each message, made with a private key that only your sending system holds. The receiver fetches the matching public key from your DNS and checks the signature. If the signed headers or the body changed on the way, the check fails.
The public key lives at a name built from a selector, which is a label you choose so you can run more than one key. With selector s2026, the key sits at s2026._domainkey.example.com. The signature header names the domain in d= and the selector in s=.
DKIM signing and verifying, step by step
Drag to exploreNotice that the private key never leaves your side. The receiver only needs DNS and the message itself.
Diagram connections:
- Your app or provider to Sign headers and body
- Sign headers and body to Receiving server (Signed message)
- Receiving server to DNS TXT lookup (Fetch public key)
- DNS TXT lookup to Check the signature (Public key)
- Check the signature to dkim=pass or fail
- Your app or provider: Holds the private key for example.com
- Sign headers and body: Adds DKIM-Signature with d= and s=
- Receiving server: Reads d=example.com and s=s2026
- DNS TXT lookup: s2026._domainkey.example.com holds the key
- Check the signature: Rehash body and signed headers, compare
- dkim=pass or fail: Pass means unchanged since signing
This short script signs a message as example.com with a throwaway key, then checks it twice. It ran on dkimpy 1.1.8 with Python 3.13. A small function stands in for DNS, so it needs no network.
# pip install dkimpy==1.1.8 cryptography
import base64
import dkim
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
# 1. Make a throwaway 2048-bit key pair (never reuse it)
key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
private_pem = key.private_bytes(
serialization.Encoding.PEM,
serialization.PrivateFormat.TraditionalOpenSSL,
serialization.NoEncryption(),
)
public_der = key.public_key().public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo,
)
txt_value = "v=DKIM1; k=rsa; p=" + base64.b64encode(public_der).decode()
# 2. Pretend DNS: the verifier asks for s2026._domainkey.example.com
def fake_dns(name, timeout=5):
if name == b"s2026._domainkey.example.com.":
return txt_value.encode()
return None
message = (
b"From: [email protected]\r\n"
b"To: [email protected]\r\n"
b"Subject: Your OTP\r\n"
b"\r\n"
b"Your one-time password is 482913.\r\n"
)
# 3. Sign as example.com with selector s2026
signature = dkim.sign(
message, b"s2026", b"example.com", private_pem,
canonicalize=(b"relaxed", b"relaxed"),
include_headers=[b"from", b"to", b"subject"],
)
signed = signature + message
# 4. Verify the untouched message, then a tampered copy
print("untouched:", dkim.verify(signed, dnsfunc=fake_dns))
tampered = signed.replace(b"482913", b"111111")
print("tampered: ", dkim.verify(tampered, dnsfunc=fake_dns))Expected output: untouched: True on the first line and tampered: False on the second. Changing one six-digit OTP in the body broke the signature, because the body hash in bh= no longer matched.
What breaks a DKIM signature and what does not
| Change after signing | Result | Why |
|---|---|---|
| None | pass | Nothing changed. |
| One digit in the body | fail | The body hash no longer matches. |
| The signed Subject line | fail | Subject was listed in h=, the signed header list. |
| A new header nobody signed | pass | Only headers in h= are covered. |
| Extra spaces in Subject, relaxed mode | pass | Relaxed canonicalization ignores whitespace changes. |
| Extra spaces in Subject, simple mode | fail | Simple mode tolerates almost no change. |
| A footer added by a mailing list | fail | The body changed. |
| Public key emptied in DNS (p=) | fail | An empty p= means the key is revoked. |
Canonicalization means tidying the message the same way on both sides before hashing. RFC 6376 defaults to simple, so set c=relaxed/relaxed when you configure signing yourself. RFC 6376 also requires the From header to be signed.
Key size is set by RFC 8301, which updated DKIM in 2018. Signers must use RSA keys of at least 1024 bits and should use 2048. The older rsa-sha1 algorithm must not be used. A 2048-bit public key, with its tags, is about 410 characters in the TXT value, which is more than one 255-character DNS string can hold. DNS tools show it as two quoted strings, and verifiers join them.
How does DMARC decide whether your mail passes?
DMARC passes when SPF passes for a domain that matches the From domain, or DKIM passes for a domain that matches it. One aligned pass is enough. The domain owner publishes the rule as a TXT record at _dmarc.example.com.
_dmarc.example.com. TXT "v=DMARC1; p=none; rua=mailto:[email protected]"Matching is called alignment. Relaxed alignment, the default, accepts any name under the same organisational domain, which is the registered name such as example.com. So bounce.example.com aligns with example.com. Strict alignment needs the exact same name. You set them with aspf for SPF and adkim for DKIM.
| Setup | SPF passes for | DKIM passes for | DMARC (relaxed) | DMARC (strict) |
|---|---|---|---|---|
| A. Own server, both set up | example.com | example.com | pass | pass |
| B. Provider using only its own domains | bounce.example.net | example.net | fail | fail |
| C. Provider signs DKIM as you | bounce.example.net | example.com | pass (DKIM) | pass (DKIM) |
| D. Custom bounce subdomain, no DKIM | bounce.example.com | nothing | pass (SPF) | fail |
| E. Forwarded unchanged | nothing (SPF fails) | example.com | pass (DKIM) | pass (DKIM) |
| F. Mailing list adds a footer | nothing | nothing (body changed) | fail | fail |
Row B is the trap. Both checks pass, but for the provider's domains, so nothing is aligned and DMARC fails. This is why you set up DKIM for your own domain at the provider before you send.
What the three policies ask receivers to do
| Policy | What the domain owner is saying | When to use it |
|---|---|---|
| p=none | No preference; just send me reports. | First weeks, while you find every sender. |
| p=quarantine | Treat failing mail as suspicious, for example the spam folder. | Once reports show your real mail passes. |
| p=reject | Failing mail is not valid use of my domain. | The goal, once quarantine causes no harm. |
The rua tag gives an address for aggregate reports. These are summaries from receivers, usually daily, listing which IPs sent mail as your domain and whether each passed. They are how you find the forgotten CRM or helpdesk tool sending as you.
How do you move DMARC from p=none to p=reject safely?
You move in stages, reading reports at each one, and you only tighten the policy when every real sender passes. The steps below assume one domain and a few sending services.
List every sender
Write down each system that sends as your domain: app server, email provider, CRM, helpdesk, billing and office mail.
Publish SPF and DKIM for each
Keep SPF under 10 lookups. Ask each service to sign DKIM with your domain, not its own.
Publish p=none with rua
Collect aggregate reports for a few weeks so you see traffic you did not know about.
Fix what fails
For each failing source, either set up aligned DKIM or stop it sending as your domain.
Move to quarantine
Change p=none to p=quarantine and keep watching reports and support tickets.
Move to reject
Switch to p=reject once quarantine causes no real mail to go missing. Keep reading reports afterwards.
What changed for DMARC in 2026 with RFC 9989?
DMARC became a Standards Track specification in May 2026, when the RFC Editor published RFC 9989. It obsoletes RFC 7489, the 2015 version, which was only an Informational document, not a formal standard. Aggregate reports moved to RFC 9990 and failure reports to RFC 9991.
| Topic | RFC 7489 (2015) | RFC 9989 (2026) |
|---|---|---|
| Status | Informational | Proposed Standard |
| Gradual rollout | pct=, a percentage of mail the policy applies to | pct historic; t=y lowers the policy one step (reject acts as quarantine) |
| Finding the organisational domain | A public suffix list | A DNS tree walk, capped at eight lookups |
| Non-existent subdomains | Not covered in the base spec | np= sets a policy for subdomains that do not exist |
| Reports | In the same document | Split into RFC 9990 and RFC 9991 |
The tree walk means receivers find your organisational domain by querying _dmarc names in DNS, moving up one label at a time. RFC 9989 also says receivers must ignore tags they do not recognise, so an old pct tag in your record should not break it.
What do Gmail and Yahoo require from bulk senders in 2026?
Both ask every sender for basic authentication and ask bulk senders for SPF, DKIM, DMARC and one-click unsubscribe. Gmail counts you as a bulk sender once you send close to 5,000 messages in 24 hours to personal Gmail accounts. That status is permanent.
| Rule | Gmail | Yahoo |
|---|---|---|
| Authentication, all senders | SPF or DKIM | SPF or DKIM at a minimum |
| Authentication, bulk senders | SPF and DKIM | SPF and DKIM |
| DMARC, bulk senders | A record; p=none is enough | At least p=none, and DMARC must pass |
| From alignment, bulk senders | Aligned with SPF or DKIM domain | Aligned with SPF or DKIM domain |
| Unsubscribe | One-click (RFC 8058) for marketing and subscribed mail | One-click for marketing and subscribed mail |
| Honour unsubscribes within | 48 hours | 2 days |
| Spam complaint rate | Below 0.3%; aim below 0.1% | Below 0.3% |
| Other | Forward and reverse DNS, TLS encryption, RFC 5322 format | Forward and reverse DNS, RFC 5321 and 5322 |
Gmail says enforcement ramped up in November 2025, including temporary and permanent rejections. Its FAQ notes that alignment is not required for forwarded and mailing list mail. Gmail measures the spam rate in its Postmaster Tools dashboard.
One-click unsubscribe in two headers
RFC 8058 defines one-click unsubscribe with two headers. The List-Unsubscribe header holds one HTTPS link, such as https://example.com/u/abc123, written inside angle brackets. A second header, List-Unsubscribe-Post, carries the exact value List-Unsubscribe=One-Click.
When the reader clicks unsubscribe, the mailbox sends an HTTPS POST to that link. That POST alone should unsubscribe the user, with no extra page or login. The RFC says the request carries no cookies, your server must not redirect it, and both headers must be covered by your DKIM signature.
Why do bounces, complaints and spam traps hurt your sender reputation?
Receivers judge you by how you behave, not only by whether you pass authentication. Mailing addresses that do not exist, mail people mark as spam and hits on spam traps all say you do not manage your list.
Bounces
SMTP replies starting with 4 are temporary errors, so you retry later. Replies starting with 5 are permanent. Gmail's 550 5.1.1 means the account does not exist, and you should never mail that address again.
- Hard bounce: a 5xx reply such as 550 5.1.1. Add the address to a suppression list, a table of addresses you never send to.
- Soft bounce: a 4xx reply such as 421 4.7.0, try again later. Retry with backoff, waiting longer each time, and suppress the address if it keeps failing.
- Rate warnings: Gmail's 421 4.7.28 says it saw an unusual rate of mail from you. Slow down rather than retrying harder.
- Authentication errors: 550 5.7.26 means unauthenticated or DMARC-blocked mail. Fix DNS, not the retry logic.
Google's own advice is to reduce volume when bounces or deferrals rise, and to unsubscribe recipients who bounce several times automatically. Yahoo asks senders to remove invalid recipients promptly.
Spam traps
A spam trap is an address used to catch senders with poor list practices. Spamhaus describes several kinds. Pristine traps were never given to a real person. Seeded traps are scattered where address scrapers will find them. Dead-address traps were real addresses that were shut down and later turned into traps. Typo traps sit on look-alike domains.
Seeded traps catch senders who buy or scrape lists. Dead-address traps catch senders who keep mailing addresses that stopped working. Spamhaus advises confirmed opt-in, where the user clicks a link to confirm the address, rather than hunting for traps. Gmail and Yahoo also recommend confirming addresses before you subscribe them.
How do you warm up a new IP address or sending domain?
Warming up means starting with low volume to your most engaged users and raising it slowly, so receivers can build a picture of you. Gmail says to avoid sudden spikes if you have no history of sending large volumes.
As of 24 October 2026, neither Gmail's sender guidelines nor Yahoo's best-practices page gives a day-by-day warm-up schedule. Treat any fixed table you find as someone's opinion. A sensible rule is to raise volume only while bounces, deferrals and complaints stay low, and to pause when 4xx codes appear.
- Shared IP: your provider's IP is used by many senders. Gmail notes that everyone's behaviour on a shared IP affects its reputation.
- Dedicated IP: only you send from it, so the reputation is yours alone. It starts with no history and needs warming.
- Separate streams: Gmail suggests a different IP for each message type and the same From address within one category.
Picture our team again. Before the big campaign, they could have sent it from a marketing subdomain such as news.example.com, starting with recent buyers. The OTP stream on [email protected] would then keep its own clean history.
How do you check that SPF, DKIM and DMARC are set up correctly?
Look up the three DNS records, then read the authentication results on a real message you sent to yourself. These dig commands ran against a local test server serving made-up example.com records.
$ dig +short TXT example.com
"v=spf1 ip4:192.0.2.10 include:_spf.mailer.example.com -all"
$ dig +short TXT _dmarc.example.com
"v=DMARC1; p=none; rua=mailto:[email protected]"
$ dig +short TXT s2026._domainkey.example.com
"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEF..." "...rest of the key"Check three things in that output. There is exactly one line starting v=spf1. The DMARC line starts v=DMARC1. The DKIM key may arrive as two quoted strings, which is normal for a 2048-bit key.
Next, send a message to a mailbox you own and open its full headers. Receivers add an Authentication-Results header, defined in RFC 8601. You want to see results like this, with your own domain after each method.
Authentication-Results: mx.example.net;
spf=pass smtp.mailfrom=bounce.example.com;
dkim=pass header.d=example.com;
dmarc=pass header.from=example.comIf header.d shows your provider's domain, DKIM is not aligned. You are then relying on SPF alone, or you are in row B of the DMARC table. Fix that before you raise volume.
How do you explain email deliverability in a system design interview?
Treat it as part of a notification system: authentication in DNS, reputation in behaviour, and feedback loops in code. Interviewers want to hear that you separate mail streams and handle bounces.
Name the goal
Inbox placement for OTPs and receipts first, marketing second, measured by bounces, complaints and delivery time.
Authenticate the domain
SPF under 10 lookups, DKIM with your own domain and a 2048-bit key, DMARC from p=none towards p=reject.
Separate streams
Transactional mail on one subdomain and IP pool, marketing on another, so one cannot hurt the other.
Close the feedback loop
Process bounces and complaints from webhooks, keep a suppression list, and honour unsubscribes within 48 hours.
Protect reputation
Warm up new IPs slowly, use confirmed opt-in, and alert when complaint rates rise towards 0.1%.
Mention the trade-offs
Dedicated IPs give control but need warm-up; strict alignment is safer but breaks more third-party senders.
Questions people ask
- Do I need SPF, DKIM and DMARC, or is one enough?
- Set up all three. DMARC needs only one aligned pass, but RFC 9989 recommends using both SPF and DKIM. As of 24 October 2026, Gmail and Yahoo require both SPF and DKIM from bulk senders. DKIM matters most in practice, because it survives plain forwarding while SPF does not.
- Is a DMARC policy of p=none enough?
- It is enough to meet the Gmail and Yahoo bulk sender rules as of 24 October 2026, but it does not stop anyone spoofing your domain. Treat p=none as a monitoring stage. Read the aggregate reports, fix every legitimate sender, then move to p=quarantine and finally p=reject.
- Should my SPF record end in -all or ~all?
- Both stop SPF passing for unlisted servers, so DMARC treats them the same way. The -all ending gives a fail and ~all gives a softfail. RFC 7208 says receivers should not reject on softfail alone. You can start with ~all while you find all your senders, then switch to -all.
- Why does forwarded email fail SPF but still pass DMARC?
- A forwarder sends your message from its own IP address, which is not in your SPF record, so SPF no longer passes for your domain. If the forwarder does not change the message, your DKIM signature still verifies for your domain. That aligned DKIM pass is enough for DMARC. Mailing lists that add footers break DKIM as well.
- Should I get a dedicated IP address for sending email?
- Only when your volume is steady and large enough to build its own reputation. On a shared IP, other senders' behaviour affects you, as Gmail notes. A dedicated IP isolates you but starts with no history, so you must warm it up slowly with your most engaged users first.
- How long should a DKIM key be?
- Use a 2048-bit RSA key. RFC 8301 requires at least 1024 bits and recommends 2048, and it forbids the old rsa-sha1 algorithm. A 2048-bit public key does not fit in one 255-character DNS string, so it is published as two strings that verifiers join together.
- What is the difference between a hard bounce and a soft bounce?
- A hard bounce is a permanent failure, an SMTP reply starting with 5, such as Gmail's 550 5.1.1 for an account that does not exist. A soft bounce starts with 4 and means try later. Suppress hard bounces at once, and retry soft bounces with backoff before giving up.
Sources and further reading
- RFC 7208: Sender Policy Framework (SPF), sections 2, 3, 4.5, 4.6.4, 8 and 10.3
- RFC 6376: DomainKeys Identified Mail (DKIM) Signatures
- RFC 8301: Cryptographic Algorithm and Key Usage Update to DKIM
- RFC 7489: DMARC (2015, Informational)
- RFC 9989: DMARC (May 2026, Proposed Standard), info page
- RFC 9989 full text
- RFC 9990: DMARC Aggregate Reporting, info page
- RFC 8058: Signaling One-Click Functionality for List Email Headers
- RFC 8601: Message Header Field for Indicating Message Authentication Status
- RFC 5321: Simple Mail Transfer Protocol
- Gmail Help: Email sender guidelines
- Gmail Help: Email sender guidelines FAQ
- Google Workspace: Gmail SMTP errors and codes
- Yahoo Sender Hub: Sender best practices
- Spamhaus: Spamtraps, fix the problem not the symptom (2022)
- dkimpy 1.1.8 on PyPI (library used for the DKIM tests)
- pyspf 2.0.14 on PyPI (library used for the SPF tests)