Skip to content

Commit a381df8

Browse files
Three roadmap rows the fork was best known for not having
Authentication-Results (RFC 8601) and Received-SPF (RFC 7208 9.1). Until now the SPF, DKIM and DMARC verdicts existed only as X-hMailServer scoring headers, so nothing downstream could act on them and our own ARC seals reported spf=none - they read it from a Received-SPF header this server never emitted. The verdicts had to be plumbed, not recomputed. SpamTestResult carries only a test name, a pass/fail and a score, so the things the header needs - each signature's d=, the SPF identity, the DMARC policy that applied - were known only inside the tests and thrown away on return. There is now a carrier the connection owns and both phases fill: SPF is a pre-transmission test while DKIM and DMARC are post-transmission, so anything scoped to one phase could only ever describe half the answer. Both switches default off, because the field is a claim other systems act on and it is worth something only if the reader trusts this server's name. RFC 8601 section 5 stripping is implemented. A field arriving with our own authserv-id was not written by us - this runs before ours is added - so it is a forgery, and a downstream filter trusting our name would otherwise act on a dkim=pass the sender wrote for themselves. Fields bearing anyone else's authserv-id survive byte for byte, which is why the rewrite walks raw lines instead of reparsing: the message may carry a DKIM signature over those exact bytes. Two things nearly broke that. SplitString discards the trailing empty substring, so reconstructing from "is this the last element" would have eaten the header's final newline and left \r\r\n where the header/body boundary belongs - silent corruption on the one path meant to remove a forgery. And Message::SetSize takes an int where the size is a long, which /WX rejects. Every value that reaches these headers is attacker-supplied - envelope sender, HELO string, a signature's d= - so they are dropped to a conservative character set rather than escaped, and capped. There is no escaping that is safe in every position the text can land in: inside a comment, beside a tag, at the start of a folded line. The Received-SPF explanation is generated locally rather than quoted from the policy's exp=, which is a TXT record the sending domain controls. There is deliberately no auth= method: this writes nothing for authenticated submissions, so an auth= slot would be a field nothing could ever populate, which is the exists-but-inert shape this tree has been bitten by. Client certificates (mutual TLS) for inbound sessions. Per-port off / request / require with a per-port CA bundle. The inbound path is a separate branch and a separate verifier class, so outbound verification, MTA-STS enforcement and DANE-EE are untouched - and the RFC 7435 opportunistic forgiveness in OverrideResult_, which is right for outbound, can never apply inbound. verify_fail_if_no_peer_cert is what rejects a certificate-less client, because the callback is never invoked in that case. Save-time validation refuses the three configurations that would look enforced and enforce nothing: a policy on a plaintext port, a policy with no CA file, and require on STARTTLSOptional, where a client that never issues STARTTLS is never asked for a certificate. A require port with an unusable CA bundle does not start, rather than accepting connections it can only reject. DKIM dual-selector rotation. A second selector and key per domain so the new selector can be published and confirmed in DNS before it signs anything. The primary signs and the secondary never does: it is by definition waiting for propagation, so a second signature would fail at every receiver until propagation completed, and while RFC 6376 6.1 says one passing signature suffices, real filters score a broken one against you. Promote refuses when either staged value is empty - it would otherwise blank the primary and stop signing silently - and refuses when the staged key is not on disk, checked before the old key is cleared rather than after. Schema 6006 -> 6007 -> 6008 across all four dialects, both create-table paths and the DBUpdater chain. Two probes per step, not one: SQL CE commits each ALTER implicitly and cannot roll back, so a two-column step can half-apply and each column has to be proven separately. Every default is today's behaviour - empty selectors, policy 0 - so an upgraded database behaves exactly as it did. README and Roadmap audited against the code rather than against each other, which found claims that were simply false. "Full RFC 4013 SASLprep of non-ASCII credentials" was true of PLAIN and LOGIN and not of SCRAM, which does none. The metrics paragraph named an uptime metric and a database-up metric that no longer exist under those names, described a histogram as a summary, and said /readyz checks pool state when it now requires a real database round trip. Four rows were marked not-started that are implemented, and nine Details described gaps that have since been closed. Full suite: 1411 passed, 0 failed, 0 skipped, crash oracle clean.
1 parent 77af73e commit a381df8

