---
title: "Authenticating Senders with SPF, DKIM, and DMARC"
canonical: "https://test.abhinandan.one/python-sender-trust"
markdown_url: "https://test.abhinandan.one/python-sender-trust.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Python SDK"
description: "is_trusted_sender anchors a validate_email_auth verdict to an expected From domain, letting Python SDK handlers gate actions only on authenticated senders."
keywords: ["is_trusted_sender", "validate_email_auth", "domain-anchored sender trust", "email authenticity verdict", "retryable DMARC", "primitive python sdk auth"]
last_modified: "2026-08-21T18:22:43.359885+00:00"
published_at: "2026-08-11T18:55:04.356814+00:00"
source_files:
  - "sdk-python/README.md"
  - "sdk-python/src/primitive/webhook.py"
sections:
  - {anchor: "why-the-verdict-alone-isnt-enough", title: "Why the verdict alone isn't enough"}
  - {anchor: "compute-the-bare-verdict", title: "Compute the bare verdict"}
  - {anchor: "step-call-validate_email_auth-with-the-events-auth-object", title: "Call validate_email_auth with the event's auth object"}
  - {anchor: "step-read-the-verdict-but-dont-gate-on-it-alone", title: "Read the verdict, but don't gate on it alone"}
  - {anchor: "anchor-the-verdict-to-a-domain-you-trust", title: "Anchor the verdict to a domain you trust"}
  - {anchor: "step-import-is_trusted_sender", title: "Import is_trusted_sender"}
  - {anchor: "step-call-it-with-the-raw-event-and-your-expected-domain", title: "Call it with the raw event and your expected domain"}
  - {anchor: "step-branch-on-trusted-retryable-and-the-fallback-case", title: "Branch on trusted, retryable, and the fallback case"}
  - {anchor: "expected-result", title: "Expected result"}
  - {anchor: "what-not-to-do", title: "What NOT to do"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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](https://test.abhinandan.one/email-model.md) 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`, or `unknown`, 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.

> **Warning:** 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

```python
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 `legit` verdict only tells you the email authenticated as *some* domain. Treat it as an input to a further decision, not the decision itself, proceed to `is_trusted_sender` for 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

```python
from primitive import is_trusted_sender
```

### 2. Call it with the raw event and your expected domain

```python
trust = is_trusted_sender(email.raw, domain="example.com")
```

`email.raw` is the [`EmailReceivedEvent`](https://test.abhinandan.one/email-model.md) attached to the normalized [`ReceivedEmail`](https://test.abhinandan.one/email-model.md) object, the same shape `validate_email_auth` reads from. Pass `sender="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

```python
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.trusted` is `True` only when **all** of the following hold:
  - the verdict is `legit`
  - the domain DMARC evaluated equals the `domain` you passed
  - the From header strict-parses to a single valid address in that domain (exactly matching `sender=` when you passed one)
- `trust.reason` is a stable, machine-readable code naming the first check that failed, so you can branch on it without inspecting message text.
- `trust.auth.reasons` carries the underlying `validate_email_auth` reasons for debugging.

> **Tip:** 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_target` or `email.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.

> **Note:** The same helper exists with identical semantics in the other SDKs: `isTrustedSender` in the [Node.js SDK](https://test.abhinandan.one/node-sdk-email-authenticity.md) and `IsTrustedSender` in the [Go SDK](https://test.abhinandan.one/go-validating-email-authenticity.md). Pick the one for your language; the trust model is identical.
