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