Skip to main content
Deliverability is decided in DNS before a single byte of mail moves. A receiving server looks up your domain, asks who is allowed to send for it, is this message intact and authentic, and can I reach you over enforced TLS — and answers those questions entirely from records you publish. This page is the operator’s DNS contract: what to publish for a mail domain, why each record exists, how OxiMail generates and serves it, and how to verify the result. Two commands frame the whole workflow:
  • oximail setup provisions the full record set. With a Cloudflare token it writes the records straight into the zone through the Cloudflare API, reading the zone first: a record is created only where the zone holds none of its kind (publishing). Without a token it tells you to create the records by hand.
  • oximail check-dns performs live lookups and scores the domain. It asks the box’s own validating resolver (unbound on 127.0.0.1, see the resolver), which refuses a zone whose signatures do not validate and keeps a “no such record” for five minutes at most, so a record you have just published is seen. It used to query a public resolver directly. The report ends with n/n -- Ready for production or n/n -- Fix the items above.
Run oximail check-dns from the server after every DNS change. DNSSEC is enforced on the TXT lookups, so a zone with broken DNSSEC surfaces as a hard error rather than silently accepting forged records.

The record set

For a mail domain example.com served by the host mail.example.com at IP 203.0.113.10, the full set OxiMail provisions is: The sections below explain the records that carry real policy. The MX, A, and SRV records are mechanical — publish them and verify with check-dns.

SPF

OxiMail publishes a strict, single-source SPF record:
ip4:203.0.113.10 authorizes exactly your mail host; -all is a hard fail — any other IP claiming to send as example.com is rejected outright. This is the right default for a single self-hosted server. Adjust it by hand if your topology is wider:
  • Send from more than one IP (a backup MX host, a separate relay): add each ip4: / ip6: mechanism, e.g. v=spf1 ip4:203.0.113.10 ip4:203.0.113.20 -all.
  • Relay through a third party (a transactional provider): add their published include:, e.g. include:_spf.example-relay.com.
  • IPv6 senders: SPF only authorizes what it lists. If your MX also sends over IPv6, add the matching ip6: mechanism, otherwise IPv6 mail hard-fails SPF at the receiver.
On the inbound side, SPF is read as RFC 7208 writes it. A record is parsed with its grammar; the limit of ten DNS-querying terms counts each term once, so a domain with exactly ten passes; a domain that publishes no record is none, not temperror. SPF, DMARC and ARC share a DNS budget of 20 seconds per message: past it, SPF and DMARC are temperror and ARC gives no verdict, never a pass nor a fail.
check-dns evaluates SPF by direct IP presence and +all only; it does not recurse into include: or redirect=. If you rely on an include:, verify alignment with an external SPF validator as well.

DKIM

DKIM signs each outgoing message with a private key; receivers fetch the public key from DNS and verify the signature. OxiMail signs at delivery time in the outbound worker (see the SMTP layer). Generate the key and read back the record to publish:
This writes the private key to /etc/oximail/dkim/example.com.default.key and prints the matching DNS record:
  • Selector is default. The DNS name is always <selector>._domainkey.<domain>, so a rotation publishes a new selector alongside the old one, lets both verify during the cutover, then retires the old record.
  • Algorithm defaults to RSA (k=rsa). The full setup wizard publishes the RSA record automatically when run with a Cloudflare token.
  • Oversigning. The signature’s h= list names From, Reply-To, To, Cc, Subject, the two List-Unsubscribe headers and X-OxiMail-Flow one extra time, so a downstream actor cannot inject a second copy of a header a reader acts on without invalidating the signature (RFC 6376 §8.15). No operator action needed; it is how OxiMail signs.
