vuls: init at 0.27.0 (#348530)
[NixPkgs.git] / nixos / modules / services / mail / mailman.nix
blob95b77da250b29174cad42e012bdb15a66f7c383a
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         virtualRoot = lib.mkOption {
267           default = "/";
268           example = lib.literalExpression "/lists";
269           type = lib.types.str;
270           description = ''
271             Path to mount the mailman-web django application on.
272           '';
273         };
274       };
276       extraPythonPackages = lib.mkOption {
277         description = "Packages to add to the python environment used by mailman and mailman-web";
278         type = lib.types.listOf lib.types.package;
279         default = [];
280       };
282       settings = lib.mkOption {
283         description = "Settings for mailman.cfg";
284         type = lib.types.attrsOf (lib.types.attrsOf lib.types.str);
285         default = {};
286       };
288       hyperkitty = {
289         enable = lib.mkEnableOption "the Hyperkitty archiver for Mailman";
291         baseUrl = lib.mkOption {
292           type = lib.types.str;
293           default = "http://localhost:18507/archives/";
294           description = ''
295             Where can Mailman connect to Hyperkitty's internal API, preferably on
296             localhost?
297           '';
298         };
299       };
301     };
302   };
304   ###### implementation
306   config = lib.mkIf cfg.enable {
308     services.mailman.settings = {
309       mailman.site_owner = lib.mkDefault cfg.siteOwner;
310       mailman.layout = "fhs";
312       "paths.fhs" = {
313         bin_dir = "${pkgs.mailmanPackages.mailman}/bin";
314         var_dir = "/var/lib/mailman";
315         queue_dir = "$var_dir/queue";
316         template_dir = "$var_dir/templates";
317         log_dir = "/var/log/mailman";
318         lock_dir = "/run/mailman/lock";
319         etc_dir = "/etc";
320         pid_file = "/run/mailman/master.pid";
321       };
323       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.");
325       "archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable {
326         class = "mailman_hyperkitty.Archiver";
327         enable = "yes";
328         configuration = "/var/lib/mailman/mailman-hyperkitty.cfg";
329       };
330     } // (let
331       loggerNames = ["root" "archiver" "bounce" "config" "database" "debug" "error" "fromusenet" "http" "locks" "mischief" "plugins" "runner" "smtp"];
332       loggerSectionNames = map (n: "logging.${n}") loggerNames;
333       in lib.genAttrs loggerSectionNames(name: { handler = "stderr"; })
334     );
336     assertions = let
337       inherit (config.services) postfix;
339       requirePostfixHash = optionPath: dataFile:
340         let
341           expected = "hash:/var/lib/mailman/data/${dataFile}";
342           value = lib.attrByPath optionPath [] postfix;
343         in
344           { assertion = postfix.enable -> lib.isList value && lib.elem expected value;
345             message = ''
346               services.postfix.${lib.concatStringsSep "." optionPath} must contain
347               "${expected}".
348               See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
349             '';
350           };
351     in [
352       { assertion = cfg.webHosts != [];
353         message = ''
354           services.mailman.serve.enable requires there to be at least one entry
355           in services.mailman.webHosts.
356         '';
357       }
358     ] ++ (lib.optionals cfg.enablePostfix [
359       { assertion = postfix.enable;
360         message = ''
361           Mailman's default NixOS configuration requires Postfix to be enabled.
363           If you want to use another MTA, set services.mailman.enablePostfix
364           to false and configure settings in services.mailman.settings.mta.
366           Refer to <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>
367           for more info.
368         '';
369       }
370       (requirePostfixHash [ "config" "relay_domains" ] "postfix_domains")
371       (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
372       (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
373     ]);
375     users.users.mailman = {
376       description = "GNU Mailman";
377       isSystemUser = true;
378       group = "mailman";
379     };
380     users.users.mailman-web = lib.mkIf (cfg.webUser == "mailman-web") {
381       description = "GNU Mailman web interface";
382       isSystemUser = true;
383       group = "mailman";
384     };
385     users.groups.mailman = {};
387     environment.etc."mailman3/settings.py".text = ''
388       import os
389       from configparser import ConfigParser
391       # Required by mailman_web.settings, but will be overridden when
392       # settings_local.json is loaded.
393       os.environ["SECRET_KEY"] = ""
395       from mailman_web.settings.base import *
396       from mailman_web.settings.mailman import *
398       import json
400       with open('${webSettingsJSON}') as f:
401           globals().update(json.load(f))
403       with open('/var/lib/mailman-web/settings_local.json') as f:
404           globals().update(json.load(f))
406       with open('/etc/mailman.cfg') as f:
407           config = ConfigParser()
408           config.read_file(f)
409           MAILMAN_REST_API_PASS = config['webservice']['admin_pass']
411       ${lib.optionalString (cfg.ldap.enable) ''
412         import ldap
413         from django_auth_ldap.config import LDAPSearch, ${cfg.ldap.groupSearch.type}
414         AUTH_LDAP_SERVER_URI = "${cfg.ldap.serverUri}"
415         AUTH_LDAP_BIND_DN = "${cfg.ldap.bindDn}"
416         with open("${cfg.ldap.bindPasswordFile}") as f:
417             AUTH_LDAP_BIND_PASSWORD = f.read().rstrip('\n')
418         AUTH_LDAP_USER_SEARCH = LDAPSearch("${cfg.ldap.userSearch.ou}",
419             ldap.SCOPE_SUBTREE, "${cfg.ldap.userSearch.query}")
420         AUTH_LDAP_GROUP_TYPE = ${cfg.ldap.groupSearch.type}()
421         AUTH_LDAP_GROUP_SEARCH = LDAPSearch("${cfg.ldap.groupSearch.ou}",
422             ldap.SCOPE_SUBTREE, "${cfg.ldap.groupSearch.query}")
423         AUTH_LDAP_USER_ATTR_MAP = {
424           ${lib.concatStrings (lib.flip lib.mapAttrsToList cfg.ldap.attrMap (key: value: ''
425             "${key}": "${value}",
426           ''))}
427         }
428         ${lib.optionalString (cfg.ldap.superUserGroup != null) ''
429           AUTH_LDAP_USER_FLAGS_BY_GROUP = {
430             "is_superuser": "${cfg.ldap.superUserGroup}"
431           }
432         ''}
433         AUTHENTICATION_BACKENDS = (
434             "django_auth_ldap.backend.LDAPBackend",
435             "django.contrib.auth.backends.ModelBackend"
436         )
437       ''}
438     '';
440     services.nginx = lib.mkIf (cfg.serve.enable && cfg.webHosts != []) {
441       enable = lib.mkDefault true;
442       virtualHosts = lib.genAttrs cfg.webHosts (webHost: {
443         locations = {
444           ${cfg.serve.virtualRoot}.extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
445           "${lib.removeSuffix "/" cfg.serve.virtualRoot}/static/".alias = webSettings.STATIC_ROOT + "/";
446         };
447       });
448       proxyTimeout = lib.mkDefault "120s";
449     };
451     environment.systemPackages = [ (pkgs.buildEnv {
452       name = "mailman-tools";
453       # We don't want to pollute the system PATH with a python
454       # interpreter etc. so let's pick only the stuff we actually
455       # want from {web,mailman}Env
456       pathsToLink = ["/bin"];
457       paths = [ mailmanEnv webEnv ];
458       # Only mailman-related stuff is installed, the rest is removed
459       # in `postBuild`.
460       ignoreCollisions = true;
461       postBuild = ''
462         find $out/bin/ -mindepth 1 -not -name "mailman*" -delete
463       '';
464     }) ];
466     services.postfix = lib.mkIf cfg.enablePostfix {
467       recipientDelimiter = "+";         # bake recipient addresses in mail envelopes via VERP
468       config = {
469         owner_request_special = "no";   # Mailman handles -owner addresses on its own
470       };
471     };
473     systemd.sockets.mailman-uwsgi = lib.mkIf cfg.serve.enable {
474       wantedBy = ["sockets.target"];
475       before = ["nginx.service"];
476       socketConfig.ListenStream = "/run/mailman-web.socket";
477     };
478     systemd.services = {
479       mailman = {
480         description = "GNU Mailman Master Process";
481         before = lib.optional cfg.enablePostfix "postfix.service";
482         after = [ "network.target" ]
483           ++ lib.optional cfg.enablePostfix "postfix-setup.service"
484           ++ lib.optional withPostgresql "postgresql.service";
485         restartTriggers = [ mailmanCfgFile ];
486         requires = lib.optional withPostgresql "postgresql.service";
487         wantedBy = [ "multi-user.target" ];
488         serviceConfig = {
489           ExecStart = "${mailmanEnv}/bin/mailman start";
490           ExecStop = "${mailmanEnv}/bin/mailman stop";
491           User = "mailman";
492           Group = "mailman";
493           Type = "forking";
494           RuntimeDirectory = "mailman";
495           LogsDirectory = "mailman";
496           PIDFile = "/run/mailman/master.pid";
497           Restart = "on-failure";
498           TimeoutStartSec = 180;
499           TimeoutStopSec = 180;
500         };
501       };
503       mailman-settings = {
504         description = "Generate settings files (including secrets) for Mailman";
505         before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
506         requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
507         path = with pkgs; [ jq ];
508         after = lib.optional withPostgresql "postgresql.service";
509         requires = lib.optional withPostgresql "postgresql.service";
510         serviceConfig.RemainAfterExit = true;
511         serviceConfig.Type = "oneshot";
512         script = ''
513           install -m0750 -o mailman -g mailman ${mailmanCfgFile} /etc/mailman.cfg
514           ${if cfg.restApiPassFile == null then ''
515             sed -i "s/#NIXOS_MAILMAN_REST_API_PASS_SECRET#/$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)/g" \
516               /etc/mailman.cfg
517           '' else ''
518             ${pkgs.replace-secret}/bin/replace-secret \
519               '#NIXOS_MAILMAN_REST_API_PASS_SECRET#' \
520               ${cfg.restApiPassFile} \
521               /etc/mailman.cfg
522           ''}
524           mailmanDir=/var/lib/mailman
525           mailmanWebDir=/var/lib/mailman-web
527           mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
528           mailmanWebCfg=$mailmanWebDir/settings_local.json
530           install -m 0775 -o mailman -g mailman -d /var/lib/mailman-web-static
531           install -m 0770 -o mailman -g mailman -d $mailmanDir
532           install -m 0770 -o ${cfg.webUser} -g mailman -d $mailmanWebDir
534           if [ ! -e $mailmanWebCfg ]; then
535               hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
536               secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
538               install -m 0440 -o root -g mailman \
539                 <(jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
540                   --arg archiver_key "$hyperkittyApiKey" \
541                   --arg secret_key "$secretKey") \
542                 "$mailmanWebCfg"
543           fi
545           hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY "$mailmanWebCfg")"
546           mailmanCfgTmp=$(mktemp)
547           sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
548           chown mailman:mailman "$mailmanCfgTmp"
549           mv "$mailmanCfgTmp" "$mailmanCfg"
550         '';
551       };
553       mailman-web-setup = {
554         description = "Prepare mailman-web files and database";
555         before = [ "mailman-uwsgi.service" ];
556         requiredBy = [ "mailman-uwsgi.service" ];
557         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
558         script = ''
559           [[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete
560           ${webEnv}/bin/mailman-web migrate
561           ${webEnv}/bin/mailman-web collectstatic
562           ${webEnv}/bin/mailman-web compress
563         '';
564         serviceConfig = {
565           User = cfg.webUser;
566           Group = "mailman";
567           Type = "oneshot";
568           WorkingDirectory = "/var/lib/mailman-web";
569         };
570       };
572       mailman-uwsgi = lib.mkIf cfg.serve.enable (let
573         uwsgiConfig.uwsgi = {
574           type = "normal";
575           plugins = ["python3"];
576           home = webEnv;
577           http = "127.0.0.1:18507";
578         }
579         // (if cfg.serve.virtualRoot == "/"
580           then { module = "mailman_web.wsgi:application"; }
581           else {
582             mount = "${cfg.serve.virtualRoot}=mailman_web.wsgi:application";
583             manage-script-name = true;
584           });
585         uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
586       in {
587         wantedBy = ["multi-user.target"];
588         after = lib.optional withPostgresql "postgresql.service";
589         requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"]
590           ++ lib.optional withPostgresql "postgresql.service";
591         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
592         serviceConfig = {
593           # Since the mailman-web settings.py obstinately creates a logs
594           # dir in the cwd, change to the (writable) runtime directory before
595           # starting uwsgi.
596           ExecStart = "${pkgs.coreutils}/bin/env -C $RUNTIME_DIRECTORY ${pkgs.uwsgi.override { plugins = ["python3"]; python3 = webEnv.python; }}/bin/uwsgi --json ${uwsgiConfigFile}";
597           User = cfg.webUser;
598           Group = "mailman";
599           RuntimeDirectory = "mailman-uwsgi";
600           Restart = "on-failure";
601         };
602       });
604       mailman-daily = {
605         description = "Trigger daily Mailman events";
606         startAt = "daily";
607         restartTriggers = [ mailmanCfgFile ];
608         serviceConfig = {
609           ExecStart = "${mailmanEnv}/bin/mailman digests --send";
610           User = "mailman";
611           Group = "mailman";
612         };
613       };
615       hyperkitty = lib.mkIf cfg.hyperkitty.enable {
616         description = "GNU Hyperkitty QCluster Process";
617         after = [ "network.target" ];
618         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
619         wantedBy = [ "mailman.service" "multi-user.target" ];
620         serviceConfig = {
621           ExecStart = "${webEnv}/bin/mailman-web qcluster";
622           User = cfg.webUser;
623           Group = "mailman";
624           WorkingDirectory = "/var/lib/mailman-web";
625           Restart = "on-failure";
626         };
627       };
628     } // lib.flip lib.mapAttrs' {
629       "minutely" = "minutely";
630       "quarter_hourly" = "*:00/15";
631       "hourly" = "hourly";
632       "daily" = "daily";
633       "weekly" = "weekly";
634       "yearly" = "yearly";
635     } (name: startAt:
636       lib.nameValuePair "hyperkitty-${name}" (lib.mkIf cfg.hyperkitty.enable {
637         description = "Trigger ${name} Hyperkitty events";
638         inherit startAt;
639         restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
640         serviceConfig = {
641           ExecStart = "${webEnv}/bin/mailman-web runjobs ${name}";
642           User = cfg.webUser;
643           Group = "mailman";
644           WorkingDirectory = "/var/lib/mailman-web";
645         };
646       }));
647   };
649   meta = {
650     maintainers = with lib.maintainers; [ qyliss ];
651     doc = ./mailman.md;
652   };