Provider credentials are now sealed at rest using a configurable secret
management layer. Credentials are wrapped in a self-describing SecretEnvelope
(version, method, nonce, ciphertext) before being stored, enabling transparent
key rotation and multiple backends.
The BIMI draft lets a domain explicitly opt out of BIMI by publishing
a record with v=BIMI1 and an empty l= tag. Surface that in the editor
and treat it as a first-class case in compliance.
Validators run synchronously and surface:
- Owner-name shape: <selector>._bimi (rejects empty selectors and
non-label characters).
- Version: only "BIMI1" is accepted by the current draft.
- Logo URL: l= is mandatory, must be HTTPS, warns when the path does
not end in .svg.
- VMC URL: optional but flagged as info when missing (Gmail and Yahoo
need it). Must be HTTPS when present; info if it does not look like
a .pem file.
- Evidence URL: must be HTTPS when present.
- Cross-record DMARC check: warns when no DMARC is published or when
every DMARC at the apex sits at p=none, since BIMI is only honoured
with an enforcing DMARC policy.
Adds support for Brand Indicators for Message Identification (BIMI),
the emerging standard that lets receiving mail clients display verified
brand logos next to authenticated messages.
Surfaces the RFC 7489 sec. 7.1 trap: when a DMARC record at example.com
publishes rua/ruf addresses on a third-party domain, that domain must
publish "v=DMARC1" at example.com._report._dmarc.<external> to authorize
report delivery. Without it, reports are silently dropped, which is by
far the most common cause of "DMARC is published but no report ever
arrives".
Adds a backend endpoint that resolves the RFC 7489 sec. 7.1 cross-domain
reporting authorization record, paired with the (incoming) DMARC async
validator on the front-end.
When a DMARC record at example.com publishes rua=mailto:dmarc@third.tld,
third-party receivers MUST refuse to deliver reports unless
example.com._report._dmarc.third.tld publishes a v=DMARC1 TXT. This is
the silent failure mode behind most "we configured DMARC but never see
any report" tickets.
Adds zone-aware checks to the MTA-STS async validator. Once the policy
file is fetched and parsed, compare its mx patterns to the apex MX
records of the current zone (RFC 8461 sec. 4.1):
- mta_sts.zone-no-mx (warning): the policy lists mx entries but the
zone has no MX records, so receivers will refuse delivery.
- mta_sts.zone-mx-not-covered (error in enforce, warning in testing):
one of the apex MX hosts is not matched by any policy pattern.
Senders enforcing the policy will reject mail to that host.
- mta_sts.policy-mx-unused (info): a policy pattern matches no MX in
the zone, hinting at a stale entry.
Validates a MX record set (svcs.MXs) at edit time:
- Null MX (RFC 7505): a "." target must be the only MX in the set, with
preference 0. Both deviations are surfaced.
- Targets: invalid hostnames, out-of-range preferences (uint16) and
duplicate targets (case-insensitive on the FQDN).
- Cross-zone: flags MX targets that are CNAME owners in the same zone
(RFC 5321 sec. 5.1) and warns when an in-zone target lacks any
A/AAAA service. External targets are left to runtime checkers.
Extends the compliance context with findAllServices(type?) so a
validator can iterate every service in the zone, not just a single
subdomain. The DMARC validator now uses it to flag configurations
where alignment is structurally impossible:
- p=quarantine|reject and the zone has neither a DKIM nor an SPF
record -> error: every legitimate message will fail DMARC.
- p=none in the same situation -> warning: DMARC has nothing to
align against, monitoring data will be empty.
- adkim=s (strict DKIM alignment) with no DKIM record published
anywhere in the zone -> warning: only SPF alignment can succeed.
Wires the new POST /api/resolver/mta-sts-policy endpoint into the
MTA-STS validator. The async pass runs after the local TXT checks,
debounced and cancellable through EditorCompliance, and surfaces:
- Transport-level failures: dns-error, tls-error, fetch-error,
too-large.
- HTTP-level failures: not-found (404), http-error (other non-2xx),
redirect (server tried to redirect, RFC 8461 sec. 3.3 forbids it).
- Policy file content: missing/invalid version, missing/invalid mode,
mode=none (warning, effectively disabled), mode=testing (info),
missing mx in enforce/testing modes, missing/out-of-range max_age
(0..31557600), short max_age (< 1 day, warning).
Adds a backend endpoint that fetches and parses an MTA-STS policy
file at https://mta-sts.<domain>/.well-known/mta-sts.txt per RFC 8461
sec. 3.3, paired with the (existing) MTA-STS TXT validator on the front-end.
Adds checks for svcs.TLS_RPT against RFC 8460 sec. 3.
The validator surfaces:
- Wrong owner name (must be _smtp._tls.<domain>).
- Missing or non-TLSRPTv1 v= tag.
- Missing rua= report destination.
- Empty entries inside rua=.
- rua URIs that are neither mailto: nor http(s):.
- Malformed mailto URIs (missing @ or domain).
Adds checks for svcs.MTA_STS against RFC 8461 sec. 3.1.
The validator surfaces:
- Wrong owner name (must be _mta-sts.<domain>).
- Missing or non-STSv1 v= tag.
- Missing id= tag.
- id= containing characters outside [A-Za-z0-9] or longer than 32 chars.
Adds compliance checks for svcs.DMARC against RFC 7489.
The validator parses the published TXT and surfaces:
- Wrong owner name (record must live at _dmarc.<domain>).
- Missing or non-DMARC1 v= tag.
- Missing, unknown, or "monitoring-only" p= policy.
- Invalid sp= subdomain policy.
- Invalid adkim/aspf alignment values.
- pct= out of [0..100] (error) and pct < 100 (info, partial deployment).
- Non-positive or non-numeric ri=.
- Unknown fo= entries (0 / 1 / d / s) and unknown rf= formats (afrf).
- Empty or malformed rua/ruf URIs (mailto and http(s) accepted; mailto
size suffix !N preserved).
Wires the new POST /api/resolver/spf-flatten endpoint into the SPF
validator. The async path runs after the local checks, debounced and
cancellable through EditorCompliance, and surfaces:
- spf.recursive-many-lookups / spf.recursive-too-many-lookups based on
the recursive lookupCount returned by the backend
- spf.too-many-void-lookups when more than 2 NXDOMAIN/NoData responses
occur during the walk (RFC 7208 §4.6.4)
- per-include diagnostics: spf.include-loop, spf.include-no-spf,
spf.include-resolver-error, spf.include-error — pointing at the exact
domain and mechanism that failed
Adds a recursive SPF flatten endpoint sized for the compliance UI:
- happydns.SPFFlattenRequest accepts a {domain, record?} pair so the UI
can preview an unsaved record without persisting it first; the optional
inline record bypasses the root TXT lookup.
- happydns.SPFFlattenResponse returns the recursive tree with per-node
Mechanism / Domain / Record / LookupsHere / Error fields, plus the
RFC 7208 §4.6.4 budget counters (LookupCount, VoidLookups, Exceeded,
VoidExceeded, Truncated).
- Hard caps at 10 lookups, 2 void lookups, depth 12, 2s per query and
10s overall. Cycle detection via the visited-domain set.
- Resolver selection mirrors ResolveQuestion (local / custom / default
to 1.1.1.1) with the same IPv6-bracket handling.
Extracts the SPF parser/serializer out of the editor into
$lib/services/spf.ts (matching dmarc.ts / mta_sts.ts) and adds a sync
validator that flags non-recursive issues against RFC 7208:
- missing or wrong v=spf1
- absence / multiplicity / non-final placement of ‘all’
- redirect= combined with ‘all’ or duplicated
- ptr deprecation (RFC 7208 §5.5)
- local DNS-lookup budget (warn ≥8, error >10) — recursive flatten will
come later via an async backend endpoint
- mechanisms missing values, empty terms, duplicates, length cap
Validates a DKIM TXT record (svcs.DKIMRecord) at edit time:
- Selector: must be present, must match the label charset.
- Version: only "DKIM1" is accepted (RFC 6376 sec. 3.6.1).
- Public key: detects missing p=, empty p= (revocation, warning), and
non-base64 payloads. Warns on RSA keys shorter than ~2048 bits and
errors on RSA keys shorter than ~1024 bits per RFC 8301.
- Algorithms: warns on SHA-1 (RFC 8301) and unknown hashes; flags
unknown key types or service types.
- Flags: surfaces t=y (testing) as info; warns on unknown flags.
- Granularity: marks g= as deprecated since RFC 6376.