On the inbound side, dkim=fail says why. The verification reason now travels alongside the verdict, into the Authentication-Results header and into the result stored per message — so a post-mortem can separate cases a bare verdict conflated, and which call for opposite actions: Three readings changed with the move to the RFC reference suites (the openspf suites, the dkimpy vectors). A c=simple signature hashes each header field as written, so such signatures verify, ours included at dkimpy. A missing public key is permerror. And a valid signature whose l= tag leaves part of the body unsigned is set aside (RFC 6376 §8.2: the choice is the verifier’s): it is reported dkim=policy, a signature not acceptable to this server’s policy (RFC 8601 §2.7.1), with a reason such as l=N covers N of the M octets, in the Authentication-Results, in the stored verdict and in the DMARC aggregate report. Such a signature never counts as a pass; it used to read dkim=none. In a summary of several signatures, the order is pass, fail, temperror, permerror, policy, none. The reason recorded is that of the first non-passing signature. A pass carries none, deliberately: a bad signature sitting next to a good one is normal — mailing lists re-sign — and dkim=pass reason=… would read as a defect where there is none.
When you add a mail domain after the initial setup, generate and publish its DKIM record explicitly. A domain whose key exists but whose default._domainkey TXT record is missing will sign mail that receivers then reject as dkim=fail (no key found). Run oximail check-dns for each domain to confirm the DKIM check passes.

DMARC

DMARC tells receivers what to do when a message fails both SPF and DKIM alignment, and where to send aggregate reports. OxiMail’s default is:
p=quarantine routes failing mail to the recipient’s Junk folder. Tune the policy to your rollout stage:
  • p=none — monitor only. Publish this first if you are unsure your SPF and DKIM are correct; read the rua aggregate reports, then tighten.
  • p=quarantine — the OxiMail default. Failing mail is junked, not refused.
  • p=reject — failing mail is refused at SMTP time. On the receiving side, OxiMail honours a sender’s p=reject with a 550 5.7.26 rejection (see the SMTP layer).
On the receiving side, DMARC judges the author the reader sees. The author is read with the RFC 5322 §3.4 grammar: the one mailbox of the one From: field. DMARC used to take the domain after the first < of the value, even inside a quoted display name or a comment, so From: "x <x@attacker.example>" <ceo@victim.example> passed DMARC on attacker.example while the mail client showed ceo@victim.example, under a p=reject policy; and a single header line outside the grammar anywhere in the message turned DMARC into none. A message without exactly one author (no From:, several From: fields, several mailboxes, a From: or a header block outside the grammar) is DMARC permerror, without a DNS query, and the envelope domain is never judged in its place. It is refused before the 250, on the direct and the backup-MX paths:
The reason is a fixed text per case, never the sender’s bytes. Trusted peers ([security] trusted_ips, loopback) are exempt. Make sure the rua mailbox exists and is monitored — it is where you find out a legitimate source is failing before recipients stop seeing your mail. OxiMail reads the aggregate reports it receives and emits one event per report, DmarcReport or DmarcReportWithWarnings, split at emission on a fixed predicate an alerting chain routes without interpreting a field: rejected or quarantined messages, or alignment failures nobody explained. The reporter’s policy_evaluated/reason (forwarded, mailing_list, local_policy, …) is kept and carried as override_reasons, so failures the reporter attributed, an auto-forwarder announcing itself for instance, stay visible with their motive on an information line rather than raising the alarm; measured on a production domain over six days, fourteen of fifteen reported failures were one large provider’s own forwarder. Counts are messages, never records, since one record can aggregate tens of thousands. The mirror side, the reports this host sends about the domains it receives from (RFC 7489), is [dmarc_reports] enabled: one row per evaluated message whose applied record asks for reports, grouped by policy domain over closed UTC days by a daily worker.

ARC and the DMARC exception

A forwarder or a mailing list breaks SPF and often DKIM. ARC (RFC 8617) lets it vouch for what it received: when DMARC fails but a valid ARC chain says the origin passed, the message may be delivered. Any domain can seal a chain that validates, though, so a forger of a p=reject domain used to reach the Inbox by sealing its own chain. The exception now holds only when the first sealer of the chain (the d= of the i=1 ARC-Seal) is one you name, a local policy (RFC 8617 §7.2.1):
The first sealer, not the newest: an honest forwarder re-sealing the forgery at i=2 does not make the attacker’s claim true. Name the mailing lists and forwarders your users receive through (google.com for Google Groups, for instance). An entry that is not a domain name refuses the boot. Without a listed sealer, the DMARC verdict stands, p=reject included, and the reason arc_sealer_untrusted says why. Every ARC validation failure is now fail (RFC 8617 §5.2.1); none means a verification cut by the DNS budget. The seals this server adds verify, including when a signed header is repeated.

