1 { config, lib, pkgs, ... }:
6 cfg = config.services.maddy;
9 # Minimal configuration with TLS disabled, adapted from upstream example
10 # configuration here https://github.com/foxcpp/maddy/blob/master/maddy.conf
11 # Do not use this in production!
13 auth.pass_table local_authdb {
21 storage.imapsql local_mailboxes {
26 table.chain local_rewrites {
27 optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
28 optional_step static {
29 entry postmaster postmaster@$(primary_domain)
31 optional_step file /etc/maddy/aliases
34 msgpipeline local_routing {
35 destination postmaster $(local_domains) {
37 replace_rcpt &local_rewrites
39 deliver_to &local_mailboxes
42 reject 550 5.1.1 "User doesn't exist"
46 smtp tcp://0.0.0.0:25 {
57 source $(local_domains) {
58 reject 501 5.1.8 "Use Submission for outgoing SMTP"
61 destination postmaster $(local_domains) {
62 deliver_to &local_routing
65 reject 550 5.1.1 "User doesn't exist"
70 submission tcp://0.0.0.0:587 {
75 source $(local_domains) {
78 prepare_email &local_rewrites
79 user_to_email identity
82 destination postmaster $(local_domains) {
83 deliver_to &local_routing
87 dkim $(primary_domain) $(local_domains) default
89 deliver_to &remote_queue
93 reject 501 5.1.8 "Non-local sender domain"
97 target.remote outbound_delivery {
99 destination rate 20 1s
100 destination concurrency 10
109 min_tls_level encrypted
115 target.queue remote_queue {
116 target &outbound_delivery
117 autogenerated_msg_domain $(primary_domain)
119 destination postmaster $(local_domains) {
120 deliver_to &local_routing
122 default_destination {
123 reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
128 imap tcp://0.0.0.0:143 {
130 storage &local_mailboxes
138 enable = lib.mkEnableOption "Maddy, a free an open source mail server";
140 user = lib.mkOption {
142 type = with lib.types; uniq str;
144 User account under which maddy runs.
147 If left as the default value this user will automatically be created
148 on system activation, otherwise the sysadmin is responsible for
149 ensuring the user exists before the maddy service starts.
154 group = lib.mkOption {
156 type = with lib.types; uniq str;
158 Group account under which maddy runs.
161 If left as the default value this group will automatically be created
162 on system activation, otherwise the sysadmin is responsible for
163 ensuring the group exists before the maddy service starts.
168 hostname = lib.mkOption {
169 default = "localhost";
170 type = with lib.types; uniq str;
171 example = ''example.com'';
173 Hostname to use. It should be FQDN.
177 primaryDomain = lib.mkOption {
178 default = "localhost";
179 type = with lib.types; uniq str;
180 example = ''mail.example.com'';
182 Primary MX domain to use. It should be FQDN.
186 localDomains = lib.mkOption {
187 type = with lib.types; listOf str;
188 default = ["$(primary_domain)"];
195 Define list of allowed domains.
199 config = lib.mkOption {
200 type = with lib.types; nullOr lines;
201 default = defaultConfig;
203 Server configuration, see
204 [https://maddy.email](https://maddy.email) for
205 more information. The default configuration of this module will setup
206 minimal Maddy instance for mail transfer without TLS encryption.
209 This should not be used in a production environment.
215 loader = lib.mkOption {
216 type = with lib.types; nullOr (enum [ "off" "file" "acme" ]);
219 TLS certificates are obtained by modules called "certificate
222 The `file` loader module reads certificates from files specified by
223 the `certificates` option.
225 Alternatively the `acme` module can be used to automatically obtain
226 certificates using the ACME protocol.
228 Module configuration is done via the `tls.extraConfig` option.
230 Secrets such as API keys or passwords should not be supplied in
231 plaintext. Instead the `secrets` option can be used to read secrets
232 at runtime as environment variables. Secrets can be referenced with
237 certificates = lib.mkOption {
238 type = with lib.types; listOf (submodule {
240 keyPath = lib.mkOption {
241 type = lib.types.path;
242 example = "/etc/ssl/mx1.example.org.key";
244 Path to the private key used for TLS.
247 certPath = lib.mkOption {
248 type = lib.types.path;
249 example = "/etc/ssl/mx1.example.org.crt";
251 Path to the certificate used for TLS.
257 example = lib.literalExpression ''
259 keyPath = "/etc/ssl/mx1.example.org.key";
260 certPath = "/etc/ssl/mx1.example.org.crt";
264 A list of attribute sets containing paths to TLS certificates and
265 keys. Maddy will use SNI if multiple pairs are selected.
269 extraConfig = lib.mkOption {
270 type = with lib.types; nullOr lines;
272 Arguments for the specified certificate loader.
274 In case the `tls` loader is set, the defaults are considered secure
275 and there is no need to change anything in most cases.
276 For available options see [upstream manual](https://maddy.email/reference/tls/).
278 For ACME configuration, see [following page](https://maddy.email/reference/tls-acme).
284 openFirewall = lib.mkOption {
285 type = lib.types.bool;
288 Open the configured incoming and outgoing mail server ports.
292 ensureAccounts = lib.mkOption {
293 type = with lib.types; listOf str;
296 List of IMAP accounts which get automatically created. Note that for
297 a complete setup, user credentials for these accounts are required
298 and can be created using the `ensureCredentials` option.
299 This option does not delete accounts which are not (anymore) listed.
307 ensureCredentials = lib.mkOption {
310 List of user accounts which get automatically created if they don't
311 exist yet. Note that for a complete setup, corresponding mail boxes
312 have to get created using the `ensureAccounts` option.
313 This option does not delete accounts which are not (anymore) listed.
316 "user1@localhost".passwordFile = /secrets/user1-localhost;
317 "user2@localhost".passwordFile = /secrets/user2-localhost;
319 type = lib.types.attrsOf (lib.types.submodule {
321 passwordFile = lib.mkOption {
322 type = lib.types.path;
323 example = "/path/to/file";
326 Specifies the path to a file containing the
327 clear text password for the user.
334 secrets = lib.mkOption {
335 type = with lib.types; listOf path;
337 A list of files containing the various secrets. Should be in the format
338 expected by systemd's `EnvironmentFile` directory. Secrets can be
339 referenced in the format `{env:VAR}`.
347 config = lib.mkIf cfg.enable {
351 assertion = cfg.tls.loader == "file" -> cfg.tls.certificates != [];
353 If Maddy is configured to use TLS, tls.certificates with attribute sets
354 of certPath and keyPath must be provided.
355 Read more about obtaining TLS certificates here:
356 https://maddy.email/tutorials/setting-up/#tls-certificates
360 assertion = cfg.tls.loader == "acme" -> cfg.tls.extraConfig != "";
362 If Maddy is configured to obtain TLS certificates using the ACME
363 loader, extra configuration options must be supplied via
364 tls.extraConfig option.
365 See upstream documentation for more details:
366 https://maddy.email/reference/tls-acme
373 packages = [ pkgs.maddy ];
379 StateDirectory = [ "maddy" ];
380 EnvironmentFile = cfg.secrets;
382 restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
383 wantedBy = [ "multi-user.target" ];
385 maddy-ensure-accounts = {
387 ${lib.optionalString (cfg.ensureAccounts != []) ''
388 ${lib.concatMapStrings (account: ''
389 if ! ${pkgs.maddy}/bin/maddyctl imap-acct list | grep "${account}"; then
390 ${pkgs.maddy}/bin/maddyctl imap-acct create ${account}
392 '') cfg.ensureAccounts}
394 ${lib.optionalString (cfg.ensureCredentials != {}) ''
395 ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: cfg: ''
396 if ! ${pkgs.maddy}/bin/maddyctl creds list | grep "${name}"; then
397 ${pkgs.maddy}/bin/maddyctl creds create --password $(cat ${lib.escapeShellArg cfg.passwordFile}) ${name}
399 '') cfg.ensureCredentials)}
406 after = [ "maddy.service" ];
407 wantedBy = [ "multi-user.target" ];
414 environment.etc."maddy/maddy.conf" = {
416 $(hostname) = ${cfg.hostname}
417 $(primary_domain) = ${cfg.primaryDomain}
418 $(local_domains) = ${toString cfg.localDomains}
419 hostname ${cfg.hostname}
421 ${if (cfg.tls.loader == "file") then ''
422 tls file ${lib.concatStringsSep " " (
423 map (x: x.certPath + " " + x.keyPath
424 ) cfg.tls.certificates)} ${lib.optionalString (cfg.tls.extraConfig != "") ''
425 { ${cfg.tls.extraConfig} }
427 '' else if (cfg.tls.loader == "acme") then ''
430 ${cfg.tls.extraConfig}
433 '' else if (cfg.tls.loader == "off") then ''
441 users.users = lib.optionalAttrs (cfg.user == name) {
445 description = "Maddy mail transfer agent user";
449 users.groups = lib.optionalAttrs (cfg.group == name) {
453 networking.firewall = lib.mkIf cfg.openFirewall {
454 allowedTCPPorts = [ 25 143 587 ];
457 environment.systemPackages = [