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