MTA-STS and TLS-RPT

MTA-STS (RFC 8461) lets you require that other servers use validated TLS when they deliver to you, closing the downgrade-to-cleartext window that plain opportunistic STARTTLS leaves open. It has three moving parts, and OxiMail serves the policy itself:
  1. The TXT record _mta-sts.example.com = v=STSv1; id=<YYYYMMDD> announces that a policy exists. The id changes whenever the policy changes.
  2. The policy host mta-sts.example.com (a CNAME to your mail host) is where senders fetch the policy over HTTPS.
  3. The server serves /.well-known/mta-sts.txt from the [mta_sts] configuration block:
  • enforce — senders that honour MTA-STS refuse to deliver to you over unvalidated TLS.
  • testing — policy is published but failures are reported, not enforced. Use this while validating, then switch to enforce.
  • none — withdraw the policy.
TLS-RPT (RFC 8460) is the companion that gives you visibility:
On the sending side, OxiMail honours the MTA-STS policies of the domains it delivers to. A policy MX pattern such as *.example.com covers exactly one leftmost label (RFC 8461 §4.1): the match requires one non-empty label followed by the suffix. It used to test only that the MX ended with the suffix, so an attacker who spoofed the recipient’s MX with a name such as notexample.com, and a valid certificate for it, received the mail an enforce policy was there to protect. Receivers send daily reports of TLS connection failures to the rua address, which is how you detect a misconfigured policy or an active downgrade attempt. As with DMARC, monitor that mailbox.

DANE (TLSA)

DANE (RFC 7672) is the DNSSEC-anchored alternative to MTA-STS: a TLSA record under _25._tcp.mail.example.com pins your certificate so a sender validates TLS against DNS rather than a public CA list. It requires a DNSSEC-signed zone. It also requires a validating resolver on the box, including for denials of existence. “No TLSA record” must be proven (NSEC/NSEC3) just like a record must be signed: without that proof, “absent” and “stripped in transit” are the same answer, and stripping the TLSA is the DANE downgrade attack. Some forwarding stubs (systemd-resolved with DNSSEC=no, router forwarders) validate signed positive answers but re-serve denials without their proof. OxiMail therefore asks every DNS question to a resolver of its own, and believes a DNSSEC answer only once it has proven that resolver.

The resolver of the box

