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