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.
Replace dueling parse/stringify $effects across service editors with
one-time top-level init plus a single write-back $effect. Remount
editors via {#key value} in ServiceEditor so children no longer need
inbound-sync logic.
Subscribes to dangling.external-target.v1 entries via AutoFillDiscoveryEntries
and runs RDAP per registrable domain (deduped, parallelised, capped at 8
concurrent), publishing a per-Ref Facts map consumed by checker-dangling.
Domain- and user-scoped consumers were missing every discovery entry
published below their scope. The exact-match prefix dscent-tgt|{u/d/s}|
introduced in 9c6398b1b only returned entries stored at the literal
target string, so a domain-scoped consumer like checker-tls or
checker-caa never received the tls.endpoint.v1 entries that
service-scoped producers (checker-dane, checker-smtp, checker-sip,
checker-srv, checker-stun-turn) publish under the same domain. The
symptom on the consumer side was "No TLS endpoints have been discovered
for this target yet." even when producers had run.
Drop the trailing "|" from the iteration prefix when the target lacks a
ServiceId (and the DomainId for user scope) so the prefix scan matches
narrower scopes too. RawURLEncoded identifiers contain neither "/" nor
"|", so slash boundaries in the encoded "u/d/s" target form remain
unambiguous. Service-scoped lookups stay exact. Each matching key is
parsed back into its actual stored target before fetching the primary
record, so the returned StoredDiscoveryEntry.Target reflects where the
entry was published, not the (broader) target that found it.
Propagate the persisted CheckEvaluation.States through BuildReportContext
and the HTTP report transport so reporters can render rule-driven
sections (hints, severity) without re-deriving them from raw data. When
no evaluation is available the context carries nil states, matching the
SDK's documented nil-safe fallback to data-only rendering.
Rules now return []CheckState, the engine stamps RuleName from the rule,
and the HTTP rule-result lookup matches on RuleName rather than Code.
domain_contact emits one state per role (Subject) instead of a
concatenated single-state message.