{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/node-sdk-email-authenticity","markdown_url":"https://test.abhinandan.one/node-sdk-email-authenticity.md","article":{"id":"6c9985d1-9d1b-4ec0-b904-0805dbcfd925","article_slug":"node-sdk-email-authenticity","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:55:04.036785+00:00","keywords":["validateEmailAuth","isTrustedSender","domain-anchored sender trust","email authenticity verdict","TrustedSenderOptions","event.email.auth"],"meta_description":"isTrustedSender anchors an SPF/DKIM/DMARC verdict to an expected From domain, returning trusted, retryable, and a stable reason code for gating actions.","og_image_url":null,"source_file_paths":["sdk-node/README.md","sdk-node/src/parser/address-parser.ts"],"recording_id":null,"replayable":false,"task_name":"Verifying Inbound Email Authenticity","category":"Node.js SDK","summary":null,"description":"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.","content_kind":"repo_page","content_markdown":"`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.\n\nIf you just need the normalized email object, see [Receiving Inbound Email](node-sdk-receiving-email).\n\n## Why the raw verdict isn't enough\n\nThe 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.\n\nA 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.\n\nThat's what **domain-anchored sender trust** (`isTrustedSender`) is for: it anchors the verdict to an expected From domain.\n\n<Tip>\n\nIf 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.\"\n\n</Tip>\n\n## Compute the bare verdict\n\nCall `validateEmailAuth` with the auth block off the raw event; it returns a verdict, a confidence level, and the reasons behind them.\n\n```typescript\nimport { validateEmailAuth } from \"@primitivedotdev/sdk/api\";\n\nconst result = validateEmailAuth(email.raw.email.auth);\n\nconsole.log(result.verdict); // \"legit\" | \"suspicious\" | \"unknown\"\nconsole.log(result.confidence);\nconsole.log(result.reasons);\n```\n\nBoth `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.\n\n## Anchor the verdict to a domain\n\nCall `isTrustedSender(event, { domain, sender? })` with the raw `email.received` event and the domain you expect the From address to belong to.\n\n<Steps>\n\n<Step title=\"Import isTrustedSender\">\n\n```typescript\nimport { isTrustedSender } from \"@primitivedotdev/sdk/api\";\n```\n\n</Step>\n\n<Step title=\"Call it with the raw event and the expected domain\">\n\nPass `email.raw` (the raw `EmailReceivedEvent`, not the normalized `ReceivedEmail`) and the domain you expect the sender to belong to:\n\n```typescript\nconst trust = isTrustedSender(email.raw, { domain: \"example.com\" });\n```\n\nOptionally pass `sender` to require an exact address match, not just the domain:\n\n```typescript\nconst trust = isTrustedSender(email.raw, {\n  domain: \"example.com\",\n  sender: \"billing@example.com\",\n});\n```\n\n</Step>\n\n<Step title=\"Branch on the result\">\n\n```typescript\nif (trust.trusted) {\n  // Authenticated mail whose From address is @example.com\n  // (and, if you passed `sender`, exactly matches it)\n} else if (trust.retryable) {\n  // Transient DNS failure during DMARC evaluation. Respond with a 5xx\n  // so webhook redelivery retries this email later.\n} else {\n  console.warn(\"Untrusted:\", trust.reason, trust.auth.reasons);\n}\n```\n\n**Expected result:** `trust.trusted` is `true` only when all of the following hold:\n\n- the verdict is `legit`\n- the domain DMARC evaluated equals `domain`\n- the From header strict-parses to a single valid address in `domain` (exactly matching `sender` when you passed one)\n\n`trust.reason` is a stable, machine-readable code naming the first check that failed, so you can branch on it without parsing message text.\n\n</Step>\n\n</Steps>\n\n## Handle the retryable case correctly\n\nAn `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`:\n\n| Cause | `retryable` | What it means |\n|---|---|---|\n| Transient DNS failure while evaluating DMARC | `true` | Retry later, the same email might resolve to `legit` or `suspicious` on redelivery |\n| Sender domain publishes no DMARC record | `false` | Permanent for this email, retrying won't change the outcome |\n\n<Warning>\n\nIf `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.\n\n</Warning>\n\n## Don't build your own check from the raw From header\n\nHand-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:\n\n- **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.\n- **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.\n- **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.\n\n`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`](node-sdk-address-parsing), which rejects multi-address and group-syntax headers outright.\n\n<Note>\n\nThe same helper exists with identical semantics in the Python SDK (`is_trusted_sender`, see [Authenticating Senders with SPF, DKIM, and DMARC](python-sender-trust)) and the Go SDK (`IsTrustedSender`, see [Validating Email Authenticity](go-validating-email-authenticity)).\n\n</Note>\n\n## Full example: gating a reply on sender trust\n\nThis 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`.\n\n```typescript\nimport primitive from \"@primitivedotdev/sdk\";\nimport { isTrustedSender } from \"@primitivedotdev/sdk/api\";\n\nexport const runtime = \"nodejs\";\nexport const maxDuration = 300;\n\nconst client = primitive.client({\n  apiKey: process.env.PRIMITIVE_API_KEY!,\n});\n\nexport async function POST(req: Request) {\n  const email = await primitive.receive(req, {\n    secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,\n  });\n\n  const trust = isTrustedSender(email.raw, { domain: \"example.com\" });\n\n  if (trust.retryable) {\n    // Ask Primitive to retry delivery later.\n    return new Response(\"temporary auth failure\", { status: 503 });\n  }\n\n  if (!trust.trusted) {\n    console.warn(\"Rejecting untrusted sender:\", trust.reason);\n    return Response.json({ ok: true, skipped: true });\n  }\n\n  await client.reply(email, \"Thank you for your email.\");\n  return Response.json({ ok: true });\n}\n```\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Receiving Inbound Email\" href=\"node-sdk-receiving-email\">\n\nLearn the full ReceivedEmail shape that isTrustedSender's raw event comes from.\n\n</Card>\n\n<Card title=\"Webhook Signature Verification\" href=\"node-sdk-webhook-signing\">\n\nVerify the Primitive-Signature HMAC header before you even get to auth verdicts.\n\n</Card>\n\n<Card title=\"Raw MIME Address Parsing (parser/address)\" href=\"node-sdk-address-parsing\">\n\nSee the strict From-header parser isTrustedSender relies on internally.\n\n</Card>\n\n<Card title=\"Handling Payment and Interaction Webhook Events\" href=\"node-sdk-webhook-events\">\n\nBranch on payment and interaction events from the same webhook endpoint.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-node/README.md","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Verifying+Inbound+Email+Authenticity&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-email-authenticity","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}