Validating Email Authenticity
Compute an email authenticity verdict from SPF, DKIM, and DMARC results with ValidateEmailAuth, then anchor authorization decisions to an expected From domain with IsTrustedSender.
Use primitive.ValidateEmailAuth when you need to know whether an inbound email's SPF, DKIM, and DMARC results add up to a trustworthy authentication outcome. Use primitive.IsTrustedSender when the decision is really "did this really come from example.com", the check most handlers actually need before taking an authorized action like approving a payment or granting access.
ValidateEmailAuth alone does not tell you which domain authenticated. A fully authenticated email from any domain, including one an attacker registered, returns legit. Never gate an authorization decision on the bare verdict; anchor it with IsTrustedSender instead.
Compute the raw authenticity verdict#
Every email.received event carries the server's SPF, DKIM, and DMARC results on event.Email.Auth. ValidateEmailAuth turns those into an overall email authenticity verdict: legit, suspicious, or unknown, with a confidence level and human-readable reasons.
package main
import (
"log"
primitive "github.com/primitivedotdev/sdks/sdk-go"
)
func handleEvent(event primitive.EmailReceivedEvent) {
result, err := primitive.ValidateEmailAuth(event.Email.Auth)
if err != nil {
log.Printf("invalid auth input: %v", err)
return
}
switch result.Verdict {
case primitive.AuthVerdictLegit:
log.Printf("legit (%s confidence): %v", result.Confidence, result.Reasons)
case primitive.AuthVerdictSuspicious:
log.Printf("suspicious (%s confidence): %v", result.Confidence, result.Reasons)
default:
log.Printf("unknown (%s confidence): %v", result.Confidence, result.Reasons)
}
}
ValidateEmailAuth accepts any value that decodes into the EmailAuth shape and returns a *WebhookValidationError if it doesn't. On success you get a ValidateEmailAuthResult:
| Field | Type | Meaning |
|---|---|---|
Verdict | AuthVerdict | AuthVerdictLegit, AuthVerdictSuspicious, or AuthVerdictUnknown |
Confidence | AuthConfidence | AuthConfidenceHigh, AuthConfidenceMedium, or AuthConfidenceLow |
Reasons | []string | Human-readable explanation, ordered most-significant first |
The verdict logic, in order:
- DMARC
temperror/permerror→unknown, low confidence: a DNS or policy lookup failure means authenticity can't be determined. - DMARC
pass→legit. Confidence ishighwhen DKIM aligned (and no weak DKIM key was used),mediumwhen only SPF aligned or when DKIM used a weak key (<1024 bits). - DMARC
fail→suspicious. Confidence ishighunder arejectorquarantinepolicy,mediumwhen SPF also failed undernonepolicy,lowotherwise. - DMARC
none(no record published) →unknownunless SPF hard-fails, in which case it'ssuspicious,mediumconfidence.
Anchor the verdict to a domain with IsTrustedSender#
IsTrustedSender is the domain-anchored sender trust check: it answers "did this authenticate as example.com," not just "did this authenticate." Use it whenever a handler is about to do something consequential, approve a payment, grant access, auto-reply with sensitive data, based on who the email is from.
- 1
Get the raw email.received event#
IsTrustedSendertakes the rawemail.receivedpayload (event.Rawon aReceivedEmail, or theEmailReceivedEventyou get fromprimitive.HandleWebhook), not the normalizedReceivedEmailobject.email, err := primitive.Receive(primitive.HandleWebhookOptions{ Body: body, Headers: headers, Secret: "whsec_...", }) if err != nil { log.Printf("invalid webhook: %v", err) return } - 2
Call IsTrustedSender with the expected domain#
trust, err := primitive.IsTrustedSender(email.Raw, primitive.TrustedSenderOptions{ Domain: "example.com", }) if err != nil { // invalid TrustedSenderOptions (e.g. empty Domain) log.Printf("bad trust options: %v", err) return }Pass
Sendertoo when the email must come from one exact address, not just the domain:trust, err := primitive.IsTrustedSender(email.Raw, primitive.TrustedSenderOptions{ Domain: "example.com", Sender: "billing@example.com", }) - 3
Branch on Trusted, Retryable, and Reason#
switch { case trust.Trusted: // Authenticated mail whose From address is @example.com // (and matches Sender exactly, when given). approvePayment() case trust.Retryable: // Transient DNS failure during DMARC evaluation. Respond with a // 5xx so webhook redelivery retries this email later. respondServerError() default: log.Printf("untrusted: %s %v", trust.Reason, trust.Auth.Reasons) rejectSilently() }Trustedistrueonly when all of the following hold:- The underlying verdict is
legit. - The domain DMARC evaluated equals
Domain. - The From header strict-parses to a single valid address in
Domain(exactly matchingSender, when given).
Reasonnames the first check that failed, andtrust.Auth.Reasonscarries the underlying verdict's human-readable explanation.Retryableseparates two very different causes: a temporary DNS error during DMARC evaluation (worth retrying later) from a sender domain that publishes no DMARC record (permanent for that email). - The underlying verdict is
Expected result#
A trusted, DMARC-aligned email from example.com produces:
legit (high confidence): [DMARC passed with DKIM alignment (example.com)]
and IsTrustedSender with Domain: "example.com" returns trust.Trusted == true.
An email from an unrelated domain, even if fully authenticated as its own domain, produces AuthVerdictLegit from ValidateEmailAuth but trust.Trusted == false from IsTrustedSender, proof that the two checks answer different questions.
Do not build your own version of this check by 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. Use IsTrustedSender, which strict-parses the header instead.
Never authorize based on email.ReplyTarget or email.Raw.Email.SMTP.MailFrom, both are fully sender-controlled. The normalized email.Sender on a ReceivedEmail is parsed leniently for display and falls back to the SMTP envelope sender, so it is not a safe authorization anchor either. IsTrustedSender is the one call built to be safe here.
The same two-check model (a bare verdict plus a domain-anchored trust check) is identical across SDKs: validateEmailAuth / isTrustedSender in Node, validate_email_auth / is_trusted_sender in Python.
Next steps#
Learn the normalized ReceivedEmail object and where email.Raw / event.Auth come from.
Receiving and Verifying WebhooksSee how Receive and ReceiveFromHTTPRequest verify signatures and normalize inbound mail before you validate auth.
Webhook Event TypesUnderstand the email, payment, and interaction event families this auth check applies within.
Error HandlingLook up WebhookValidationError and the other error types ValidateEmailAuth and IsTrustedSender can return.
Was this page helpful?