57 files changed

Lines changed: 2290 additions & 79 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,44 +29,45 @@ Mail protocols
2929
--------------
3030

3131
* **SMTP** with PIPELINING, ENHANCEDSTATUSCODES, 8BITMIME, SIZE, CHUNKING/BDAT (RFC 3030), DSN delivery status notifications (RFC 3461/3464) and SMTPUTF8/EAI for internationalised addresses.
32-
* **IMAP4rev1 and IMAP4rev2**, with IDLE, MOVE (RFC 6851), UIDPLUS (RFC 4315), CONDSTORE/QRESYNC (RFC 7162), SEARCHRES (RFC 5182), ESEARCH (RFC 4731), SORT, ACL, NAMESPACE, ID (RFC 2971), SPECIAL-USE (RFC 6154) and QUOTA.
32+
* **IMAP4rev1**, plus **IMAP4rev2** (RFC 9051) advertised with its behavioural deltas implemented — a few of the extensions rev2 folds in (LIST-STATUS, non-synchronising literals, BINARY) are still missing — with IDLE, MOVE (RFC 6851), UIDPLUS (RFC 4315), CONDSTORE/QRESYNC (RFC 7162), SEARCHRES (RFC 5182), ESEARCH (RFC 4731), SORT, ACL, NAMESPACE, ID (RFC 2971), SPECIAL-USE (RFC 6154, including explicit designation via `CREATE ... (USE (\Sent))`) and QUOTA.
3333
* **POP3**, including retrieval from external POP3 accounts on a schedule.
3434
* **Public folders**, shared across accounts with per-user ACLs.
3535

3636
Transport security
3737
------------------
3838

