at 23.05-pre 23 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 {} 48 (recursiveUpdate cfg.settings 49 ((optionalAttrs (cfg.restApiPassFile != null) { 50 webservice.admin_pass = "#NIXOS_MAILMAN_REST_API_PASS_SECRET#"; 51 }))); 52 53 mailmanCfgFile = pkgs.writeText "mailman-raw.cfg" mailmanCfg; 54 55 mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" '' 56 [general] 57 # This is your HyperKitty installation, preferably on the localhost. This 58 # address will be used by Mailman to forward incoming emails to HyperKitty 59 # for archiving. It does not need to be publicly available, in fact it's 60 # better if it is not. 61 base_url: ${cfg.hyperkitty.baseUrl} 62 63 # Shared API key, must be the identical to the value in HyperKitty's 64 # settings. 65 api_key: @API_KEY@ 66 ''; 67 68in { 69 70 ###### interface 71 72 imports = [ 73 (mkRenamedOptionModule [ "services" "mailman" "hyperkittyBaseUrl" ] 74 [ "services" "mailman" "hyperkitty" "baseUrl" ]) 75 76 (mkRemovedOptionModule [ "services" "mailman" "hyperkittyApiKey" ] '' 77 The Hyperkitty API key is now generated on first run, and not 78 stored in the world-readable Nix store. To continue using 79 Hyperkitty, you must set services.mailman.hyperkitty.enable = true. 80 '') 81 (mkRemovedOptionModule [ "services" "mailman" "package" ] '' 82 Didn't have an effect for several years. 83 '') 84 ]; 85 86 options = { 87 88 services.mailman = { 89 90 enable = mkOption { 91 type = types.bool; 92 default = false; 93 description = lib.mdDoc "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix)."; 94 }; 95 96 ldap = { 97 enable = mkEnableOption (lib.mdDoc "LDAP auth"); 98 serverUri = mkOption { 99 type = types.str; 100 example = "ldaps://ldap.host"; 101 description = lib.mdDoc '' 102 LDAP host to connect against. 103 ''; 104 }; 105 bindDn = mkOption { 106 type = types.str; 107 example = "cn=root,dc=nixos,dc=org"; 108 description = lib.mdDoc '' 109 Service account to bind against. 110 ''; 111 }; 112 bindPasswordFile = mkOption { 113 type = types.str; 114 example = "/run/secrets/ldap-bind"; 115 description = lib.mdDoc '' 116 Path to the file containing the bind password of the servie account 117 defined by [](#opt-services.mailman.ldap.bindDn). 118 ''; 119 }; 120 superUserGroup = mkOption { 121 type = types.nullOr types.str; 122 default = null; 123 example = "cn=admin,ou=groups,dc=nixos,dc=org"; 124 description = lib.mdDoc '' 125 Group where a user must be a member of to gain superuser rights. 126 ''; 127 }; 128 userSearch = { 129 query = mkOption { 130 type = types.str; 131 example = "(&(objectClass=inetOrgPerson)(|(uid=%(user)s)(mail=%(user)s)))"; 132 description = lib.mdDoc '' 133 Query to find a user in the LDAP database. 134 ''; 135 }; 136 ou = mkOption { 137 type = types.str; 138 example = "ou=users,dc=nixos,dc=org"; 139 description = lib.mdDoc '' 140 Organizational unit to look up a user. 141 ''; 142 }; 143 }; 144 groupSearch = { 145 type = mkOption { 146 type = types.enum [ 147 "posixGroup" "groupOfNames" "memberDNGroup" "nestedMemberDNGroup" "nestedGroupOfNames" 148 "groupOfUniqueNames" "nestedGroupOfUniqueNames" "activeDirectoryGroup" "nestedActiveDirectoryGroup" 149 "organizationalRoleGroup" "nestedOrganizationalRoleGroup" 150 ]; 151 default = "posixGroup"; 152 apply = v: "${toUpper (substring 0 1 v)}${substring 1 (stringLength v) v}Type"; 153 description = lib.mdDoc '' 154 Type of group to perform a group search against. 155 ''; 156 }; 157 query = mkOption { 158 type = types.str; 159 example = "(objectClass=groupOfNames)"; 160 description = lib.mdDoc '' 161 Query to find a group associated to a user in the LDAP database. 162 ''; 163 }; 164 ou = mkOption { 165 type = types.str; 166 example = "ou=groups,dc=nixos,dc=org"; 167 description = lib.mdDoc '' 168 Organizational unit to look up a group. 169 ''; 170 }; 171 }; 172 attrMap = { 173 username = mkOption { 174 default = "uid"; 175 type = types.str; 176 description = lib.mdDoc '' 177 LDAP-attribute that corresponds to the `username`-attribute in mailman. 178 ''; 179 }; 180 firstName = mkOption { 181 default = "givenName"; 182 type = types.str; 183 description = lib.mdDoc '' 184 LDAP-attribute that corresponds to the `firstName`-attribute in mailman. 185 ''; 186 }; 187 lastName = mkOption { 188 default = "sn"; 189 type = types.str; 190 description = lib.mdDoc '' 191 LDAP-attribute that corresponds to the `lastName`-attribute in mailman. 192 ''; 193 }; 194 email = mkOption { 195 default = "mail"; 196 type = types.str; 197 description = lib.mdDoc '' 198 LDAP-attribute that corresponds to the `email`-attribute in mailman. 199 ''; 200 }; 201 }; 202 }; 203 204 enablePostfix = mkOption { 205 type = types.bool; 206 default = true; 207 example = false; 208 description = lib.mdDoc '' 209 Enable Postfix integration. Requires an active Postfix installation. 210 211 If you want to use another MTA, set this option to false and configure 212 settings in services.mailman.settings.mta. 213 214 Refer to the Mailman manual for more info. 215 ''; 216 }; 217 218 siteOwner = mkOption { 219 type = types.str; 220 example = "postmaster@example.org"; 221 description = lib.mdDoc '' 222 Certain messages that must be delivered to a human, but which can't 223 be delivered to a list owner (e.g. a bounce from a list owner), will 224 be sent to this address. It should point to a human. 225 ''; 226 }; 227 228 webHosts = mkOption { 229 type = types.listOf types.str; 230 default = []; 231 description = lib.mdDoc '' 232 The list of hostnames and/or IP addresses from which the Mailman Web 233 UI will accept requests. By default, "localhost" and "127.0.0.1" are 234 enabled. All additional names under which your web server accepts 235 requests for the UI must be listed here or incoming requests will be 236 rejected. 237 ''; 238 }; 239 240 webUser = mkOption { 241 type = types.str; 242 default = "mailman-web"; 243 description = lib.mdDoc '' 244 User to run mailman-web as 245 ''; 246 }; 247 248 webSettings = mkOption { 249 type = types.attrs; 250 default = {}; 251 description = lib.mdDoc '' 252 Overrides for the default mailman-web Django settings. 253 ''; 254 }; 255 256 restApiPassFile = mkOption { 257 default = null; 258 type = types.nullOr types.str; 259 description = lib.mdDoc '' 260 Path to the file containing the value for `MAILMAN_REST_API_PASS`. 261 ''; 262 }; 263 264 serve = { 265 enable = mkEnableOption (lib.mdDoc "Automatic nginx and uwsgi setup for mailman-web"); 266 267 virtualRoot = mkOption { 268 default = "/"; 269 example = lib.literalExpression "/lists"; 270 type = types.str; 271 description = lib.mdDoc '' 272 Path to mount the mailman-web django application on. 273 ''; 274 }; 275 }; 276 277 extraPythonPackages = mkOption { 278 description = lib.mdDoc "Packages to add to the python environment used by mailman and mailman-web"; 279 type = types.listOf types.package; 280 default = []; 281 }; 282 283 settings = mkOption { 284 description = lib.mdDoc "Settings for mailman.cfg"; 285 type = types.attrsOf (types.attrsOf types.str); 286 default = {}; 287 }; 288 289 hyperkitty = { 290 enable = mkEnableOption (lib.mdDoc "the Hyperkitty archiver for Mailman"); 291 292 baseUrl = mkOption { 293 type = types.str; 294 default = "http://localhost:18507/archives/"; 295 description = lib.mdDoc '' 296 Where can Mailman connect to Hyperkitty's internal API, preferably on 297 localhost? 298 ''; 299 }; 300 }; 301 302 }; 303 }; 304 305 ###### implementation 306 307 config = mkIf cfg.enable { 308 309 services.mailman.settings = { 310 mailman.site_owner = lib.mkDefault cfg.siteOwner; 311 mailman.layout = "fhs"; 312 313 "paths.fhs" = { 314 bin_dir = "${pkgs.mailmanPackages.mailman}/bin"; 315 var_dir = "/var/lib/mailman"; 316 queue_dir = "$var_dir/queue"; 317 template_dir = "$var_dir/templates"; 318 log_dir = "/var/log/mailman"; 319 lock_dir = "$var_dir/lock"; 320 etc_dir = "/etc"; 321 pid_file = "/run/mailman/master.pid"; 322 }; 323 324 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 326 "archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable { 327 class = "mailman_hyperkitty.Archiver"; 328 enable = "yes"; 329 configuration = "/var/lib/mailman/mailman-hyperkitty.cfg"; 330 }; 331 } // (let 332 loggerNames = ["root" "archiver" "bounce" "config" "database" "debug" "error" "fromusenet" "http" "locks" "mischief" "plugins" "runner" "smtp"]; 333 loggerSectionNames = map (n: "logging.${n}") loggerNames; 334 in lib.genAttrs loggerSectionNames(name: { handler = "stderr"; }) 335 ); 336 337 assertions = let 338 inherit (config.services) postfix; 339 340 requirePostfixHash = optionPath: dataFile: 341 with lib; 342 let 343 expected = "hash:/var/lib/mailman/data/${dataFile}"; 344 value = attrByPath optionPath [] postfix; 345 in 346 { assertion = postfix.enable -> isList value && elem expected value; 347 message = '' 348 services.postfix.${concatStringsSep "." optionPath} must contain 349 "${expected}". 350 See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>. 351 ''; 352 }; 353 in [ 354 { assertion = cfg.webHosts != []; 355 message = '' 356 services.mailman.serve.enable requires there to be at least one entry 357 in services.mailman.webHosts. 358 ''; 359 } 360 ] ++ (lib.optionals cfg.enablePostfix [ 361 { assertion = postfix.enable; 362 message = '' 363 Mailman's default NixOS configuration requires Postfix to be enabled. 364 365 If you want to use another MTA, set services.mailman.enablePostfix 366 to false and configure settings in services.mailman.settings.mta. 367 368 Refer to <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html> 369 for more info. 370 ''; 371 } 372 (requirePostfixHash [ "relayDomains" ] "postfix_domains") 373 (requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp") 374 (requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp") 375 ]); 376 377 users.users.mailman = { 378 description = "GNU Mailman"; 379 isSystemUser = true; 380 group = "mailman"; 381 }; 382 users.users.mailman-web = lib.mkIf (cfg.webUser == "mailman-web") { 383 description = "GNU Mailman web interface"; 384 isSystemUser = true; 385 group = "mailman"; 386 }; 387 users.groups.mailman = {}; 388 389 environment.etc."mailman3/settings.py".text = '' 390 import os 391 392 # Required by mailman_web.settings, but will be overridden when 393 # settings_local.json is loaded. 394 os.environ["SECRET_KEY"] = "" 395 396 from mailman_web.settings.base import * 397 from mailman_web.settings.mailman import * 398 399 import json 400 401 with open('${webSettingsJSON}') as f: 402 globals().update(json.load(f)) 403 404 with open('/var/lib/mailman-web/settings_local.json') as f: 405 globals().update(json.load(f)) 406 407 ${optionalString (cfg.restApiPassFile != null) '' 408 with open('${cfg.restApiPassFile}') as f: 409 MAILMAN_REST_API_PASS = f.read().rstrip('\n') 410 ''} 411 412 ${optionalString (cfg.ldap.enable) '' 413 import ldap 414 from django_auth_ldap.config import LDAPSearch, ${cfg.ldap.groupSearch.type} 415 AUTH_LDAP_SERVER_URI = "${cfg.ldap.serverUri}" 416 AUTH_LDAP_BIND_DN = "${cfg.ldap.bindDn}" 417 with open("${cfg.ldap.bindPasswordFile}") as f: 418 AUTH_LDAP_BIND_PASSWORD = f.read().rstrip('\n') 419 AUTH_LDAP_USER_SEARCH = LDAPSearch("${cfg.ldap.userSearch.ou}", 420 ldap.SCOPE_SUBTREE, "${cfg.ldap.userSearch.query}") 421 AUTH_LDAP_GROUP_TYPE = ${cfg.ldap.groupSearch.type}() 422 AUTH_LDAP_GROUP_SEARCH = LDAPSearch("${cfg.ldap.groupSearch.ou}", 423 ldap.SCOPE_SUBTREE, "${cfg.ldap.groupSearch.query}") 424 AUTH_LDAP_USER_ATTR_MAP = { 425 ${concatStrings (flip mapAttrsToList cfg.ldap.attrMap (key: value: '' 426 "${key}": "${value}", 427 ''))} 428 } 429 ${optionalString (cfg.ldap.superUserGroup != null) '' 430 AUTH_LDAP_USER_FLAGS_BY_GROUP = { 431 "is_superuser": "${cfg.ldap.superUserGroup}" 432 } 433 ''} 434 AUTHENTICATION_BACKENDS = ( 435 "django_auth_ldap.backend.LDAPBackend", 436 "django.contrib.auth.backends.ModelBackend" 437 ) 438 ''} 439 ''; 440 441 services.nginx = mkIf (cfg.serve.enable && cfg.webHosts != []) { 442 enable = mkDefault true; 443 virtualHosts = lib.genAttrs cfg.webHosts (webHost: { 444 locations = { 445 ${cfg.serve.virtualRoot}.extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;"; 446 "${cfg.serve.virtualRoot}/static/".alias = webSettings.STATIC_ROOT + "/"; 447 }; 448 }); 449 }; 450 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 }) ]; 465 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 }; 472 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 = 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 }; 498 }; 499 500 mailman-settings = { 501 description = "Generate settings files (including secrets) for Mailman"; 502 before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ]; 503 requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ]; 504 path = with pkgs; [ jq ]; 505 after = optional withPostgresql "postgresql.service"; 506 requires = optional withPostgresql "postgresql.service"; 507 serviceConfig.Type = "oneshot"; 508 script = '' 509 install -m0750 -o mailman -g mailman ${mailmanCfgFile} /etc/mailman.cfg 510 ${optionalString (cfg.restApiPassFile != null) '' 511 ${pkgs.replace-secret}/bin/replace-secret \ 512 '#NIXOS_MAILMAN_REST_API_PASS_SECRET#' \ 513 ${cfg.restApiPassFile} \ 514 /etc/mailman.cfg 515 ''} 516 517 mailmanDir=/var/lib/mailman 518 mailmanWebDir=/var/lib/mailman-web 519 520 mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg 521 mailmanWebCfg=$mailmanWebDir/settings_local.json 522 523 install -m 0775 -o mailman -g mailman -d /var/lib/mailman-web-static 524 install -m 0770 -o mailman -g mailman -d $mailmanDir 525 install -m 0770 -o ${cfg.webUser} -g mailman -d $mailmanWebDir 526 527 if [ ! -e $mailmanWebCfg ]; then 528 hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64) 529 secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64) 530 531 mailmanWebCfgTmp=$(mktemp) 532 jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \ 533 --arg archiver_key "$hyperkittyApiKey" \ 534 --arg secret_key "$secretKey" \ 535 >"$mailmanWebCfgTmp" 536 chown root:mailman "$mailmanWebCfgTmp" 537 chmod 440 "$mailmanWebCfgTmp" 538 mv -n "$mailmanWebCfgTmp" "$mailmanWebCfg" 539 fi 540 541 hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY "$mailmanWebCfg")" 542 mailmanCfgTmp=$(mktemp) 543 sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp" 544 chown mailman:mailman "$mailmanCfgTmp" 545 mv "$mailmanCfgTmp" "$mailmanCfg" 546 ''; 547 }; 548 549 mailman-web-setup = { 550 description = "Prepare mailman-web files and database"; 551 before = [ "mailman-uwsgi.service" ]; 552 requiredBy = [ "mailman-uwsgi.service" ]; 553 restartTriggers = [ config.environment.etc."mailman3/settings.py".source ]; 554 script = '' 555 [[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete 556 ${webEnv}/bin/mailman-web migrate 557 ${webEnv}/bin/mailman-web collectstatic 558 ${webEnv}/bin/mailman-web compress 559 ''; 560 serviceConfig = { 561 User = cfg.webUser; 562 Group = "mailman"; 563 Type = "oneshot"; 564 WorkingDirectory = "/var/lib/mailman-web"; 565 }; 566 }; 567 568 mailman-uwsgi = mkIf cfg.serve.enable (let 569 uwsgiConfig.uwsgi = { 570 type = "normal"; 571 plugins = ["python3"]; 572 home = webEnv; 573 manage-script-name = true; 574 mount = "${cfg.serve.virtualRoot}=mailman_web.wsgi:application"; 575 http = "127.0.0.1:18507"; 576 }; 577 uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig); 578 in { 579 wantedBy = ["multi-user.target"]; 580 after = optional withPostgresql "postgresql.service"; 581 requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"] 582 ++ optional withPostgresql "postgresql.service"; 583 restartTriggers = [ config.environment.etc."mailman3/settings.py".source ]; 584 serviceConfig = { 585 # Since the mailman-web settings.py obstinately creates a logs 586 # dir in the cwd, change to the (writable) runtime directory before 587 # starting uwsgi. 588 ExecStart = "${pkgs.coreutils}/bin/env -C $RUNTIME_DIRECTORY ${pkgs.uwsgi.override { plugins = ["python3"]; }}/bin/uwsgi --json ${uwsgiConfigFile}"; 589 User = cfg.webUser; 590 Group = "mailman"; 591 RuntimeDirectory = "mailman-uwsgi"; 592 }; 593 }); 594 595 mailman-daily = { 596 description = "Trigger daily Mailman events"; 597 startAt = "daily"; 598 restartTriggers = [ mailmanCfgFile ]; 599 serviceConfig = { 600 ExecStart = "${mailmanEnv}/bin/mailman digests --send"; 601 User = "mailman"; 602 Group = "mailman"; 603 }; 604 }; 605 606 hyperkitty = lib.mkIf cfg.hyperkitty.enable { 607 description = "GNU Hyperkitty QCluster Process"; 608 after = [ "network.target" ]; 609 restartTriggers = [ config.environment.etc."mailman3/settings.py".source ]; 610 wantedBy = [ "mailman.service" "multi-user.target" ]; 611 serviceConfig = { 612 ExecStart = "${webEnv}/bin/mailman-web qcluster"; 613 User = cfg.webUser; 614 Group = "mailman"; 615 WorkingDirectory = "/var/lib/mailman-web"; 616 }; 617 }; 618 } // flip lib.mapAttrs' { 619 "minutely" = "minutely"; 620 "quarter_hourly" = "*:00/15"; 621 "hourly" = "hourly"; 622 "daily" = "daily"; 623 "weekly" = "weekly"; 624 "yearly" = "yearly"; 625 } (name: startAt: 626 lib.nameValuePair "hyperkitty-${name}" (lib.mkIf cfg.hyperkitty.enable { 627 description = "Trigger ${name} Hyperkitty events"; 628 inherit startAt; 629 restartTriggers = [ config.environment.etc."mailman3/settings.py".source ]; 630 serviceConfig = { 631 ExecStart = "${webEnv}/bin/mailman-web runjobs ${name}"; 632 User = cfg.webUser; 633 Group = "mailman"; 634 WorkingDirectory = "/var/lib/mailman-web"; 635 }; 636 })); 637 }; 638 639 meta = { 640 maintainers = with lib.maintainers; [ lheckemann qyliss ma27 ]; 641 doc = ./mailman.xml; 642 }; 643 644}