SMTP goes through libcurl, with TLS required except to a loopback literal¶
- Status: accepted
- Deciders: James Chapman
- Date: 2026-10-24
Technical Story: email pusher delivery (0.12.44)
Context and Problem Statement¶
Email pushers (ADR-0219) need an SMTP client. It sends an address this server vouches for, and possibly SMTP AUTH credentials, to a relay the operator names. How the session is protected, where the password lives, and which process may send are choices a later change could loosen without noticing what they protect.
Decision Drivers¶
- No new protocol parser written here if a pinned, maintained one exists.
- An address and credentials must not cross a network in cleartext.
- Secrets stay in owner-only files, held in memory only while used (as every other secret file).
- The federation worker, the process most exposed to hostile input, holds no secret it does not need (ADR-0062).
Considered Options¶
- libcurl's SMTP support (already pinned and used for HTTP), or a hand-written SMTP client.
- For TLS: require it always; require it except to a loopback IP literal; or try STARTTLS and fall back to cleartext.
- For the password: inline in the config file; or in a secret file read at startup and kept; or read from that file for each email.
Decision Outcome¶
Chosen:
- libcurl.
email::send_smtp_messagedrives one SMTP session per email. Peer and host name verification are on and the TLS floor is 1.2. It uses no proxy and no connection reuse, andCURLOPT_PROTOCOLS_STRis restricted to the configured scheme. Startup refuses a configured relay whose scheme the linked libcurl cannot speak, so email pushers are never accepted and then never delivered. - TLS is required except to a loopback IP literal.
smtps://is TLS from the first byte.smtp://demands STARTTLS (CURLUSESSL_ALL) before any envelope is sent, and a relay that does not offer it gets no mail. Cleartext needsserver.email.require_tls=false, and config validation allows that only for a127.0.0.0/8or[::1]literal (a local MTA) and never with credentials. A host name such aslocalhostdoes not qualify, because DNS decides where a name goes. The client refuses both cases again itself. - The password is read from
server.email.password_filefor each email. The file must pass the owner-only secret-file check at startup. It is read into acore::SecretBufferonly for the send, so a rotated password takes effect without a restart. libcurl keeps its own copy for the life of the handle, which is destroyed when the send ends. - Only the main process sends. The federation worker clears
server.emailin its own config copy before starting its runtime, as it clears the master-key path. The password file is also one ofworker_landlock_secret_paths, so the worker refuses to start if a Landlock rule (such as the SQLite directory grant) would let it read the file. The email job thread runs only in main, starts after process hardening (ADR-0082), and never holdsruntime.mutexacross the SMTP session.
Positive Consequences¶
- The SMTP state machine, STARTTLS and SASL are libcurl's, with no new dependency.
- An address or password never crosses a network in cleartext.
Negative Consequences¶
- A relay with a private CA is not supported yet: the system trust store is used (there is no
server.emailCA setting). - libcurl does not wipe its copy of the password when it frees it.
Pros and Cons of the Options¶
A hand-written SMTP client¶
- Good, because it has no libcurl protocol surface.
- Bad, because STARTTLS, SASL and dot-stuffing are easy to get subtly wrong. libcurl is already pinned and audited for this project.
Opportunistic STARTTLS with cleartext fallback¶
- Good, because it works with more relays.
- Bad, because an attacker on the path can strip STARTTLS and read the envelope and credentials.
Password inline in the config file¶
- Good, because it is simpler to configure.
- Bad, because config files are not held to the secret-file rules and are copied to the federation worker.