39-
* **TLS 1.2 and 1.3** by default, on implicit-TLS and STARTTLS ports, with SNI and configurable cipher suites.
39+
* **TLS 1.2 and 1.3** by default, on implicit-TLS and STARTTLS ports, with SNI, configurable cipher suites (separately for TLS ≤ 1.2 and TLS 1.3) and configurable key-exchange groups — **hybrid post-quantum key exchange** (X25519MLKEM768) is preferred by default, on every TLS context inbound and outbound.
4040
* **MTA-STS** (RFC 8461) policy discovery and enforcement for outbound mail, and optional hosting of your own policy at `mta-sts.<domain>`.
4141
* **DANE** (RFC 7672) with full in-process **DNSSEC validation** (RFC 4033–4035) — a bogus chain blocks delivery to that host rather than silently downgrading. The **MX RRset is validated too**, not just the TLSA record (RFC 7672 §2.2): DANE is applied only to a host the recipient domain provably published, so a forged MX answer cannot redirect delivery to a host whose own TLSA record then validates.
4242
* DNSSEC validation also protects SPF, DKIM and DMARC record lookups.
43-
* **TLS-RPT** (RFC 8460) daily aggregate reports to recipient domains.
43+
* **TLS-RPT** (RFC 8460) daily aggregate reports to recipient domains — off until `TlsRptFromAddress` is set (the server notes this in the application log while statistics are collected unsent).
4444
* **ACME v2 (Let's Encrypt)** built in: certificates are issued, renewed, assigned to TLS ports and hot-reloaded without a restart. The private key is reused across renewals, so published DANE TLSA records stay valid.
4545

4646
Sender authentication and anti-abuse
4747
------------------------------------
4848

4949
* **SPF**, **DKIM** signing and verification (including Ed25519, RFC 8463) and **DMARC** evaluation with alignment.
50-
* **ARC** sealing (RFC 8617) so forwarded mail keeps a verifiable authentication chain.
51-
* **SRS** sender rewriting for forwarded mail, and optional **BATV** (`prvs`) backscatter protection.
50+
* **DKIM signature timestamps**: every signature carries `t=`; an `x=` expiry is added when `DKIMSignatureValiditySeconds` is set, and expired signatures are refused on verification (on by default, with a configurable clock-skew allowance). Optional **oversigning** (`DkimOversignHeaders`, off by default) stops a second `From:` being prepended to signed mail.
51+
* **ARC** sealing (RFC 8617) so forwarded mail keeps a verifiable authentication chain — off by default (`ArcSealingEnabled`).
52+
* Optional **SRS** sender rewriting for forwarded mail (`SRSEnabled`, off by default), and optional **BATV** (`prvs`) backscatter protection.
5253
* **SpamAssassin** integration, **DNSBL** and **SURBL** lookups, greylisting, HELO/PTR/MX sanity checks and a weighted scoring pipeline.
5354
* **Virus scanning** via ClamAV (clamd or clamscan) or any command-line scanner.
5455
* Attachment blocking, IP ranges with per-range policy, and connection auto-banning after repeated authentication failures.
5556

5657
Account security and authentication
5758
-----------------------------------
5859

59-
* **SCRAM-SHA-256** SASL across IMAP, SMTP submission and POP3, plus **SCRAM-SHA-256-PLUS** channel binding on all three, with deterministic anti-enumeration salts.
60+
* **SCRAM-SHA-256** SASL across IMAP, SMTP submission and POP3, plus **SCRAM-SHA-256-PLUS** channel binding on all three, with deterministic anti-enumeration salts. SMTP and POP3 offer SCRAM whenever AUTH is available; on IMAP, SASL (PLAIN and SCRAM alike) sits behind one setting whose shipped default is off.
6061
* **OAuth2 / OpenID Connect** bearer tokens — SASL XOAUTH2 and OAUTHBEARER (RFC 7628) — validated against an external identity provider's signing key.
6162
* **LDAP directory authentication** against Active Directory or any LDAP directory, so accounts authenticate with their domain password. Simple bind and SASL Negotiate; LDAPS and StartTLS; certificate validation on by default, and a password is never sent over an unprotected connection unless that is explicitly permitted. Unlike the Windows-logon path it needs no domain-joined host, which is the usual situation for a mail server in a DMZ. Infrastructure failures are reported separately from wrong passwords, so a directory outage does not read as a hundred users mistyping. Off by default: the whole `[LDAP]` ini section is absent until you add it.
6263
* **Argon2id** and **PBKDF2-HMAC-SHA256** password hashing, with transparent upgrade on login, a minimum-accepted-hash policy, and an optional server-side pepper.
63-
* Full RFC 4013 SASLprep of non-ASCII credentials.
64+
* Full RFC 4013 SASLprep of non-ASCII credentials on the PLAIN and LOGIN paths (SCRAM usernames are matched as sent).
6465
* Optional **TOTP two-factor authentication** for administrative logon.
6566

6667
Mail filtering and routing
6768
--------------------------
6869

69-
* **Sieve** (RFC 5228) — a standards-based interpreter runs each account's active script during delivery (`keep`, `fileinto`, `discard`, `redirect`, implicit keep), with an optional **ManageSieve** (RFC 5804) listener so clients can manage scripts over TCP.
70+
* **Sieve** (RFC 5228) — a standards-based interpreter runs each account's active script during delivery (`keep`, `fileinto`, `discard`, `redirect`, implicit keep, plus the `copy`, `relational`, `subaddress`, `imap4flags` and `vacation` extensions), with an optional **ManageSieve** (RFC 5804) listener so clients can manage scripts over TCP.
7071
* The original rules engine, with global and per-account rules, regular-expression criteria and scripted actions.
7172
* Server-side **event scripts** (VBScript/JScript) on connection, HELO, DATA, accept and delivery events.
7273
* Routes, aliases, distribution lists, catch-all addresses and plus-addressing.
@@ -93,7 +94,7 @@ Technology
9394
| MySQL/MariaDB client | MariaDB Connector/C, shipped as `libmysql.dll` with auth plugins — works with MySQL 8 `caching_sha2_password` and MariaDB `ed25519`/`gssapi` out of the box |
9495
| Administration GUI and tools | C# / .NET 10 (WPF, Fluent design) |
9596
| Extensibility | COM/IDispatch API, plus a REST administration API |
96-
| Schema | Database version 6005, upgradeable from every earlier hMailServer release |
97+
| Schema | Database version 6008, upgradeable from every earlier hMailServer release |
9798

9899
**Quality gates.** Every release ships SPDX and CycloneDX SBOMs (Syft). The repository runs CodeQL analysis, Dependabot CVE alerts with grouped update pull requests, a dependency-review gate on pull requests, an installer smoke test that installs the built installer on a clean machine and verifies the service comes up, and a monthly comparison against the original upstream repository so nothing landing there is missed.
99100

@@ -298,6 +299,17 @@ Transport security and authentication:
298299
DaneEnforcementEnabled=1 ; honor recipient DANE/TLSA records when sending
299300
DnssecValidationEnabled=1 ; validate DNSSEC for DANE and SPF/DKIM/DMARC lookups
300301
DnssecTrustAnchors= ; override root trust anchors ("tag alg digesttype hex;...")
302+
TlsKeyExchangeGroups=X25519MLKEM768:SecP256r1MLKEM768:X25519:secp384r1:secp256r1
303+
; TLS key-exchange groups, hybrid post-quantum first; reaches every
304+
; TLS context in the server, inbound and outbound
305+
TlsCipherSuites13= ; TLS 1.3 ciphersuites (empty = OpenSSL's defaults; the SslCipherList
306+
; setting covers TLS 1.2 and below)
307+
DkimOversignHeaders= ; DKIM oversigning (RFC 6376 5.4): header names listed in h= once more
308+
; than the message carries them (empty = off; From is always included
309+
; when the feature is on)
310+
DKIMSignatureValiditySeconds=0 ; add an x= expiry to outgoing DKIM signatures (0 = no x= tag)
311+
DKIMEnforceSignatureExpiry=1 ; refuse expired DKIM signatures when verifying
312+
DKIMExpiryClockSkewSeconds=300 ; clock-drift allowance when enforcing x=
301313
ArcSealingEnabled=0 ; add ARC seals when forwarding (uses the domain's DKIM key)
302314
TlsRptFromAddress= ; sender for daily TLS-RPT reports (empty = disabled)
303315
TlsRptOrganizationName=hMailServer
@@ -364,15 +376,17 @@ Administration and monitoring:
364376
; SEARCH BODY/TEXT reads every message in the mailbox, so these bound what a single
365377
; authenticated command can cost; raise them for mailboxes of several hundred thousand messages
366378
ManageSieveServerPort=0 ; ManageSieve (RFC 5804) script-management service (0 = disabled, standard port 4190)
367-
ManageSieveServerBindAddress=127.0.0.1 ; SASL PLAIN over plaintext; bind to localhost unless TLS-fronted
379+
ManageSieveServerBindAddress=127.0.0.1 ; STARTTLS is offered when a TLS certificate is configured, and an
380+
; IP range can require TLS before authentication; otherwise SASL PLAIN
381+
; travels in the clear, so keep the bind on localhost
368382
JsonLogging=0 ; write logs as JSON lines
369383
</pre>
370384

371-
**Mail filtering (Sieve, RFC 5228).** Each account can have an active Sieve script that runs during local delivery, supporting `keep`, `fileinto`, `discard` and `redirect` with the core tests (`header`, `address`, `exists`, `size`, `allof`/`anyof`/`not`) and `:is`/`:contains`/`:matches` match types. Scripts are edited from the Control Panel account **Sieve** tab (or the COM `Account.SieveScript` property) and stored as files under the data directory. With `ManageSieveServerPort` set, mail clients can upload and manage multiple named scripts over **ManageSieve (RFC 5804)** (`CAPABILITY`, SASL `PLAIN` `AUTHENTICATE`, `PUTSCRIPT`/`CHECKSCRIPT`, `LISTSCRIPTS`, `GETSCRIPT`, `SETACTIVE`, `DELETESCRIPT`).
385+
**Mail filtering (Sieve, RFC 5228).** Each account can have an active Sieve script that runs during local delivery, supporting `keep`, `fileinto`, `discard`, `redirect` and `stop` with the core tests (`header`, `address`, `envelope`, `exists`, `size`, `allof`/`anyof`/`not`), the `:is`/`:contains`/`:matches` match types, and the extensions **copy** (RFC 3894), **relational** (RFC 5231), **subaddress** (RFC 5233), **imap4flags** (RFC 5232) and **vacation** (RFC 5230, including `:seconds`). Scripts are edited from the Control Panel account **Sieve** tab (or the COM `Account.SieveScript` property) and stored as files under the data directory. With `ManageSieveServerPort` set, mail clients can upload and manage multiple named scripts over **ManageSieve (RFC 5804)** (`CAPABILITY`, `STARTTLS`, SASL `PLAIN` `AUTHENTICATE`, `PUTSCRIPT`/`CHECKSCRIPT`, `LISTSCRIPTS`, `GETSCRIPT`, `SETACTIVE`, `DELETESCRIPT`, `HAVESPACE`).
372386

373-
The metrics listener also serves Kubernetes-style health probes: `/livez` (process liveness), `/readyz` (200 when `StateRunning` and the database pool is connected, else 503 — and 503 while the server is draining/stopping) and `/healthz` (JSON: status, server state, database). `/metrics` exposes counters and gauges for processed/spam/virus messages, TLS handshakes (success/failure), authentication (success/failure), sessions per protocol, uptime, database up/pool, the SMTP delivery-queue depth, delivery outcomes (`hmailserver_messages_delivered_total`/`_deferred_total`/`_bounced_total`), the message-store consistency result (`hmailserver_messagestore_missing_files`), and aggregate per-command processing latency (`hmailserver_command_processing_seconds` summary).
387+
The metrics listener also serves Kubernetes-style health probes: `/livez` (process liveness), `/readyz` (200 only when `StateRunning` and the database has answered a real round trip within the last 20 seconds, else 503 — and 503 while the server is draining/stopping) and `/healthz` (JSON: status, server state, database, uptime). `/metrics` exposes counters and gauges for processed/spam/virus messages, TLS handshakes (success/failure), authentication (success/failure), sessions per protocol, the start time (`hmailserver_start_time_seconds`), database connectivity (`hmailserver_database_connected`, proved by a round trip, plus pool gauges), the SMTP delivery-queue depth and oldest-message age, certificate expiry, work-queue depth, delivery outcomes (`hmailserver_messages_delivered_total`/`_deferred_total`/`_bounced_total`), the message-store consistency result (`hmailserver_messagestore_missing_files`), and per-command and per-query latency histograms (`hmailserver_command_processing_seconds`, `hmailserver_db_query_seconds`).
374388

375-
REST endpoints: `/api/v1/status`, `/api/v1/domains`, `/api/v1/domains/<name>/accounts` (GET/POST), `/api/v1/accounts/<address>` (DELETE), `/api/v1/queue` (GET), `/api/v1/queue/<id>/retry` (POST), `/api/v1/queue/<id>` (DELETE), `/api/v1/tlsa` (GET, publish-ready DANE TLSA records).
389+
REST endpoints: `/api/v1/status`, `/api/v1/domains`, `/api/v1/domains/<name>/accounts` (GET/POST), `/api/v1/accounts/<address>` (DELETE), `/api/v1/queue` (GET), `/api/v1/queue/<id>/retry` (POST), `/api/v1/queue/<id>` (DELETE), `/api/v1/apikeys` (GET/POST, administrator password only), `/api/v1/apikeys/<id>` (DELETE), `/api/v1/tlsa` (GET, publish-ready DANE TLSA records).
376390

377391
Secret protection and least-privilege:
378392

0 commit comments

Comments
 (0)