Feature Specification: Resource smtp
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/resources/smtp/default.nix, core/resources/smtp/maddy-provider.nix, core/resources/smtp/external-relay.nix, core/setups/wiring/smtp-routing.nix, core/setups/wiring/mail-domains.nix, core/setups/wiring/resource-routing.nix, core/setups/hosts-common.nix, core/checks/integrations/smtp/), pull requests #1184 and #1205, and issue #527.
User Scenarios & Testing (mandatory)
User Story 1 - Give an application a mail submission account (Priority: P1)
An application must send mail, for example a password reset. It states a
request with a sender address. The policy gives the application one
submission account on the relay. The consumer node gets the host, the port,
the username and a password file. The resource sets the want of the
request on the node. A static client module of the component (smtp.nix)
reads contracts.smtp.results.<app>.default, which holds the sender and the
contract result, to configure the mailer. The request holds only data.
Why this priority: password resets and notifications do not work without outbound mail.
Independent Test: contracts-smtp-maddy (a NixOS test of the maddy
provider). nix-unit-resources-plain-requests checks that each smtp
request of a component is plain data. nix-unit-resources-smtp checks the
process and the apply of the resource: one want per application, the
result on the consumer node, the password file of each application, and the
secrets of the relay node.
Acceptance Scenarios:
- Given two enabled requests from two applications, When
processruns, Then it gives one request per application, keyed by the application name. - Given a request from application
<app>, When the deployment is evaluated, Then the provider side getscontracts.smtp.want.<app>.default.request.sender. - Given a routed request, When the consumer side is evaluated, Then
contracts.smtp.results.<app>.defaultholds thesenderof the request,host = <domain>,port(default587) andusername = <sender>. - Given the
maddyprovider, When the relay node is evaluated, Then the password of<app>is at<prefix>/secret/<instanceName>/<app>Passwordin the KV store. - Given the
maddyprovider and two applications on one node, When the node is evaluated, Thencontracts.smtp.results.<app>.default.passwordFileof each application is the rendered password file of its own account,<tmpDir>/secret/<instanceName>/<app>Password.
User Story 2 - Pick the relay per group (Priority: P1)
A group either runs its own maddy relay or submits to an external relay.
The group states this choice in the smtp attribute of its group data
(core/setups/groups/*.nix), and smtp-routing.nix reads that attribute. The
routing knows no group name, host name or secret path. A group on the external
relay provides smtp with no relay node.
Why this priority: the choice decides if the group runs a mail node at all.
Independent Test: nix-unit-setups-smtp-routing.
Acceptance Scenarios:
- Given a group with
smtp.provider = "external-relay", When the routing is evaluated, Then the provider isexternal-relayon the statedsmtp.smarthosthost, port, account and password file. - Given a group on
external-relaythat does not statehost,usernameorpasswordFile, When the routing is evaluated, Then the evaluation stops with an error that names the missing attribute. - Given a group with no
smtp.provider, When the routing is evaluated, Then the provider ismaddy. - Given a group on
maddywithsmtp.smarthost, When the routing is evaluated, Then the relay forwards through the stated host, port and account, and the password comes fromvars/secret/smarthost-upstream/<passwordSeed>. - Given a group on
maddywith nosmtp.smarthost, When the routing is evaluated, Then the relay delivers directly to the recipient MX. - Given a group with a
smtp.providerother thanmaddyorexternal-relay, When the routing is evaluated, Then the evaluation stops with an error.
User Story 3 - Sign outbound mail and accept DMARC reports (Priority: P2)
A receiver checks DKIM and DMARC. The maddy relay signs each message with
one key for the organizational domain. When the operator has an email, the
relay accepts the DMARC aggregate reports and forwards them to that address.
Why this priority: the smarthost rewrites the envelope sender. Only a
DKIM signature aligns with the From: header.
Independent Test: integrations-smtp-dkim and
integrations-smtp-reports.
Acceptance Scenarios:
- Given
dkim.enable = true, When the relay sends a message, Then the message carries a DKIM signature with the selectorfediversity. - Given the generated DKIM key is missing, When
maddystarts, Then the start fails, so maddy does not sign with a key nobody published. - Given
reports.enable = true, When the relay is evaluated, Then the firewall opens port25and mail todmarc@smtp.<domain>goes toreports.forwardTo. - Given
reports.enable = false, When the relay is evaluated, Then the firewall opens onlyport.
Edge Cases
- One application states two different
senderaddresses: an assertion fails at eval. - Two applications state the same
sender: an assertion fails at eval, because the sender is the account username. dkim.enable = trueanddkim.domainis empty: an assertion fails at eval.reports.addressis not under a local mail domain, orreports.forwardTois under one: an assertion fails at eval.- The upstream maddy configuration changes and a
replaceStringsrewrite does not match: an assertion fails at eval. enable = false:applygives nothing.
Requirements (mandatory)
Functional Requirements
- FR-001: The policy
processMUST give one request per application. - FR-002: The policy MUST give no output when
enableis false. - FR-003: The policy MUST give a
mainConfigthat setsrunDaemon = trueon the relay node. - FR-004: The policy MUST give the vars instances of the routed provider as
mainVarsInstances. - FR-005: The policy MUST declare
domaininsslDomains. - FR-006: The
maddyprovider MUST give each application its own account and password. - FR-007: The
maddyprovider MUST usestarttlsandauth plainto the smarthost, and MUST rewrite the envelope sender to the smarthost account. - FR-008: Without a smarthost, the
maddyprovider MUST deliver withmin_tls_level encrypted, DANE and MTA-STS. - FR-009: The
maddyprovider MUST read its TLS certificate fromcontracts.ssl.results.fediversity.<domain>. - FR-010: The
external-relayprovider MUST NOT run a daemon. - FR-011:
mail-domains.nixMUST be the single source of the sender addresses, the DKIM selector and the report address, for the relay and for the zone records. - FR-012: The request MUST contain only data. The consumer side MUST give the request to
contracts.smtpof the node, not as an argument to a function of the request. - FR-013:
smtp-routing.nixMUST take the provider, the smarthost and the account from thesmtpattribute of the group data. It MUST NOT name a group, a host or a secret path.
Key Entities
- smtp request:
enableandsender. It holds only data. - smtp client module: the
smtp.nixof a component. It readscontracts.smtp.results.<app>.defaultandfediversity.smtp.accounts.<app>. - smtp policy:
enable,domain,port,instanceNameandrouting. - maddy provider: the options under
fediversity.smtp, among themsmarthost,dkim,reports, the read-onlyaccountsandgenerateInstances. - external-relay provider:
fediversity.smtp-relay.host,port,usernameandpasswordFile. - Vars instances:
<instanceName>(one password per application),<instanceName>-smarthost(SMTP_SMARTHOST_PASSWORD) and<instanceName>-dkim.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
contracts-smtp-maddy,integrations-smtp-dkimandintegrations-smtp-reportspass. - SC-002:
nix-unit-setups-smtp-routing,nix-unit-setups-lldap-mail,nix-unit-resources-plain-requestsandnix-unit-resources-smtppass. - SC-003: A password reset mail from
lldapor Authelia reaches an external mailbox with a passing DKIM signature.
Assumptions
- The wiring and the setup flow are as in 005. The request and policy model is as in 001.
- The relay is the
smtpcomponent. Its certificate comes from thesslresource. - The consumers include
ldapandoidc. Their senders arenoreply@ldap.<domain>andnoreply@auth.<domain>. - An operator provisions the smarthost accounts on
mail.protagio.nland seeds their passwords by hand.