Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /node-sdk-email-authenticity.md.
Verified · 8/11/2026

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.

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.

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. 1

    Import isTrustedSender#

    import { isTrustedSender } from "@primitivedotdev/sdk/api";
    
  2. 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:

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

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

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

    Branch on the result#

    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:

CauseretryableWhat it means
Transient DNS failure while evaluating DMARCtrueRetry later, the same email might resolve to legit or suspicious on redelivery
Sender domain publishes no DMARC recordfalsePermanent 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, 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) and the Go SDK (IsTrustedSender, see Validating Email Authenticity).

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.

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 });
}

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer