grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / mail / maddy.nix
blobab0e1f40f5b09819e87cae710c8fc81eb0a1821f
1 { config, lib, pkgs, ... }:
2 let
4   name = "maddy";
6   cfg = config.services.maddy;
8   defaultConfig = ''
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 {
14       table sql_table {
15         driver sqlite3
16         dsn credentials.db
17         table_name passwords
18       }
19     }
21     storage.imapsql local_mailboxes {
22       driver sqlite3
23       dsn imapsql.db
24     }
26     table.chain local_rewrites {
27       optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
28       optional_step static {
29         entry postmaster postmaster@$(primary_domain)
30       }
31       optional_step file /etc/maddy/aliases
32     }
34     msgpipeline local_routing {
35       destination postmaster $(local_domains) {
36         modify {
37           replace_rcpt &local_rewrites
38         }
39         deliver_to &local_mailboxes
40       }
41       default_destination {
42         reject 550 5.1.1 "User doesn't exist"
43       }
44     }
46     smtp tcp://0.0.0.0:25 {
47       limits {
48         all rate 20 1s
49         all concurrency 10
50       }
51       dmarc yes
52       check {
53         require_mx_record
54         dkim
55         spf
56       }
57       source $(local_domains) {
58         reject 501 5.1.8 "Use Submission for outgoing SMTP"
59       }
60       default_source {
61         destination postmaster $(local_domains) {
62           deliver_to &local_routing
63         }
64         default_destination {
65           reject 550 5.1.1 "User doesn't exist"
66         }
67       }
68     }
70     submission tcp://0.0.0.0:587 {
71       limits {
72         all rate 50 1s
73       }
74       auth &local_authdb
75       source $(local_domains) {
76         check {
77             authorize_sender {
78                 prepare_email &local_rewrites
79                 user_to_email identity
80             }
81         }
82         destination postmaster $(local_domains) {
83             deliver_to &local_routing
84         }
85         default_destination {
86             modify {
87                 dkim $(primary_domain) $(local_domains) default
88             }
89             deliver_to &remote_queue
90         }
91       }
92       default_source {
93         reject 501 5.1.8 "Non-local sender domain"
94       }
95     }
97     target.remote outbound_delivery {
98       limits {
99         destination rate 20 1s
100         destination concurrency 10
101       }
102       mx_auth {
103         dane
104         mtasts {
105           cache fs
106           fs_dir mtasts_cache/
107         }
108         local_policy {
109             min_tls_level encrypted
110             min_mx_level none
111         }
112       }
113     }
115     target.queue remote_queue {
116       target &outbound_delivery
117       autogenerated_msg_domain $(primary_domain)
118       bounce {
119         destination postmaster $(local_domains) {
120           deliver_to &local_routing
121         }
122         default_destination {
123             reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
124         }
125       }
126     }
128     imap tcp://0.0.0.0:143 {
129       auth &local_authdb
130       storage &local_mailboxes
131     }
132   '';
134 in {
135   options = {
136     services.maddy = {
138       enable = lib.mkEnableOption "Maddy, a free an open source mail server";
140       user = lib.mkOption {
141         default = "maddy";
142         type = with lib.types; uniq str;
143         description = ''
144           User account under which maddy runs.
146           ::: {.note}
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.
150           :::
151         '';
152       };
154       group = lib.mkOption {
155         default = "maddy";
156         type = with lib.types; uniq str;
157         description = ''
158           Group account under which maddy runs.
160           ::: {.note}
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.
164           :::
165         '';
166       };
168       hostname = lib.mkOption {
169         default = "localhost";
170         type = with lib.types; uniq str;
171         example = ''example.com'';
172         description = ''
173           Hostname to use. It should be FQDN.
174         '';
175       };
177       primaryDomain = lib.mkOption {
178         default = "localhost";
179         type = with lib.types; uniq str;
180         example = ''mail.example.com'';
181         description = ''
182           Primary MX domain to use. It should be FQDN.
183         '';
184       };
186       localDomains = lib.mkOption {
187         type = with lib.types; listOf str;
188         default = ["$(primary_domain)"];
189         example = [
190           "$(primary_domain)"
191           "example.com"
192           "other.example.com"
193         ];
194         description = ''
195           Define list of allowed domains.
196         '';
197       };
199       config = lib.mkOption {
200         type = with lib.types; nullOr lines;
201         default = defaultConfig;
202         description = ''
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.
208           ::: {.note}
209           This should not be used in a production environment.
210           :::
211         '';
212       };
214       tls = {
215         loader = lib.mkOption {
216           type = with lib.types; nullOr (enum [ "off" "file" "acme" ]);
217           default = "off";
218           description = ''
219             TLS certificates are obtained by modules called "certificate
220             loaders".
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
233             `{env:VAR}`.
234           '';
235         };
237         certificates = lib.mkOption {
238           type = with lib.types; listOf (submodule {
239             options = {
240               keyPath = lib.mkOption {
241                 type = lib.types.path;
242                 example = "/etc/ssl/mx1.example.org.key";
243                 description = ''
244                   Path to the private key used for TLS.
245                 '';
246               };
247               certPath = lib.mkOption {
248                 type = lib.types.path;
249                 example = "/etc/ssl/mx1.example.org.crt";
250                 description = ''
251                   Path to the certificate used for TLS.
252                 '';
253               };
254             };
255           });
256           default = [];
257           example = lib.literalExpression ''
258             [{
259               keyPath = "/etc/ssl/mx1.example.org.key";
260               certPath = "/etc/ssl/mx1.example.org.crt";
261             }]
262           '';
263           description = ''
264             A list of attribute sets containing paths to TLS certificates and
265             keys. Maddy will use SNI if multiple pairs are selected.
266           '';
267         };
269         extraConfig = lib.mkOption {
270           type = with lib.types; nullOr lines;
271           description = ''
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).
279           '';
280           default = "";
281         };
282       };
284       openFirewall = lib.mkOption {
285         type = lib.types.bool;
286         default = false;
287         description = ''
288           Open the configured incoming and outgoing mail server ports.
289         '';
290       };
292       ensureAccounts = lib.mkOption {
293         type = with lib.types; listOf str;
294         default = [];
295         description = ''
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.
300         '';
301         example = [
302           "user1@localhost"
303           "user2@localhost"
304         ];
305       };
307       ensureCredentials = lib.mkOption {
308         default = {};
309         description = ''
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.
314         '';
315         example = {
316           "user1@localhost".passwordFile = /secrets/user1-localhost;
317           "user2@localhost".passwordFile = /secrets/user2-localhost;
318         };
319         type = lib.types.attrsOf (lib.types.submodule {
320           options = {
321             passwordFile = lib.mkOption {
322               type = lib.types.path;
323               example = "/path/to/file";
324               default = null;
325               description = ''
326                 Specifies the path to a file containing the
327                 clear text password for the user.
328               '';
329             };
330           };
331         });
332       };
334       secrets = lib.mkOption {
335         type = with lib.types; listOf path;
336         description = ''
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}`.
340         '';
341         default = [ ];
342       };
344     };
345   };
347   config = lib.mkIf cfg.enable {
349     assertions = [
350       {
351         assertion = cfg.tls.loader == "file" -> cfg.tls.certificates != [];
352         message = ''
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
357         '';
358       }
359       {
360         assertion = cfg.tls.loader == "acme" -> cfg.tls.extraConfig != "";
361         message = ''
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
367         '';
368       }
369     ];
371     systemd = {
373       packages = [ pkgs.maddy ];
374       services = {
375         maddy = {
376           serviceConfig = {
377             User = cfg.user;
378             Group = cfg.group;
379             StateDirectory = [ "maddy" ];
380             EnvironmentFile = cfg.secrets;
381           };
382           restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
383           wantedBy = [ "multi-user.target" ];
384         };
385         maddy-ensure-accounts = {
386           script = ''
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}
391                 fi
392               '') cfg.ensureAccounts}
393             ''}
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}
398                 fi
399               '') cfg.ensureCredentials)}
400             ''}
401           '';
402           serviceConfig = {
403             Type = "oneshot";
404             User= "maddy";
405           };
406           after = [ "maddy.service" ];
407           wantedBy = [ "multi-user.target" ];
408         };
410       };
412     };
414     environment.etc."maddy/maddy.conf" = {
415       text = ''
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} }
426           ''}
427         '' else if (cfg.tls.loader == "acme") then ''
428           tls {
429             loader acme {
430               ${cfg.tls.extraConfig}
431             }
432           }
433         '' else if (cfg.tls.loader == "off") then ''
434           tls off
435         '' else ""}
437         ${cfg.config}
438       '';
439     };
441     users.users = lib.optionalAttrs (cfg.user == name) {
442       ${name} = {
443         isSystemUser = true;
444         group = cfg.group;
445         description = "Maddy mail transfer agent user";
446       };
447     };
449     users.groups = lib.optionalAttrs (cfg.group == name) {
450       ${cfg.group} = { };
451     };
453     networking.firewall = lib.mkIf cfg.openFirewall {
454       allowedTCPPorts = [ 25 143 587 ];
455     };
457     environment.systemPackages = [
458       pkgs.maddy
459     ];
460   };