Since ADR-165, the server asks every DNS question to unbound on 127.0.0.1:53, through one resolver for the whole process; it never reads /etc/resolv.conf. The first-boot wizard installs and configures that unbound on Debian; elsewhere it prints the configuration and where to put it. A probe decides whether its answers are worth anything: the resolver answers the identity the install wrote in /etc/unbound/unbound.conf.d/oximail.conf (a relay on the loopback, dnsmasq or a forwarder, cannot), the root comes back validated, an unsigned zone does not, zones that do not validate are refused, and denials of existence are proven. The probe runs at boot, then every hour, every five minutes while the resolver is not proven; each change of state is logged, and oximail_resolver_trust{state} is 1 on exactly one of reliable, unreliable, mute, unmeasured. Nothing in this is fatal, and nothing passes unchecked. While the resolver is not proven, no DNSSEC answer is believed, and the mail to a domain that used DANE in the last 30 days, that publishes a TLSA record, or whose TLSA lookup fails waits in the queue instead of leaving without authentication; it leaves by itself once the resolver is proven again. The rest of the server serves. When that wait costs more than the risk, lift it for a bounded time:
The verb is a dry run without --yes and works with the server stopped. oximail resolver check runs the server’s own probe (--json for scripts): exit 0 reliable, 1 unmeasured (the network kept a test name out of reach), 2 unreliable or mute. It reads no configuration and works with the server stopped; as root it also reads unbound’s control channel, whose modules must include validator. A deployment can run it on the new binary before the swap and refuse a box proven unreliable. A record just published is seen within five minutes. The configuration setup writes caps a cached “no such record” at five minutes and turns unbound’s aggressive NSEC off: with it on, a cached NSEC denied a name for the zone’s full negative TTL (1800 seconds at Cloudflare), past that cap, so a record published a moment earlier stayed invisible for half an hour. A box installed before keeps its old text: oximail doctor names the first line that differs under resolver_config, and oximail resolver ensure --apply rewrites the file (the identity kept), checks it and restarts unbound; without --apply it only says what would change. After publishing in a Cloudflare zone, the wizard and oximail dkim publish also empty the local resolver’s cache of that zone, so the check that follows reads the new records. Every name OxiMail asks is absolute. A relative name is retried by the system resolver under each suffix of the search list of /etc/resolv.conf, on any error, and the last attempt wins: a TLSA lookup that failed validation used to come back as the proven absence of _25._tcp.<mx>.<suffix>, “no TLSA”, which is the downgrade DANE exists to stop. Other programs on the box still take that list, and leak the names they ask to the suffix’s servers, so oximail doctor warns under resolver_search when /etc/resolv.conf carries a search or domain line. No TLSA lookup is made for an MX host whose address records are not signed (RFC 7672 §2.2.2), and a failed address lookup makes that host unreachable, like a failed TLSA lookup. oximail doctor reports the whole posture: resolver_trust (fail when unreliable or mute, warn when unmeasured), dns_negative_proof (fail while denials come back without their proof), resolver_config and resolver_search.

Inbound TLSA and outbound DANE

OxiMail enforces DANE on the outbound path by default and keeps your inbound TLSA records fresh automatically after each certificate renewal. Outbound, the MX and TLSA lookups are read with their DNSSEC proofs: a signed destination gets DANE, an unsigned one gets opportunistic TLS, and an answer the validator cannot trust (a bogus proof, a denial without proof, SERVFAIL, a timeout) makes that server unreachable for the attempt, never “no DANE”: the next MX is tried, then the message waits in the queue and is retried (RFC 7672 §2.1.2). The journal names the host and the reason. Because the pinned value follows the served key (a fresh key at renewal with acme_reuse_key = false, a key rotation, a new certificate from a proxy), this requires a configured DNS provider:
When dane_enabled = true (the default), [dns] provider must be set or the server refuses to start (fail-loud, ADR-027). Without a provider, an ACME renewal would silently leave your TLSA records stale and break inbound mail for every DANE-validating sender.
The provider options:
  • cloudflare — OxiMail writes the TLSA records itself through the Cloudflare API. A mail domain delegated as a subdomain is found in the zone that holds it: b.example.org is published in the zone example.org, the most specific zone the token sees winning; the wizard, oximail dkim publish and DANE used to fail there with no zone found.
  • rfc2136 — OxiMail writes them itself on your own authoritative DNS server, by a TSIG-signed dynamic update (RFC 2136, RFC 8945). See below.
  • manual — you update the TLSA records out of band. After a renewal the journal says whether the published records still match.
With cloudflare or rfc2136 and dane_enabled = true, OxiMail publishes the TLSA records after each ACME issuance or renewal, after a key rotation, and when a [tls] cert_path certificate changes on disk. New records are created before superseded ones are removed, so a validating sender never meets a name with no matching pin. With dane_enabled = false, nothing writes TLSA automatically, whatever [dns] says; oximail dane publish still publishes through the configured provider when you run it. oximail doctor then skips its dane row as long as no TLSA is published for the pinned ports, and keeps warning on a record left published, which nothing re-signs at the next key change. The setting does not touch delivery: outbound DANE toward a destination MX works the same whatever it says.

RFC 2136: your own DNS server

