Authenticating Senders with SPF, DKIM, and DMARC
Compute an email authenticity verdict from an inbound email's SPF/DKIM/DMARC results, then anchor it to an expected From domain with is_trusted_sender before gating any action on "this really came from our domain."
is_trusted_sender decides whether an inbound email really came from a domain you trust, not just whether it authenticated at all. Reach for it before you let an inbound message trigger a refund, a payout, a config change, or any other action a spoofed sender shouldn't be able to trigger.
Every email.received event carries the server's SPF, DKIM, and DMARC results on event.email.auth. Two functions turn those results into a decision:
validate_email_auth(event.email.auth): computes an email authenticity verdict,legit,suspicious, orunknown, with a confidence level and human-readable reasons.is_trusted_sender(email.raw, domain=...): performs domain-anchored sender trust, it anchors that verdict to an expected From domain, which is the check you actually want for authorization decisions.
Why the verdict alone isn't enough#
A legit verdict proves the email authenticated as its own From domain, not as a domain you trust, so gating on the verdict alone lets any attacker-owned authenticated domain through. validate_email_auth answers "was this email authenticated?", not "authenticated as which domain?" A fully authenticated email from any domain, including one an attacker registered five minutes ago, returns legit. If you gate a privileged action on verdict == "legit" alone, an attacker who owns their own domain and configures SPF/DKIM/DMARC correctly for it sails right through.
is_trusted_sender closes that gap by checking that the domain DMARC evaluated matches the domain you expected, and that the From header parses to a single valid address in that domain.
Never authorize based on email.reply_target or email.smtp.mail_from. Both are fully sender-controlled. The normalized email.sender is not a safe anchor either: it is parsed leniently for display and falls back to the SMTP envelope sender.
Compute the bare verdict#
Call validate_email_auth(event.email.auth) to turn the server's SPF, DKIM, and DMARC results into a verdict, a confidence level, and a list of reasons.
- 1
Call validate_email_auth with the event's auth object#
from primitive import validate_email_auth result = validate_email_auth(event.email.auth) print(result.verdict) # "legit" | "suspicious" | "unknown" print(result.confidence) # "high" | "medium" | "low" print(result.reasons) # e.g. ["DMARC passed with DKIM alignment (example.com)"] - 2
Read the verdict, but don't gate on it alone#
A
legitverdict only tells you the email authenticated as some domain. Treat it as an input to a further decision, not the decision itself, proceed tois_trusted_senderfor anything that grants access or triggers an action.
Anchor the verdict to a domain you trust#
Call is_trusted_sender(email.raw, domain="example.com") to check the verdict against an expected From domain. This is the check to use for real authorization decisions: "did this email really come from example.com?"
- 1
Import is_trusted_sender#
from primitive import is_trusted_sender - 2
Call it with the raw event and your expected domain#
trust = is_trusted_sender(email.raw, domain="example.com")email.rawis theEmailReceivedEventattached to the normalizedReceivedEmailobject, the same shapevalidate_email_authreads from. Passsender="ceo@example.com"too if you need to anchor to one exact address, not just the domain. - 3
Branch on trusted, retryable, and the fallback case#
if trust.trusted: ... # authenticated mail whose From address is @example.com elif trust.retryable: # Transient DNS failure during DMARC evaluation. Respond with a 5xx # so webhook redelivery retries this email later. ... else: print("untrusted:", trust.reason, trust.auth.reasons)
Expected result#
trust.trustedisTrueonly when all of the following hold:- the verdict is
legit - the domain DMARC evaluated equals the
domainyou passed - the From header strict-parses to a single valid address in that domain (exactly matching
sender=when you passed one)
- the verdict is
trust.reasonis a stable, machine-readable code naming the first check that failed, so you can branch on it without inspecting message text.trust.auth.reasonscarries the underlyingvalidate_email_authreasons for debugging.
An unknown verdict has two very different causes bundled together: a temporary DNS error (worth retrying) and a sender domain that publishes no DMARC record at all (permanent for that email). is_trusted_sender separates them for you via trust.retryable, check it before deciding whether to retry or reject outright.
What NOT to do#
Don't hand-roll the check: three mistakes make a homegrown sender check spoofable.
- Regexing the raw From header.
From: "trusted@example.com" <x@evil.com>puts an allowlisted address in the display name while DMARC evaluates, and can pass for,evil.com. Only a strict RFC 5322 parse catches this. - Trusting
email.reply_targetoremail.smtp.mail_from. Both are sender-controlled and carry no authentication guarantee. - Trusting the normalized
email.sender. It's parsed leniently for display and falls back to the SMTP envelope sender when the From header doesn't parse cleanly, so it is not a safe authorization anchor.
The same helper exists with identical semantics in the other SDKs: isTrustedSender in the Node.js SDK and IsTrustedSender in the Go SDK. Pick the one for your language; the trust model is identical.
Next steps#
See where email.raw and the auth object come from on the normalized ReceivedEmail shape.
Verifying Webhook SignaturesVerify the webhook delivery itself is authentic before you even get to sender trust.
Python SDK Error ReferenceLook up the error raised when validate_email_auth receives a malformed auth object.
Inbound and Outbound Email ModelUnderstand the full ReceivedEmail shape and where the auth field fits.
Was this page helpful?