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 atteam+. - Code:
lib/addons/dns-integrity/. Import from@/lib/addons/dns-integrity. - Reuses: the backbone's
lib/dns/resolver.tsand theemailadd-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) | Level | Deduction |
|---|---|---|
| No MX records | warn | −5 |
| No SPF record | error | −30 |
| Multiple SPF records | error | −30 |
| SPF over the 10-lookup limit | error | −25 |
| SPF near the lookup limit (8–10) | warn | −8 |
SPF +all (open) | error | −20 |
| No DMARC record | warn | −15 |
DMARC p=none (monitor only) | warn | −5 |
| No DKIM selector found | warn | −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=spf1record; ifspfIncludeis given it must be present (case-insensitive). - DMARC — reads
_dmarc.<domain>TXT;v=DMARC1present ⇒verified, elsepending. DMARC never fails onboarding. - overall —
verifiedonly when DKIM and SPF both pass;failedif either failed; otherwisepending. Human-readable reasons accumulate indetails[].
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:
analyzeMailHealthbacksGET /api/v1/email/domains/{id}/health(the deliverability score). It is not registered as a schedulable monitor type (lib/dns/monitors/types.tshas no mail-health entry, and the monitors runner never imports it).verifyDomainAuthbacksPOST /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.