For a zone you host yourself (BIND, Knot, PowerDNS, anything that accepts TSIG-signed updates), point OxiMail at the zone’s primary:
The three keys are required with this provider, and a missing one is named at boot. The key file holds one line in the nsupdate -y form, [algorithm:]name:base64-secret, for example hmac-sha256:oximail-update:<base64>. The algorithm is hmac-sha256 (the default when omitted), hmac-sha384 or hmac-sha512. Like every secret sidecar, it is mode 0600, owned by the service user, one line; a file readable by group or others is refused. oximail check-config reads it and refuses a key it could not use, and a server with DANE on refuses to start when it cannot read it. An inline rfc2136_tsig_key in the TOML is refused at boot; oximail config externalize-secrets moves it to a sidecar (configuration). With BIND, tsig-keygen -a hmac-sha256 oximail-update prints the key clause for named.conf; its secret is the base64 part of the line above. On the primary, allow that key to update TLSA records at the names OxiMail pins, and nothing else:
OxiMail talks to the primary over TCP: the box must reach it on port 53. It reads the current records with a TSIG-signed query, then adds and deletes single records with TSIG-signed updates. An update must come back signed by the same key with the NOERROR code; a read also accepts a signed NXDOMAIN, which says that nothing is published at the name yet. That read used to be taken for an error and the port skipped, so a name never pinned before never received its first pin. Any other answer, unsigned or a refusal, is an error naming the code, never a success; a key the primary does not know is named as such (NOTAUTH, unsigned) rather than reading like a forged answer. The exchange has been verified against a Knot primary with the three algorithms. The TLSA names must lie inside rfc2136_zone. The zone must be DNSSEC-signed, so the primary has to sign what it receives (BIND’s dnssec-policy, for instance).

Which ports are pinned

OxiMail publishes a 3 1 1 record at _<port>._tcp.<hostname> for the SMTP ports the box serves, and only those: 25, 465 and 587, or 25 alone on a backup MX, which accepts mail from other servers but hosts no user who submits. It does not pin the HTTPS port: no browser validates DANE, and federation between OxiMail servers authenticates a peer by the key pinned when the two operators paired their servers, not by DNS. oximail dane verify, oximail dane publish and oximail dane rotate all use the same set of ports. A TLSA record on any other port is maintained by nobody: the renewal and rotation hooks never touch it, so the next key change leaves it on the old key, and a DANE-validating client then refuses the connection. oximail dane verify also looks up the TLS ports the box does not pin (443, 993, 995, 143, 110, 4190, plus 465 and 587 on a backup MX) and lists what it finds under unmaintained_pins; oximail doctor reports them under dane_unmaintained_pins. Remove such a record at the DNS provider. This is how a _443._tcp record left by an earlier version shows up. The record and the certificate file can agree while clients meet another key: a stale listener, a proxy that took the name over, a certificate swap that did not happen. oximail doctor --reachability therefore also reads, under dane_served_spki, the key each pinned port actually serves: it opens the port as a sending server does (STARTTLS on 25 and 587, TLS at once on 465), completes a handshake and compares the key presented with the key of the certificate file. A different key is a failure. A pinned port where nothing listens is a warning, since the pin protects nothing there. The server runs the same probe every hour and publishes oximail_dane_served_spki_state{host, port, state} (match, differs, not_served, probe_error), so an alert can watch it between two rounds of doctor. See TLS & ACME for certificate provisioning and renewal.

Reverse DNS (PTR) and deliverability

Reverse DNS is published by whoever controls the IP block (your hosting provider — Hetzner, OVH, AWS, …), not in your domain’s zone. It is one of the strongest reputation signals a receiver has.
  • Set the PTR for 203.0.113.10 to mail.example.com — the same name your server uses in its SMTP HELO/EHLO. Many large receivers reject or heavily penalize a mismatch (no FCrDNS).
  • check-dns and doctor pass the PTR check when the record names the server’s host name exactly ([server] hostname, or check-dns --hostname); confirm too that the forward A record points back to the same IP. The check used to compare it with a name built as mail.<domain>, wrong for any other host name.
  • New IPs have no sending reputation. Warm up gradually and keep volume steady; a sudden spike from a cold IP looks like a compromised host.

Autodiscovery

Mail clients find your endpoints from the SRV records and the autoconfig / autodiscover CNAMEs in the table above. The full oximail setup run also publishes a CAA record restricting certificate issuance to Let’s Encrypt, which both hardens the domain and matches OxiMail’s ACME setup. None of these carry tunable policy — publish them and let check-dns confirm.

