at 23.11-pre 24 kB view raw
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}