vuls: init at 0.27.0
[NixPkgs.git] / nixos / modules / services / mail / sympa.nix
blobc83844516557c40d3d0458d42963e89cdf5d8e78
1 { config, lib, pkgs, ... }:
2 let
3   cfg = config.services.sympa;
4   dataDir = "/var/lib/sympa";
5   user = "sympa";
6   group = "sympa";
7   pkg = pkgs.sympa;
8   fqdns = lib.attrNames cfg.domains;
9   usingNginx = cfg.web.enable && cfg.web.server == "nginx";
10   mysqlLocal = cfg.database.createLocally && cfg.database.type == "MySQL";
11   pgsqlLocal = cfg.database.createLocally && cfg.database.type == "PostgreSQL";
13   sympaSubServices = [
14     "sympa-archive.service"
15     "sympa-bounce.service"
16     "sympa-bulk.service"
17     "sympa-task.service"
18   ];
20   # common for all services including wwsympa
21   commonServiceConfig = {
22     StateDirectory = "sympa";
23     ProtectHome = true;
24     ProtectSystem = "full";
25     ProtectControlGroups = true;
26   };
28   # wwsympa has its own service config
29   sympaServiceConfig = srv: {
30     Type = "simple";
31     Restart = "always";
32     ExecStart = "${pkg}/bin/${srv}.pl --foreground";
33     PIDFile = "/run/sympa/${srv}.pid";
34     User = user;
35     Group = group;
37     # avoid duplicating log messageges in journal
38     StandardError = "null";
39   } // commonServiceConfig;
41   configVal = value:
42     if lib.isBool value then
43       if value then "on" else "off"
44     else toString value;
45   configGenerator = c: lib.concatStrings (lib.flip lib.mapAttrsToList c (key: val: "${key}\t${configVal val}\n"));
47   mainConfig = pkgs.writeText "sympa.conf" (configGenerator cfg.settings);
48   robotConfig = fqdn: domain: pkgs.writeText "${fqdn}-robot.conf" (configGenerator domain.settings);
50   transport = pkgs.writeText "transport.sympa" (lib.concatStringsSep "\n" (lib.flip map fqdns (domain: ''
51     ${domain}                        error:User unknown in recipient table
52     sympa@${domain}                  sympa:sympa@${domain}
53     listmaster@${domain}             sympa:listmaster@${domain}
54     bounce@${domain}                 sympabounce:sympa@${domain}
55     abuse-feedback-report@${domain}  sympabounce:sympa@${domain}
56   '')));
58   virtual = pkgs.writeText "virtual.sympa" (lib.concatStringsSep "\n" (lib.flip map fqdns (domain: ''
59     sympa-request@${domain}  postmaster@localhost
60     sympa-owner@${domain}    postmaster@localhost
61   '')));
63   listAliases = pkgs.writeText "list_aliases.tt2" ''
64     #--- [% list.name %]@[% list.domain %]: list transport map created at [% date %]
65     [% list.name %]@[% list.domain %] sympa:[% list.name %]@[% list.domain %]
66     [% list.name %]-request@[% list.domain %] sympa:[% list.name %]-request@[% list.domain %]
67     [% list.name %]-editor@[% list.domain %] sympa:[% list.name %]-editor@[% list.domain %]
68     #[% list.name %]-subscribe@[% list.domain %] sympa:[% list.name %]-subscribe@[%list.domain %]
69     [% list.name %]-unsubscribe@[% list.domain %] sympa:[% list.name %]-unsubscribe@[% list.domain %]
70     [% list.name %][% return_path_suffix %]@[% list.domain %] sympabounce:[% list.name %]@[% list.domain %]
71   '';
73   enabledFiles = lib.filterAttrs (n: v: v.enable) cfg.settingsFile;
77   ###### interface
78   options.services.sympa = with lib.types; {
80     enable = lib.mkEnableOption "Sympa mailing list manager";
82     lang = lib.mkOption {
83       type = str;
84       default = "en_US";
85       example = "cs";
86       description = ''
87         Default Sympa language.
88         See <https://github.com/sympa-community/sympa/tree/sympa-6.2/po/sympa>
89         for available options.
90       '';
91     };
93     listMasters = lib.mkOption {
94       type = listOf str;
95       example = [ "postmaster@sympa.example.org" ];
96       description = ''
97         The list of the email addresses of the listmasters
98         (users authorized to perform global server commands).
99       '';
100     };
102     mainDomain = lib.mkOption {
103       type = nullOr str;
104       default = null;
105       example = "lists.example.org";
106       description = ''
107         Main domain to be used in {file}`sympa.conf`.
108         If `null`, one of the {option}`services.sympa.domains` is chosen for you.
109       '';
110     };
112     domains = lib.mkOption {
113       type = attrsOf (submodule ({ name, config, ... }: {
114         options = {
115           webHost = lib.mkOption {
116             type = nullOr str;
117             default = null;
118             example = "archive.example.org";
119             description = ''
120               Domain part of the web interface URL (no web interface for this domain if `null`).
121               DNS record of type A (or AAAA or CNAME) has to exist with this value.
122             '';
123           };
124           webLocation = lib.mkOption {
125             type = str;
126             default = "/";
127             example = "/sympa";
128             description = "URL path part of the web interface.";
129           };
130           settings = lib.mkOption {
131             type = attrsOf (oneOf [ str int bool ]);
132             default = {};
133             example = {
134               default_max_list_members = 3;
135             };
136             description = ''
137               The {file}`robot.conf` configuration file as key value set.
138               See <https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html>
139               for list of configuration parameters.
140             '';
141           };
142         };
144         config.settings = lib.mkIf (cfg.web.enable && config.webHost != null) {
145           wwsympa_url = lib.mkDefault "https://${config.webHost}${lib.removeSuffix "/" config.webLocation}";
146         };
147       }));
149       description = ''
150         Email domains handled by this instance. There have
151         to be MX records for keys of this attribute set.
152       '';
153       example = lib.literalExpression ''
154         {
155           "lists.example.org" = {
156             webHost = "lists.example.org";
157             webLocation = "/";
158           };
159           "sympa.example.com" = {
160             webHost = "example.com";
161             webLocation = "/sympa";
162           };
163         }
164       '';
165     };
167     database = {
168       type = lib.mkOption {
169         type = enum [ "SQLite" "PostgreSQL" "MySQL" ];
170         default = "SQLite";
171         example = "MySQL";
172         description = "Database engine to use.";
173       };
175       host = lib.mkOption {
176         type = nullOr str;
177         default = null;
178         description = ''
179           Database host address.
181           For MySQL, use `localhost` to connect using Unix domain socket.
183           For PostgreSQL, use path to directory (e.g. {file}`/run/postgresql`)
184           to connect using Unix domain socket located in this directory.
186           Use `null` to fall back on Sympa default, or when using
187           {option}`services.sympa.database.createLocally`.
188         '';
189       };
191       port = lib.mkOption {
192         type = nullOr port;
193         default = null;
194         description = "Database port. Use `null` for default port.";
195       };
197       name = lib.mkOption {
198         type = str;
199         default = if cfg.database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa";
200         defaultText = lib.literalExpression ''if database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa"'';
201         description = ''
202           Database name. When using SQLite this must be an absolute
203           path to the database file.
204         '';
205       };
207       user = lib.mkOption {
208         type = nullOr str;
209         default = user;
210         description = "Database user. The system user name is used as a default.";
211       };
213       passwordFile = lib.mkOption {
214         type = nullOr path;
215         default = null;
216         example = "/run/keys/sympa-dbpassword";
217         description = ''
218           A file containing the password for {option}`services.sympa.database.name`.
219         '';
220       };
222       createLocally = lib.mkOption {
223         type = bool;
224         default = true;
225         description = "Whether to create a local database automatically.";
226       };
227     };
229     web = {
230       enable = lib.mkOption {
231         type = bool;
232         default = true;
233         description = "Whether to enable Sympa web interface.";
234       };
236       server = lib.mkOption {
237         type = enum [ "nginx" "none" ];
238         default = "nginx";
239         description = ''
240           The webserver used for the Sympa web interface. Set it to `none` if you want to configure it yourself.
241           Further nginx configuration can be done by adapting
242           {option}`services.nginx.virtualHosts.«name»`.
243         '';
244       };
246       https = lib.mkOption {
247         type = bool;
248         default = true;
249         description = ''
250           Whether to use HTTPS. When nginx integration is enabled, this option forces SSL and enables ACME.
251           Please note that Sympa web interface always uses https links even when this option is disabled.
252         '';
253       };
255       fcgiProcs = lib.mkOption {
256         type = ints.positive;
257         default = 2;
258         description = "Number of FastCGI processes to fork.";
259       };
260     };
262     mta = {
263       type = lib.mkOption {
264         type = enum [ "postfix" "none" ];
265         default = "postfix";
266         description = ''
267           Mail transfer agent (MTA) integration. Use `none` if you want to configure it yourself.
269           The `postfix` integration sets up local Postfix instance that will pass incoming
270           messages from configured domains to Sympa. You still need to configure at least outgoing message
271           handling using e.g. {option}`services.postfix.relayHost`.
272         '';
273       };
274     };
276     settings = lib.mkOption {
277       type = attrsOf (oneOf [ str int bool ]);
278       default = {};
279       example = lib.literalExpression ''
280         {
281           default_home = "lists";
282           viewlogs_page_size = 50;
283         }
284       '';
285       description = ''
286         The {file}`sympa.conf` configuration file as key value set.
287         See <https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html>
288         for list of configuration parameters.
289       '';
290     };
292     settingsFile = lib.mkOption {
293       type = attrsOf (submodule ({ name, config, ... }: {
294         options = {
295           enable = lib.mkOption {
296             type = bool;
297             default = true;
298             description = "Whether this file should be generated. This option allows specific files to be disabled.";
299           };
300           text = lib.mkOption {
301             default = null;
302             type = nullOr lines;
303             description = "Text of the file.";
304           };
305           source = lib.mkOption {
306             type = path;
307             description = "Path of the source file.";
308           };
309         };
311         config.source = lib.mkIf (config.text != null) (lib.mkDefault (pkgs.writeText "sympa-${baseNameOf name}" config.text));
312       }));
313       default = {};
314       example = lib.literalExpression ''
315         {
316           "list_data/lists.example.org/help" = {
317             text = "subject This list provides help to users";
318           };
319         }
320       '';
321       description = "Set of files to be linked in {file}`${dataDir}`.";
322     };
323   };
325   ###### implementation
327   config = lib.mkIf cfg.enable {
329     services.sympa.settings = (lib.mapAttrs (_: v: lib.mkDefault v) {
330       domain     = if cfg.mainDomain != null then cfg.mainDomain else lib.head fqdns;
331       listmaster = lib.concatStringsSep "," cfg.listMasters;
332       lang       = cfg.lang;
334       home        = "${dataDir}/list_data";
335       arc_path    = "${dataDir}/arc";
336       bounce_path = "${dataDir}/bounce";
338       sendmail = "${pkgs.system-sendmail}/bin/sendmail";
340       db_type = cfg.database.type;
341       db_name = cfg.database.name;
342       db_user = cfg.database.name;
343     }
344     // (lib.optionalAttrs (cfg.database.host != null) {
345       db_host = cfg.database.host;
346     })
347     // (lib.optionalAttrs mysqlLocal {
348       db_host = "localhost"; # use unix domain socket
349     })
350     // (lib.optionalAttrs pgsqlLocal {
351       db_host = "/run/postgresql"; # use unix domain socket
352     })
353     // (lib.optionalAttrs (cfg.database.port != null) {
354       db_port = cfg.database.port;
355     })
356     // (lib.optionalAttrs (cfg.mta.type == "postfix") {
357       sendmail_aliases = "${dataDir}/sympa_transport";
358       aliases_program  = "${pkgs.postfix}/bin/postmap";
359       aliases_db_type  = "hash";
360     })
361     // (lib.optionalAttrs cfg.web.enable {
362       static_content_path = "${dataDir}/static_content";
363       css_path            = "${dataDir}/static_content/css";
364       pictures_path       = "${dataDir}/static_content/pictures";
365       mhonarc             = "${pkgs.perlPackages.MHonArc}/bin/mhonarc";
366     }));
368     services.sympa.settingsFile = {
369       "virtual.sympa"        = lib.mkDefault { source = virtual; };
370       "transport.sympa"      = lib.mkDefault { source = transport; };
371       "etc/list_aliases.tt2" = lib.mkDefault { source = listAliases; };
372     }
373     // (lib.flip lib.mapAttrs' cfg.domains (fqdn: domain:
374           lib.nameValuePair "etc/${fqdn}/robot.conf" (lib.mkDefault { source = robotConfig fqdn domain; })));
376     environment = {
377       systemPackages = [ pkg ];
378     };
380     users.users.${user} = {
381       description = "Sympa mailing list manager user";
382       group = group;
383       home = dataDir;
384       createHome = false;
385       isSystemUser = true;
386     };
388     users.groups.${group} = {};
390     assertions = [
391       { assertion = cfg.database.createLocally -> cfg.database.user == user && cfg.database.name == cfg.database.user;
392         message = "services.sympa.database.user must be set to ${user} if services.sympa.database.createLocally is set to true";
393       }
394       { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
395         message = "a password cannot be specified if services.sympa.database.createLocally is set to true";
396       }
397     ];
399     systemd.tmpfiles.rules = [
400       "d  ${dataDir}                   0711 ${user} ${group} - -"
401       "d  ${dataDir}/etc               0700 ${user} ${group} - -"
402       "d  ${dataDir}/spool             0700 ${user} ${group} - -"
403       "d  ${dataDir}/list_data         0700 ${user} ${group} - -"
404       "d  ${dataDir}/arc               0700 ${user} ${group} - -"
405       "d  ${dataDir}/bounce            0700 ${user} ${group} - -"
406       "f  ${dataDir}/sympa_transport   0600 ${user} ${group} - -"
408       # force-copy static_content so it's up to date with package
409       # set permissions for wwsympa which needs write access (...)
410       "R  ${dataDir}/static_content    -    -       -        - -"
411       "C  ${dataDir}/static_content    0711 ${user} ${group} - ${pkg}/var/lib/sympa/static_content"
412       "e  ${dataDir}/static_content/*  0711 ${user} ${group} - -"
414       "d  /run/sympa                   0755 ${user} ${group} - -"
415     ]
416     ++ (lib.flip lib.concatMap fqdns (fqdn: [
417       "d  ${dataDir}/etc/${fqdn}       0700 ${user} ${group} - -"
418       "d  ${dataDir}/list_data/${fqdn} 0700 ${user} ${group} - -"
419     ]))
420     #++ (lib.flip lib.mapAttrsToList enabledFiles (k: v:
421     #  "L+ ${dataDir}/${k}              -    -       -        - ${v.source}"
422     #))
423     ++ (lib.concatLists (lib.flip lib.mapAttrsToList enabledFiles (k: v: [
424       # sympa doesn't handle symlinks well (e.g. fails to create locks)
425       # force-copy instead
426       "R ${dataDir}/${k}              -    -       -        - -"
427       "C ${dataDir}/${k}              0700 ${user}  ${group} - ${v.source}"
428     ])));
430     systemd.services.sympa = {
431       description = "Sympa mailing list manager";
433       wantedBy = [ "multi-user.target" ];
434       after = [ "network-online.target" ];
435       wants = sympaSubServices ++ [ "network-online.target" ];
436       before = sympaSubServices;
437       serviceConfig = sympaServiceConfig "sympa_msg";
439       preStart = ''
440         umask 0077
442         cp -f ${mainConfig} ${dataDir}/etc/sympa.conf
443         ${lib.optionalString (cfg.database.passwordFile != null) ''
444           chmod u+w ${dataDir}/etc/sympa.conf
445           echo -n "db_passwd " >> ${dataDir}/etc/sympa.conf
446           cat ${cfg.database.passwordFile} >> ${dataDir}/etc/sympa.conf
447         ''}
449         ${lib.optionalString (cfg.mta.type == "postfix") ''
450           ${pkgs.postfix}/bin/postmap hash:${dataDir}/virtual.sympa
451           ${pkgs.postfix}/bin/postmap hash:${dataDir}/transport.sympa
452         ''}
453         ${pkg}/bin/sympa_newaliases.pl
454         ${pkg}/bin/sympa.pl --health_check
455       '';
456     };
457     systemd.services.sympa-archive = {
458       description = "Sympa mailing list manager (archiving)";
459       bindsTo = [ "sympa.service" ];
460       serviceConfig = sympaServiceConfig "archived";
461     };
462     systemd.services.sympa-bounce = {
463       description = "Sympa mailing list manager (bounce processing)";
464       bindsTo = [ "sympa.service" ];
465       serviceConfig = sympaServiceConfig "bounced";
466     };
467     systemd.services.sympa-bulk = {
468       description = "Sympa mailing list manager (message distribution)";
469       bindsTo = [ "sympa.service" ];
470       serviceConfig = sympaServiceConfig "bulk";
471     };
472     systemd.services.sympa-task = {
473       description = "Sympa mailing list manager (task management)";
474       bindsTo = [ "sympa.service" ];
475       serviceConfig = sympaServiceConfig "task_manager";
476     };
478     systemd.services.wwsympa = lib.mkIf usingNginx {
479       wantedBy = [ "multi-user.target" ];
480       after = [ "sympa.service" ];
481       serviceConfig = {
482         Type = "forking";
483         PIDFile = "/run/sympa/wwsympa.pid";
484         Restart = "always";
485         ExecStart = ''${pkgs.spawn_fcgi}/bin/spawn-fcgi \
486           -u ${user} \
487           -g ${group} \
488           -U nginx \
489           -M 0600 \
490           -F ${toString cfg.web.fcgiProcs} \
491           -P /run/sympa/wwsympa.pid \
492           -s /run/sympa/wwsympa.socket \
493           -- ${pkg}/lib/sympa/cgi/wwsympa.fcgi
494         '';
496       } // commonServiceConfig;
497     };
499     services.nginx.enable = lib.mkIf usingNginx true;
500     services.nginx.virtualHosts = lib.mkIf usingNginx (let
501       vHosts = lib.unique (lib.remove null (lib.mapAttrsToList (_k: v: v.webHost) cfg.domains));
502       hostLocations = host: map (v: v.webLocation) (lib.filter (v: v.webHost == host) (lib.attrValues cfg.domains));
503       httpsOpts = lib.optionalAttrs cfg.web.https { forceSSL = lib.mkDefault true; enableACME = lib.mkDefault true; };
504     in
505     lib.genAttrs vHosts (host: {
506       locations = lib.genAttrs (hostLocations host) (loc: {
507         extraConfig = ''
508           include ${config.services.nginx.package}/conf/fastcgi_params;
510           fastcgi_pass unix:/run/sympa/wwsympa.socket;
511         '';
512       }) // {
513         "/static-sympa/".alias = "${dataDir}/static_content/";
514       };
515     } // httpsOpts));
517     services.postfix = lib.mkIf (cfg.mta.type == "postfix") {
518       enable = true;
519       recipientDelimiter = "+";
520       config = {
521         virtual_alias_maps = [ "hash:${dataDir}/virtual.sympa" ];
522         virtual_mailbox_maps = [
523           "hash:${dataDir}/transport.sympa"
524           "hash:${dataDir}/sympa_transport"
525           "hash:${dataDir}/virtual.sympa"
526         ];
527         virtual_mailbox_domains = [ "hash:${dataDir}/transport.sympa" ];
528         transport_maps = [
529           "hash:${dataDir}/transport.sympa"
530           "hash:${dataDir}/sympa_transport"
531         ];
532       };
533       masterConfig = {
534         "sympa" = {
535           type = "unix";
536           privileged = true;
537           chroot = false;
538           command = "pipe";
539           args = [
540             "flags=hqRu"
541             "user=${user}"
542             "argv=${pkg}/libexec/queue"
543             "\${nexthop}"
544           ];
545         };
546         "sympabounce" = {
547           type = "unix";
548           privileged = true;
549           chroot = false;
550           command = "pipe";
551           args = [
552             "flags=hqRu"
553             "user=${user}"
554             "argv=${pkg}/libexec/bouncequeue"
555             "\${nexthop}"
556           ];
557         };
558       };
559     };
561     services.mysql = lib.optionalAttrs mysqlLocal {
562       enable = true;
563       package = lib.mkDefault pkgs.mariadb;
564       ensureDatabases = [ cfg.database.name ];
565       ensureUsers = [
566         { name = cfg.database.user;
567           ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
568         }
569       ];
570     };
572     services.postgresql = lib.optionalAttrs pgsqlLocal {
573       enable = true;
574       ensureDatabases = [ cfg.database.name ];
575       ensureUsers = [
576         { name = cfg.database.user;
577           ensureDBOwnership = true;
578         }
579       ];
580     };
582   };
584   meta.maintainers = with lib.maintainers; [ mmilata sorki ];