Publishing with oximail setup dns

oximail setup dns re-runs the wizard’s DNS step on an installed box. It reads the zone first and creates a record only where the zone holds none of its kind, so a second run on a configured zone adds nothing. It used to send its whole list on every run and count on Cloudflare refusing an identical duplicate: the MTA-STS record carries the day’s date as its id, so a re-run added a second v=STSv1 TXT beside the first, which leaves the domain with no MTA-STS policy at all (RFC 8461 §3.1); a TLS-RPT record with another rua was doubled the same way (RFC 8460 §3). A TXT is compared within its kind (v=STSv1, v=TLSRPTv1, a DAV path=); an MTA-STS record is kept whatever its id, since changing the id is the act of whoever changes the policy. A record that differs from the one OxiMail would write is shown and kept, never doubled. MX records naming another server, which you kept, stay the domain’s only MX: this server’s is not added beside them. The step ends naming the checks still failing. A dry run of the wizard (oximail setup --dry-run) reads the zone without writing and lists each record: would create, already there, differs and kept. The step runs without a terminal, so a script, an agent or ssh without a tty can republish a box’s records. Flags answer every question it can ask:
  • --replace-mx, --replace-spf, --replace-dmarc replace an existing record without asking. An SPF record is merged when it can be: your own mechanisms are kept.
  • --public-ipv4 auto (the default) measures whether the public IPv4 is this machine’s or proven forwarded to it, and asks when nothing proves it; forwarded says the router forwards the mail ports here; none publishes IPv6 only (no A record, no ip4: in SPF).
Without a terminal, a replace question no flag answered keeps the existing record and is named at the end (exit 1), and a public IPv4 nothing decided stops the run before anything is published, naming the flag (exit 2). Exit 0 means nothing is left to do.

Verifying with oximail check-dns

--domain is the mail domain, --hostname the server’s own host name: the target its MX and SRV records must name and the name its PTR must carry. --hostname defaults to [server] hostname of the configuration file; the host name used to be built as mail.<domain>. On a full server the report runs fifteen live checks: MX, rDNS (PTR), SPF, DKIM, DMARC, TLS-RPT, MTA-STS (which passes when no record is published, the posture being optional), and the records mail clients are configured from (the _submission, _submissions, _imaps, _caldavs, _carddavs and _jmap SRV records, and the path TXT of the two DAV records). On a backup MX the MX check requires the domain’s MX to name the backup, and SPF, DKIM and the client records are not asked: they belong to the primary. It prints a pass/fail line for each, and --json gives the same report to a script (hostname included; exit 0 all pass, 1 a record to fix):
The MTA-STS policy file and the DANE TLSA records are enforced by the server at send time and served from the policy host rather than re-checked here; validate those with an external MTA-STS / DANE checker after publishing. oximail doctor runs the same checks under dns, for each mail domain the box serves, against the server’s own host name; the warning names the failing checks. It used to check the host name as if it were the mail domain, and warned on every healthy box. The host name itself, which the install registers as the organization’s domain, is listed as not checked while no account or alias lives under it; any other domain is checked, even before its first account, since that is when its DNS is set up. Common failures:
  • PTR mismatch — fix the reverse record at your hosting provider, not in your zone.
  • DKIM “no record” — you generated the key but did not publish the TXT record. See the DKIM section above.
  • SPF “does not include IP” — your sending IP is not listed; add the ip4: / ip6: mechanism.
  • Lookup failed — a propagation delay (wait and re-run) or a broken DNSSEC zone (the lookup fails hard rather than trusting an unvalidated answer).

Signed and encrypted mail

Two message-level mechanisms sit next to the DNS ones: the server verifies the S/MIME signatures of the mail it receives, and it looks up and publishes OpenPGP keys on behalf of its users. Neither needs a DNS record of yours.

Verifying received signatures

