Add-on — DNS Integrity & Deliverability

Read-only mail-DNS health scoring (MX/SPF/DMARC/DKIM with RFC 7208 lookup-cost) and domain-auth verification for email onboarding, done over node:dns with no third-party DoH.

Add-on — DNS Integrity & Deliverability

Two read-only mail-DNS capabilities absorbed from usermails: a deliverability health score and an onboarding verifier that confirms a customer actually published the DKIM/SPF/DMARC records the email add-on generated. Both are resolver-injectable and use the system resolver via node:dns — usermails' Cloudflare DoH and all vendor identity were dropped.

  • id: dns-integrity · entitlement: dns_integrity — granted at team+.
  • Code: lib/addons/dns-integrity/. Import from @/lib/addons/dns-integrity.
  • Reuses: the backbone's lib/dns/resolver.ts and the email add-on (the records it verifies).
  • Read-only: nothing here mutates DNS. Managed publishing (usermails' Cloudflare dns.ts) is a deliberately deferred slice.
import { analyzeMailHealth, verifyDomainAuth } from '@/lib/addons/dns-integrity'

const health = await analyzeMailHealth('acme.com')          // { score, risks, mx, spf, dmarc, dkim, senders }
const v = await verifyDomainAuth('acme.com', { selector: 'hs', dkimPublicKey, spfInclude: 'mail.hostssh.com' })
// v.overall === 'verified' once DKIM + SPF are live

The resolver seam

lookup.ts defines the injectable DnsLookup interface — mx(name), txt(name), cname(name) — with a default systemLookup(timeoutMs = 5000) backed by node:dns through lib/dns/resolver.ts. It soft-errors "no record" (ENODATA / ENOTFOUND) to [], normalizes MX hosts (lowercased, trailing dot stripped, sorted by priority), and joins chunked TXT strings. The seam makes the whole add-on unit-testable offline.

Health score

analyzeMailHealth(domain, { lookup?, dkimSelectors? }) (health.ts) fetches MX/SPF/DMARC/DKIM and returns a MailHealth (see types.ts) — a 0–100 score plus an RFC-aware risk catalog, the parsed records, and detected sending platforms. Default DKIM selectors probed: hs, default, google, k1, s1, selector1, selector2 (hs — ours — first).

The score starts at 100 and deducts per risk (final clamped to 0–100):

Risk (title)LevelDeduction
No MX recordswarn−5
No SPF recorderror−30
Multiple SPF recordserror−30
SPF over the 10-lookup limiterror−25
SPF near the lookup limit (8–10)warn−8
SPF +all (open)error−20
No DMARC recordwarn−15
DMARC p=none (monitor only)warn−5
No DKIM selector foundwarn−10

The SPF lookup cost is computed by spfLookupCost(spf, resolveTxt, seen?, depth?) (spf.ts) — a real RFC 7208 recursion: include:, redirect=, a, mx, ptr, exists: each cost one lookup, nested SPF records are fetched through the injected resolver, visited domains are tracked to avoid loops, and recursion stops past depth 10 (the RFC's 10-lookup limit is what the "over the limit" risk keys off).

detectSenders(haystack) matches the SPF text + MX hosts against the SENDER_SIGNS catalog to name the sending platforms (Google Workspace, Microsoft 365, Amazon SES, SendGrid, Mailgun, Resend, Postmark, Mailchimp/Mandrill, ElasticEmail, Proofpoint, Mimecast) — for reporting only.

Onboarding verify

verifyDomainAuth(domain, expected, { lookup? }) (verify.ts) confirms a customer published the records we issued (email Phase 6 — domain onboarding). expected is a DomainAuthExpectation { selector, dkimPublicKey, spfInclude? }. It returns a DomainAuthVerification with a VerifyStatus (verified | pending | failed) per record and an overall:

  • DKIM — reads <selector>._domainkey.<domain> TXT; the expected public key must match (substring).
  • SPF — reads root TXT; requires exactly one v=spf1 record; if spfInclude is given it must be present (case-insensitive).
  • DMARC — reads _dmarc.<domain> TXT; v=DMARC1 present ⇒ verified, else pending. DMARC never fails onboarding.
  • overallverified only when DKIM and SPF both pass; failed if either failed; otherwise pending. Human-readable reasons accumulate in details[].

Wiring & routes

There is no route at /v1/dns-integrity, and no monitor type for it — both capabilities are consumed by the email add-on's HTTP API:

  • analyzeMailHealth backs GET /api/v1/email/domains/{id}/health (the deliverability score). It is not registered as a schedulable monitor type (lib/dns/monitors/types.ts has no mail-health entry, and the monitors runner never imports it).
  • verifyDomainAuth backs POST /api/v1/email/domains/{id}/verify (domain onboarding).

Those routes authenticate with the email add-on's hs_ API key (authenticateApiKey), not a dns_integrity check; the dns_integrity entitlement governs whether the add-on is enabled in the registry, not the route calls. For the broader DNS/monitors surface see DNS Tools.

Honest status notes

  • Read-only, no DB changes in this slice — nothing is persisted or mutated.
  • Dropped on absorb: Cloudflare DoH, the Cloudflare publishing automation (dns.ts), mailboxReceivingStatus (usermails-mailbox specific), the usermails SPF self-entry, and all vendor DKIM selectors / identity.