envision-unwrapped: 0-unstable-2024-10-20 -> 1.1.1 (#360652)
[NixPkgs.git] / nixos / modules / services / mail / mailman.nix
blob326052731a31aa2b2b1254d42b9b6748cfb9d4cf
1 { config, pkgs, lib, ... }:
2 let
4   cfg = config.services.mailman;
6   inherit (pkgs.mailmanPackages.buildEnvs { withHyperkitty = cfg.hyperkitty.enable; withLDAP = cfg.ldap.enable; })
7     mailmanEnv webEnv;
9   withPostgresql = config.services.postgresql.enable;
11   # This deliberately doesn't use recursiveUpdate so users can
12   # override the defaults.
13   webSettings = {
14     DEFAULT_FROM_EMAIL = cfg.siteOwner;
15     SERVER_EMAIL = cfg.siteOwner;
16     ALLOWED_HOSTS = [ "localhost" "127.0.0.1" ] ++ cfg.webHosts;
17     COMPRESS_OFFLINE = true;
18     STATIC_ROOT = "/var/lib/mailman-web-static";
19     MEDIA_ROOT = "/var/lib/mailman-web/media";
20     LOGGING = {
21       version = 1;
22       disable_existing_loggers = true;
23       handlers.console.class = "logging.StreamHandler";
24       loggers.django = {
25         handlers = [ "console" ];
26         level = "INFO";
27       };
28     };
29     HAYSTACK_CONNECTIONS.default = {
30       ENGINE = "haystack.backends.whoosh_backend.WhooshEngine";
31       PATH = "/var/lib/mailman-web/fulltext-index";
32     };
33   } // lib.optionalAttrs cfg.enablePostfix {
34     EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend";
35     EMAIL_HOST = "127.0.0.1";
36     EMAIL_PORT = 25;
37   } // cfg.webSettings;
39   webSettingsJSON = pkgs.writeText "settings.json" (builtins.toJSON webSettings);
41   # TODO: Should this be RFC42-ised so that users can set additional options without modifying the module?
42   postfixMtaConfig = pkgs.writeText "mailman-postfix.cfg" ''
43     [postfix]
44     postmap_command: ${pkgs.postfix}/bin/postmap
45     transport_file_type: hash
46   '';
48   mailmanCfg = lib.generators.toINI {} (lib.recursiveUpdate cfg.settings {
49     webservice.admin_pass = "#NIXOS_MAILMAN_REST_API_PASS_SECRET#";
50   });
52   mailmanCfgFile = pkgs.writeText "mailman-raw.cfg" mailmanCfg;
54   mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
55     [general]
56     # This is your HyperKitty installation, preferably on the localhost. This
57     # address will be used by Mailman to forward incoming emails to HyperKitty
58     # for archiving. It does not need to be publicly available, in fact it's
59     # better if it is not.
60     base_url: ${cfg.hyperkitty.baseUrl}
62     # Shared API key, must be the identical to the value in HyperKitty's
63     # settings.
64     api_key: @API_KEY@
65   '';
67 in {
69   ###### interface
71   imports = [
72     (lib.mkRenamedOptionModule [ "services" "mailman" "hyperkittyBaseUrl" ]
73       [ "services" "mailman" "hyperkitty" "baseUrl" ])
75     (lib.mkRemovedOptionModule [ "services" "mailman" "hyperkittyApiKey" ] ''
76       The Hyperkitty API key is now generated on first run, and not
77       stored in the world-readable Nix store.  To continue using
78       Hyperkitty, you must set services.mailman.hyperkitty.enable = true.
79     '')
80     (lib.mkRemovedOptionModule [ "services" "mailman" "package" ] ''
81       Didn't have an effect for several years.
82     '')
83   ];
85   options = {
87     services.mailman = {
89       enable = lib.mkOption {
90         type = lib.types.bool;
91         default = false;
92         description = "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix).";
93       };
95       ldap = {
96         enable = lib.mkEnableOption "LDAP auth";
97         serverUri = lib.mkOption {
98           type = lib.types.str;
99           example = "ldaps://ldap.host";
100           description = ''
101             LDAP host to connect against.
102           '';
103         };
104         bindDn = lib.mkOption {
105           type = lib.types.str;
106           example = "cn=root,dc=nixos,dc=org";
107           description = ''
108             Service account to bind against.
109           '';
110         };
111         bindPasswordFile = lib.mkOption {
112           type = lib.types.str;
113           example = "/run/secrets/ldap-bind";
114           description = ''
115             Path to the file containing the bind password of the service account
116             defined by [](#opt-services.mailman.ldap.bindDn).
117           '';
118         };
119         superUserGroup = lib.mkOption {
120           type = lib.types.nullOr lib.types.str;
121           default = null;
122           example = "cn=admin,ou=groups,dc=nixos,dc=org";
123           description = ''
124             Group where a user must be a member of to gain superuser rights.
125           '';
126         };
127         userSearch = {
128           query = lib.mkOption {
129             type = lib.types.str;
130             example = "(&(objectClass=inetOrgPerson)(|(uid=%(user)s)(mail=%(user)s)))";
131             description = ''
132               Query to find a user in the LDAP database.
133             '';
134           };
135           ou = lib.mkOption {
136             type = lib.types.str;
137             example = "ou=users,dc=nixos,dc=org";
138             description = ''
139               Organizational unit to look up a user.
140             '';
141           };
142         };
143         groupSearch = {
144           type = lib.mkOption {
145             type = lib.types.enum [
146               "posixGroup" "groupOfNames" "memberDNGroup" "nestedMemberDNGroup" "nestedGroupOfNames"
147               "groupOfUniqueNames" "nestedGroupOfUniqueNames" "activeDirectoryGroup" "nestedActiveDirectoryGroup"
148               "organizationalRoleGroup" "nestedOrganizationalRoleGroup"
149             ];
150             default = "posixGroup";
151             apply = v: "${lib.toUpper (lib.substring 0 1 v)}${lib.substring 1 (lib.stringLength v) v}Type";
152             description = ''
153               Type of group to perform a group search against.
154             '';
155           };
156           query = lib.mkOption {
157             type = lib.types.str;
158             example = "(objectClass=groupOfNames)";
159             description = ''
160               Query to find a group associated to a user in the LDAP database.
161             '';
162           };
163           ou = lib.mkOption {
164             type = lib.types.str;
165             example = "ou=groups,dc=nixos,dc=org";
166             description = ''
167               Organizational unit to look up a group.
168             '';
169           };
170         };
171         attrMap = {
172           username = lib.mkOption {
173             default = "uid";
174             type = lib.types.str;
175             description = ''
176               LDAP-attribute that corresponds to the `username`-attribute in mailman.
177             '';
178           };
179           firstName = lib.mkOption {
180             default = "givenName";
181             type = lib.types.str;
182             description = ''
183               LDAP-attribute that corresponds to the `firstName`-attribute in mailman.
184             '';
185           };
186           lastName = lib.mkOption {
187             default = "sn";
188             type = lib.types.str;
189             description = ''
190               LDAP-attribute that corresponds to the `lastName`-attribute in mailman.
191             '';
192           };
193           email = lib.mkOption {
194             default = "mail";
195             type = lib.types.str;
196             description = ''
197               LDAP-attribute that corresponds to the `email`-attribute in mailman.
198             '';
199           };
200         };
201       };
203       enablePostfix = lib.mkOption {
204         type = lib.types.bool;
205         default = true;
206         example = false;
207         description = ''
208           Enable Postfix integration. Requires an active Postfix installation.
210           If you want to use another MTA, set this option to false and configure
211           settings in services.mailman.settings.mta.
213           Refer to the Mailman manual for more info.
214         '';
215       };
217       siteOwner = lib.mkOption {
218         type = lib.types.str;
219         example = "postmaster@example.org";
220         description = ''
221           Certain messages that must be delivered to a human, but which can't
222           be delivered to a list owner (e.g. a bounce from a list owner), will
223           be sent to this address. It should point to a human.
224         '';
225       };
227       webHosts = lib.mkOption {
228         type = lib.types.listOf lib.types.str;
229         default = [];
230         description = ''
231           The list of hostnames and/or IP addresses from which the Mailman Web
232           UI will accept requests. By default, "localhost" and "127.0.0.1" are
233           enabled. All additional names under which your web server accepts
234           requests for the UI must be listed here or incoming requests will be
235           rejected.
236         '';
237       };
239       webUser = lib.mkOption {
240         type = lib.types.str;
241         default = "mailman-web";
242         description = ''
243           User to run mailman-web as
244         '';
245       };
247       webSettings = lib.mkOption {
248         type = lib.types.attrs;
249         default = {};
250         description = ''
251           Overrides for the default mailman-web Django settings.
252         '';
253       };
255       restApiPassFile = lib.mkOption {
256         default = null;
257         type = lib.types.nullOr lib.types.str;
258         description = ''
259           Path to the file containing the value for `MAILMAN_REST_API_PASS`.
260         '';
261       };
263       serve = {
264         enable = lib.mkEnableOption "automatic nginx and uwsgi setup for mailman-web";
266         uwsgiSettings = lib.mkOption {
267           default = { };
268           example = { uwsgi.buffer-size = 8192; };
269           inherit (pkgs.formats.json {}) type;
270           description = ''
271             Extra configuration to merge into uwsgi config.
272           '';
273         };
275         virtualRoot = lib.mkOption {
276           default = "/";
277           example = lib.literalExpression "/lists";
278           type = lib.types.str;
279           description = ''
280             Path to mount the mailman-web django application on.
281           '';
282         };
283       };
285       extraPythonPackages = lib.mkOption {
286         description = "Packages to add to the python environment used by mailman and mailman-web";
287         type = lib.types.listOf lib.types.package;
288         default = [];
289       };
291       settings = lib.mkOption {
292         description = "Settings for mailman.cfg";
293         type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
294         default = {};
295       };
297       hyperkitty = {
298         enable = lib.mkEnableOption "the Hyperkitty archiver for Mailman";
300         baseUrl = lib.mkOption {
301           type = lib.types.str;
302           default = "http://localhost:18507/archives/";
303           description = ''
304             Where can Mailman connect to Hyperkitty's internal API, preferably on
305             localhost?
306           '';
307         };
308       };
310     };
311   };
313   ###### implementation
315   config = lib.mkIf cfg.enable {
317     services.mailman.settings = {
318       mailman.site_owner = lib.mkDefault cfg.siteOwner;
319       mailman.layout = "fhs";
321       "paths.fhs" = {
322         bin_dir = "${pkgs.mailmanPackages.mailman}/bin";
323         var_dir = "/var/lib/mailman";
324         queue_dir = "$var_dir/queue";
325         template_dir = "$var_dir/templates";
326         log_dir = "/var/log/mailman";
327         lock_dir = "/run/mailman/lock";
328         etc_dir = "/etc";
329         pid_file = "/run/mailman/master.pid";
330       };
332       mta.configuration = lib.mkDefault (if cfg.enablePostfix then "${postfixMtaConfig}" else throw "When Mailman Postfix integration is disabled, set `services.mailman.settings.mta.configuration` to the path of the config file required to integrate with your MTA.");
334       "archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable {
335         class = "mailman_hyperkitty.Archiver";
336         enable = "yes";
337         configuration = "/var/lib/mailman/mailman-hyperkitty.cfg";
338       };
339     } // (let
340       loggerNames = ["root" "archiver" "bounce" "config" "database" "debug" "error" "fromusenet" "http" "locks" "mischief" "plugins" "runner" "smtp"];
341       loggerSectionNames = map (n: "logging.${n}") loggerNames;
342       in lib.genAttrs loggerSectionNames(name: { handler = "stderr"; })
343     );
345     assertions = let
346       inherit (config.services) postfix;
348       requirePostfixHash = optionPath: dataFile:
349         let
350           expected = "hash:/var/lib/mailman/data/${dataFile}";
351           value = lib.attrByPath optionPath [] postfix;
352         in
353           { assertion = postfix.enable -> lib.isList value && lib.elem expected value;
354             message = ''
355               services.postfix.${lib.concatStringsSep "." optionPath} must contain
356               "${expected}".
357               See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
358             '';
359           };
360     in [
361       { assertion = cfg.webHosts != [];
362         message = ''
363           services.mailman.serve.enable requires there to be at least one entry
364           in services.mailman.webHosts.
365         '';
366       }
367     ] ++ (lib.optionals cfg.enablePostfix [
368       { assertion = postfix.enable;
369         message = ''
370           Mailman's default NixOS configuration requires Postfix to be enabled.
372           If you want to use another MTA, set services.mailman.enablePostfix
373           to false and configure settings in services.mailman.settings.mta.
375           Refer to <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>
376           for more info.
377         '';
378       }
379       (requirePostfixHash [ "config" "relay_domains" ] "postfix_domains")
380       (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
381       (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
382     ]);
384     users.users.mailman = {
385       description = "GNU Mailman";
386       isSystemUser = true;
387       group = "mailman";
388     };
389     users.users.mailman-web = lib.mkIf (cfg.webUser == "mailman-web") {
390       description = "GNU Mailman web interface";
391       isSystemUser = true;
392       group = "mailman";
393     };
394     users.groups.mailman = {};
396     environment.etc."mailman3/settings.py".text = ''
397       import os
398       from configparser import ConfigParser
400       # Required by mailman_web.settings, but will be overridden when
401       # settings_local.json is loaded.
402       os.environ["SECRET_KEY"] = ""
404       from mailman_web.settings.base import *
405       from mailman_web.settings.mailman import *
407       import json
409       with open('${webSettingsJSON}') as f:
410           globals().update(json.load(f))
412       with open('/var/lib/mailman-web/settings_local.json') as f:
413           globals().update(json.load(f))
415       with open('/etc/mailman.cfg') as f:
416           config = ConfigParser()
417           config.read_file(f)
418           MAILMAN_REST_API_PASS = config['webservice']['admin_pass']
420       ${lib.optionalString (cfg.ldap.enable) ''
421         import ldap
422         from django_auth_ldap.config import LDAPSearch, ${cfg.ldap.groupSearch.type}
423         AUTH_LDAP_SERVER_URI = "${cfg.ldap.serverUri}"
424         AUTH_LDAP_BIND_DN = "${cfg.ldap.bindDn}"
425         with open("${cfg.ldap.bindPasswordFile}") as f:
426             AUTH_LDAP_BIND_PASSWORD = f.read().rstrip('\n')
427         AUTH_LDAP_USER_SEARCH = LDAPSearch("${cfg.ldap.userSearch.ou}",
428             ldap.SCOPE_SUBTREE, "${cfg.ldap.userSearch.query}")
429         AUTH_LDAP_GROUP_TYPE = ${cfg.ldap.groupSearch.type}()
430         AUTH_LDAP_GROUP_SEARCH = LDAPSearch("${cfg.ldap.groupSearch.ou}",
431             ldap.SCOPE_SUBTREE, "${cfg.ldap.groupSearch.query}")
432         AUTH_LDAP_USER_ATTR_MAP = {
433           ${lib.concatStrings (lib.flip lib.mapAttrsToList cfg.ldap.attrMap (key: value: ''
434             "${key}": "${value}",
435           ''))}
436         }
437         ${lib.optionalString (cfg.ldap.superUserGroup != null) ''
438           AUTH_LDAP_USER_FLAGS_BY_GROUP = {
439             "is_superuser": "${cfg.ldap.superUserGroup}"
440           }
441         ''}
442         AUTHENTICATION_BACKENDS = (
443             "django_auth_ldap.backend.LDAPBackend",
444             "django.contrib.auth.backends.ModelBackend"
445         )
446       ''}
447     '';
449     services.nginx = lib.mkIf (cfg.serve.enable && cfg.webHosts != []) {
450       enable = lib.mkDefault true;
451       virtualHosts = lib.genAttrs cfg.webHosts (webHost: {
452         locations = {
453           ${cfg.serve.virtualRoot}.extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
454           "${lib.removeSuffix "/" cfg.serve.virtualRoot}/static/".alias = webSettings.STATIC_ROOT + "/";
455         };
456       });
457       proxyTimeout = lib.mkDefault "120s";
458     };
460     environment.systemPackages = [ (pkgs.buildEnv {
461       name = "mailman-tools";
462       # We don't want to pollute the system PATH with a python
463       # interpreter etc. so let's pick only the stuff we actually
464       # want from {web,mailman}Env
465       pathsToLink = ["/bin"];
466       paths = [ mailmanEnv webEnv ];
467       # Only mailman-related stuff is installed, the rest is removed
468       # in `postBuild`.
469       ignoreCollisions = true;
470       postBuild = ''
471         find $out/bin/ -mindepth 1 -not -name "mailman*" -delete
472       '' + lib.optionalString config.security.sudo.enable ''
473         mv $out/bin/mailman $out/bin/.mailman-wrapped
474         echo '#!${pkgs.runtimeShell}
475         sudo=exec
476         if [[ "$USER" != mailman ]]; then
477           sudo="exec /run/wrappers/bin/sudo -u mailman"
478         fi
479         $sudo ${placeholder "out"}/bin/.mailman-wrapped "$@"
480         ' > $out/bin/mailman
481         chmod +x $out/bin/mailman
482       '';
483     }) ];
485     services.postfix = lib.mkIf cfg.enablePostfix {
486       recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
487       config = {
488         owner_request_special = "no";   # Mailman handles -owner addresses on its own
489       };
490     };
492     systemd.sockets.mailman-uwsgi = lib.mkIf cfg.serve.enable {
493       wantedBy = ["sockets.target"];
494       before = ["nginx.service"];
495       socketConfig.ListenStream = "/run/mailman-web.socket";
496     };
497     systemd.services = {
498       mailman = {
499         description = "GNU Mailman Master Process";
500         before = lib.optional cfg.enablePostfix "postfix.service";
501         after = [ "network.target" ]
502           ++ lib.optional cfg.enablePostfix "postfix-setup.service"
503           ++ lib.optional withPostgresql "postgresql.service";
504         restartTriggers = [ mailmanCfgFile ];
505         requires = lib.optional withPostgresql "postgresql.service";
506         wantedBy = [ "multi-user.target" ];
507         serviceConfig = {
508           ExecStart = "${mailmanEnv}/bin/mailman start";
509           ExecStop = "${mailmanEnv}/bin/mailman stop";
510           User = "mailman";
511           Group = "mailman";
512           Type = "forking";
513           RuntimeDirectory = "mailman";
514           LogsDirectory = "mailman";
515           PIDFile = "/run/mailman/master.pid";
516           Restart = "on-failure";
517           TimeoutStartSec = 180;
518           TimeoutStopSec = 180;
519         };
520       };
522       mailman-settings = {
523         description = "Generate settings files (including secrets) for Mailman";
524         before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
525         requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
526         path = with pkgs; [ jq ];
527         after = lib.optional withPostgresql "postgresql.service";
528         requires = lib.optional withPostgresql "postgresql.service";
529         serviceConfig.RemainAfterExit = true;
530         serviceConfig.Type = "oneshot";
531         script = ''
532           install -m0750 -o mailman -g mailman ${mailmanCfgFile} /etc/mailman.cfg
533           ${if cfg.restApiPassFile == null then ''
534             sed -i "s/#NIXOS_MAILMAN_REST_API_PASS_SECRET#/$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)/g" \
535               /etc/mailman.cfg
536           '' else ''
537             ${pkgs.replace-secret}/bin/replace-secret \
538               '#NIXOS_MAILMAN_REST_API_PASS_SECRET#' \
539               ${cfg.restApiPassFile} \
540               /etc/mailman.cfg
541           ''}
543           mailmanDir=/var/lib/mailman
544           mailmanWebDir=/var/lib/mailman-web
546           mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
547           mailmanWebCfg=$mailmanWebDir/settings_local.json
549           install -m 0775 -o mailman -g mailman -d /var/lib/mailman-web-static
550           install -m 0770 -o mailman -g mailman -d $mailmanDir
551           install -m 0770 -o ${cfg.webUser} -g mailman -d $mailmanWebDir
553           if [ ! -e $mailmanWebCfg ]; then
554               hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
555               secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
557               install -m 0440 -o root -g mailman \
558                 <(jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
559                   --arg archiver_key "$hyperkittyApiKey" \
560                   --arg secret_key "$secretKey") \
561                 "$mailmanWebCfg"
562           fi
564           hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY "$mailmanWebCfg")"
565           mailmanCfgTmp=$(mktemp)
566           sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
567           chown mailman:mailman "$mailmanCfgTmp"
568           mv "$mailmanCfgTmp" "$mailmanCfg"
569         '';
570       };
572       mailman-web-setup = {
573         description = "Prepare mailman-web files and database";
574         before = [ "mailman-uwsgi.service" ];
575         requiredBy = [ "mailman-uwsgi.service" ];
576         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
577         script = ''
578           [[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete
579           ${webEnv}/bin/mailman-web migrate
580           ${webEnv}/bin/mailman-web collectstatic
581           ${webEnv}/bin/mailman-web compress
582         '';
583         serviceConfig = {
584           User = cfg.webUser;
585           Group = "mailman";
586           Type = "oneshot";
587           WorkingDirectory = "/var/lib/mailman-web";
588         };
589       };
591       mailman-uwsgi = lib.mkIf cfg.serve.enable (let
592         uwsgiConfig = lib.recursiveUpdate {
593           uwsgi = {
594             type = "normal";
595             plugins = ["python3"];
596             home = webEnv;
597             http = "127.0.0.1:18507";
598           }
599           // (if cfg.serve.virtualRoot == "/"
600             then { module = "mailman_web.wsgi:application"; }
601             else {
602               mount = "${cfg.serve.virtualRoot}=mailman_web.wsgi:application";
603               manage-script-name = true;
604           });
605         } cfg.serve.uwsgiSettings;
606         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
607       in {
608         wantedBy = ["multi-user.target"];
609         after = lib.optional withPostgresql "postgresql.service";
610         requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"]
611           ++ lib.optional withPostgresql "postgresql.service";
612         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
613         serviceConfig = {
614           # Since the mailman-web settings.py obstinately creates a logs
615           # dir in the cwd, change to the (writable) runtime directory before
616           # starting uwsgi.
617           ExecStart = "${pkgs.coreutils}/bin/env -C $RUNTIME_DIRECTORY ${pkgs.uwsgi.override { plugins = ["python3"]; python3 = webEnv.python; }}/bin/uwsgi --json ${uwsgiConfigFile}";
618           User = cfg.webUser;
619           Group = "mailman";
620           RuntimeDirectory = "mailman-uwsgi";
621           Restart = "on-failure";
622         };
623       });
625       mailman-daily = {
626         description = "Trigger daily Mailman events";
627         startAt = "daily";
628         restartTriggers = [ mailmanCfgFile ];
629         serviceConfig = {
630           ExecStart = "${mailmanEnv}/bin/mailman digests --send";
631           User = "mailman";
632           Group = "mailman";
633         };
634       };
636       hyperkitty = lib.mkIf cfg.hyperkitty.enable {
637         description = "GNU Hyperkitty QCluster Process";
638         after = [ "network.target" ];
639         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
640         wantedBy = [ "mailman.service" "multi-user.target" ];
641         serviceConfig = {
642           ExecStart = "${webEnv}/bin/mailman-web qcluster";
643           User = cfg.webUser;
644           Group = "mailman";
645           WorkingDirectory = "/var/lib/mailman-web";
646           Restart = "on-failure";
647         };
648       };
649     } // lib.flip lib.mapAttrs' {
650       "minutely" = "minutely";
651       "quarter_hourly" = "*:00/15";
652       "hourly" = "hourly";
653       "daily" = "daily";
654       "weekly" = "weekly";
655       "yearly" = "yearly";
656     } (name: startAt:
657       lib.nameValuePair "hyperkitty-${name}" (lib.mkIf cfg.hyperkitty.enable {
658         description = "Trigger ${name} Hyperkitty events";
659         inherit startAt;
660         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
661         serviceConfig = {
662           ExecStart = "${webEnv}/bin/mailman-web runjobs ${name}";
663           User = cfg.webUser;
664           Group = "mailman";
665           WorkingDirectory = "/var/lib/mailman-web";
666         };
667       }));
668   };
670   meta = {
671     maintainers = with lib.maintainers; [ qyliss ];
672     doc = ./mailman.md;
673   };