Every signed message a mailbox receives gets the RFC 9219 verdict clients read (signed messages): the status now, the status at delivery, the reasons of a failure, and who signed. Ingestion only reads the root Content-Type and marks a signed message due, before the 250, with no CMS parser in the SMTP session; a worker verifies after delivery and re-evaluates each verdict when it can change (certificate expiry, the next update of a revocation list, a retry). The verdict is a dated fact: it never changes acceptance, filing or the spam score.
  • The trust store is embedded in the binary: the email roots of the Mozilla programme plus the public intermediates declared to the CCADB, since a signed message often carries only its leaf certificate and the server never fetches a missing intermediate from an address the certificate gives (AIA). Each file replaces its own set only: trust_roots alone (the root of an internal authority, say) keeps the embedded intermediates, trust_intermediates alone keeps the embedded roots. An operator file carries no date: doctor smime_trust says so, and warns when the store holds no intermediate. A named file that does not load is fatal at boot, never silently replaced. When the trust store changes, the messages that failed at delivery under the previous one are verified again.
  • Revocation is checked by CRL, never OCSP. The lists are fetched only for the distribution points of a certificate path that already validated, over http or https through the server’s single outbound door, and cached until their nextUpdate: that is the feature’s only outbound traffic. OCSP would tell the authority who reads what, and when; AIA would let the sender’s certificate choose where the server connects. Per organization, the override key smime.revocation (a boolean) switches checking off for that organization only; switched off, a valid signature is signed/verified and its signer says revocationChecked: false.
  • oximail doctor reports the store under smime_trust: the counts of roots and intermediates, the source of each set, the date of the embedded bundle and the revocation switch; warn when the embedded bundle is more than a year old or the store holds no intermediate, fail when a named file does not load or the store holds no anchor, skipped with verify = false. On a backup MX or a smarthost, which store no mailbox, neither the S/MIME verifier nor the brand-logo resolver runs: smime_trust and bimi_trust both say skipped, and why, instead of an ok about a function the box does not have.
  • Metrics: oximail_smime_verdict_total{status}, oximail_smime_crl_total{outcome} and oximail_smime_worker_failed_total{reason} (failures on the server’s side, which never look like a sender’s failure). Logs carry the message id, the status and a reason code, never an address or a certificate subject.
Signed mail stored before the upgrade that introduced verification carries no verdict, and the worker only takes the messages ingestion marked. Run once, after the upgrade:
The running server then verifies them at its pace. oximail smime verify re-verifies from the stored bytes, with the same trust store, CRL cache and writes as the worker: every account of the box, or --account <email>; dry-run by default, --apply to verify and write. A verdict already verified at delivery stays so. Both verbs take --json.

OpenPGP key lookup

Clients look up a recipient’s OpenPGP key and publish their user’s own through the server (RecipientKey/lookup, PgpKey/publish, see OpenPGP keys), so a key server learns neither the user’s IP address nor anything the server did not already relay. A lookup asks the recipient domain’s Web Key Directory first (openpgpkey.<domain>, then the domain itself), then the configured key server.
A switched-off gesture answers forbidden and the session announces it closed, so clients stay silent instead of failing on every recipient. The key server must be an https URL, or the server refuses to start. Every request goes through the single outbound door: https only, private destinations refused at resolution, 8-second timeout, bodies capped at 512 KiB, no WKD request for an IP-literal domain. A result is cached for ten minutes per address, never when the source could not be reached.

Standards

  • RFC 7208 — Sender Policy Framework (SPF).
  • RFC 6376 — DomainKeys Identified Mail (DKIM).
  • RFC 7489 — Domain-based Message Authentication, Reporting, and Conformance (DMARC).
  • RFC 8461 — SMTP MTA Strict Transport Security (MTA-STS).
  • RFC 8460 — SMTP TLS Reporting (TLS-RPT).
  • RFC 7672 — SMTP Security via Opportunistic DANE TLS.
  • RFC 8550 — S/MIME 4.0 Certificate Handling.
  • RFC 9219 — S/MIME Signature Verification Extension to JMAP.
For how these records are consumed and enforced on the wire, see the SMTP layer; for certificate provisioning, see TLS & ACME; for the spam pipeline that authentication feeds, see anti-spam.