---
title: "Verifying Inbound Email Authenticity"
canonical: "https://test.abhinandan.one/node-sdk-email-authenticity"
markdown_url: "https://test.abhinandan.one/node-sdk-email-authenticity.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Node.js SDK"
description: "isTrustedSender anchors an SPF/DKIM/DMARC verdict to an expected From domain, returning trusted, retryable, and a stable reason code for gating actions."
keywords: ["validateEmailAuth", "isTrustedSender", "domain-anchored sender trust", "email authenticity verdict", "TrustedSenderOptions", "event.email.auth"]
last_modified: "2026-08-11T18:55:04.212052+00:00"
published_at: "2026-08-11T18:55:04.036785+00:00"
source_files:
  - "sdk-node/README.md"
  - "sdk-node/src/parser/address-parser.ts"
sections:
  - {anchor: "why-the-raw-verdict-isnt-enough", title: "Why the raw verdict isn't enough"}
  - {anchor: "compute-the-bare-verdict", title: "Compute the bare verdict"}
  - {anchor: "anchor-the-verdict-to-a-domain", title: "Anchor the verdict to a domain"}
  - {anchor: "step-import-istrustedsender", title: "Import isTrustedSender"}
  - {anchor: "step-call-it-with-the-raw-event-and-the-expected-domain", title: "Call it with the raw event and the expected domain"}
  - {anchor: "step-branch-on-the-result", title: "Branch on the result"}
  - {anchor: "handle-the-retryable-case-correctly", title: "Handle the retryable case correctly"}
  - {anchor: "dont-build-your-own-check-from-the-raw-from-header", title: "Don't build your own check from the raw From header"}
  - {anchor: "full-example-gating-a-reply-on-sender-trust", title: "Full example: gating a reply on sender trust"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Verifying Inbound Email Authenticity

Use validateEmailAuth and isTrustedSender to turn an inbound email's SPF/DKIM/DMARC results into a trust decision, anchored to the domain you expect the mail to come from.

`validateEmailAuth` and `isTrustedSender`, both exported from `@primitivedotdev/sdk/api`, turn the SPF, DKIM, and DMARC results Primitive already computed into a decision about whether an inbound email really came from the domain you expect. Use them before an inbound email triggers a privileged action such as issuing a refund, granting access, or replying with sensitive data.

If you just need the normalized email object, see [Receiving Inbound Email](https://test.abhinandan.one/node-sdk-receiving-email.md).

## Why the raw verdict isn't enough

The bare verdict says whether an email authenticated at all, not which domain it authenticated as, so it can't answer "did this come from *our* domain?" on its own. Every `email.received` event carries the server's SPF, DKIM, and DMARC results on `event.email.auth`, and `validateEmailAuth(event.email.auth)` turns them into an **email authenticity verdict**: `legit`, `suspicious`, or `unknown`, with a confidence level and human-readable reasons.

A fully authenticated email from *any* domain, including one an attacker registered, returns `legit`. If your handler only checks the verdict, an attacker sending fully authenticated mail from a domain they control passes the check just as easily as your real partner does.

That's what **domain-anchored sender trust** (`isTrustedSender`) is for: it anchors the verdict to an expected From domain.

> **Tip:** If you only need to know whether an email is spam-like or forged in general (no specific domain to check against), `validateEmailAuth` alone is enough. Reach for `isTrustedSender` the moment you're about to say "because this came from *our* domain."

## Compute the bare verdict

Call `validateEmailAuth` with the auth block off the raw event; it returns a verdict, a confidence level, and the reasons behind them.

```typescript
import { validateEmailAuth } from "@primitivedotdev/sdk/api";

const result = validateEmailAuth(email.raw.email.auth);

console.log(result.verdict); // "legit" | "suspicious" | "unknown"
console.log(result.confidence);
console.log(result.reasons);
```

Both `validateEmailAuth` and `isTrustedSender` are importable from `@primitivedotdev/sdk/api` (not just the root import), so they also work inside Primitive Functions, which import from the Workers-safe `/api` subpath.

## Anchor the verdict to a domain

Call `isTrustedSender(event, { domain, sender? })` with the raw `email.received` event and the domain you expect the From address to belong to.

### 1. Import isTrustedSender

```typescript
import { isTrustedSender } from "@primitivedotdev/sdk/api";
```

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

Pass `email.raw` (the raw `EmailReceivedEvent`, not the normalized `ReceivedEmail`) and the domain you expect the sender to belong to:

```typescript
const trust = isTrustedSender(email.raw, { domain: "example.com" });
```

Optionally pass `sender` to require an exact address match, not just the domain:

```typescript
const trust = isTrustedSender(email.raw, {
  domain: "example.com",
  sender: "billing@example.com",
});
```

### 3. Branch on the result

```typescript
if (trust.trusted) {
  // Authenticated mail whose From address is @example.com
  // (and, if you passed `sender`, exactly matches it)
} else if (trust.retryable) {
  // Transient DNS failure during DMARC evaluation. Respond with a 5xx
  // so webhook redelivery retries this email later.
} else {
  console.warn("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 `domain`
- the From header strict-parses to a single valid address in `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 parsing message text.

## Handle the retryable case correctly

An `unknown` verdict can be temporary or permanent, so branch on `trust.retryable` before deciding whether to reject the email. `unknown` has two very different causes, and `isTrustedSender` separates them for you via `trust.retryable`:

| Cause | `retryable` | What it means |
|---|---|---|
| Transient DNS failure while evaluating DMARC | `true` | Retry later, the same email might resolve to `legit` or `suspicious` on redelivery |
| Sender domain publishes no DMARC record | `false` | Permanent for this email, retrying won't change the outcome |

> **Warning:** If `trust.retryable` is `true`, respond to the webhook with a 5xx status instead of accepting it. Primitive's webhook redelivery will retry the email later, and by then the DNS failure may have cleared. Treating a retryable failure as a hard rejection can permanently drop mail that would have authenticated cleanly.

## Don't build your own check from the raw From header

Hand-rolled From-header checks are unsafe, because the header can carry an allowlisted address in its display name and the fields that look sender-identifying are attacker-controlled. If you're tempted to skip `isTrustedSender` and regex the From header yourself, three things will bite you:

- **Display-name spoofing**: `From: "trusted@example.com" <x@evil.com>` puts an allowlisted address in the display name while DMARC evaluates, and can pass, for `evil.com`. A naive regex over the header text finds `trusted@example.com` and gets it wrong.
- **Sender-controlled fields are not authorization anchors**: `email.replyTarget` and `email.smtp.mail_from` are both fully sender-controlled. Never gate a decision on them.
- **The normalized `email.sender` is for display only**: it's parsed leniently and falls back to the SMTP envelope sender when the From header doesn't strict-parse, which is exactly the behavior you don't want for a security decision.

`isTrustedSender` requires the From header to strict-parse to a single valid address in the expected domain, so an ambiguous header fails the check instead of being guessed at. The SDK exports the same strict parser directly as [`parseFromHeader`](https://test.abhinandan.one/node-sdk-parsing-email/node-sdk-address-parsing.md), which rejects multi-address and group-syntax headers outright.

> **Note:** The same helper exists with identical semantics in the Python SDK (`is_trusted_sender`, see [Authenticating Senders with SPF, DKIM, and DMARC](https://test.abhinandan.one/python-sender-trust.md)) and the Go SDK (`IsTrustedSender`, see [Validating Email Authenticity](https://test.abhinandan.one/go-validating-email-authenticity.md)).

## Full example: gating a reply on sender trust

This Next.js route handler receives an inbound email, retries on a transient DMARC DNS failure, skips untrusted senders, and replies only to authenticated mail from `example.com`.

```typescript
import primitive from "@primitivedotdev/sdk";
import { isTrustedSender } from "@primitivedotdev/sdk/api";

export const runtime = "nodejs";
export const maxDuration = 300;

const client = primitive.client({
  apiKey: process.env.PRIMITIVE_API_KEY!,
});

export async function POST(req: Request) {
  const email = await primitive.receive(req, {
    secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
  });

  const trust = isTrustedSender(email.raw, { domain: "example.com" });

  if (trust.retryable) {
    // Ask Primitive to retry delivery later.
    return new Response("temporary auth failure", { status: 503 });
  }

  if (!trust.trusted) {
    console.warn("Rejecting untrusted sender:", trust.reason);
    return Response.json({ ok: true, skipped: true });
  }

  await client.reply(email, "Thank you for your email.");
  return Response.json({ ok: true });
}
```
