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