at 25.11-pre 21 kB view raw
1{ 2 config, 3 lib, 4 pkgs, 5 ... 6}: 7let 8 cfg = config.services.sympa; 9 dataDir = "/var/lib/sympa"; 10 user = "sympa"; 11 group = "sympa"; 12 pkg = pkgs.sympa; 13 fqdns = lib.attrNames cfg.domains; 14 usingNginx = cfg.web.enable && cfg.web.server == "nginx"; 15 mysqlLocal = cfg.database.createLocally && cfg.database.type == "MySQL"; 16 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "PostgreSQL"; 17 18 sympaSubServices = [ 19 "sympa-archive.service" 20 "sympa-bounce.service" 21 "sympa-bulk.service" 22 "sympa-task.service" 23 ]; 24 25 # common for all services including wwsympa 26 commonServiceConfig = { 27 StateDirectory = "sympa"; 28 ProtectHome = true; 29 ProtectSystem = "full"; 30 ProtectControlGroups = true; 31 }; 32 33 # wwsympa has its own service config 34 sympaServiceConfig = 35 srv: 36 { 37 Type = "simple"; 38 Restart = "always"; 39 ExecStart = "${pkg}/bin/${srv}.pl --foreground"; 40 PIDFile = "/run/sympa/${srv}.pid"; 41 User = user; 42 Group = group; 43 44 # avoid duplicating log messageges in journal 45 StandardError = "null"; 46 } 47 // commonServiceConfig; 48 49 configVal = value: if lib.isBool value then if value then "on" else "off" else toString value; 50 configGenerator = 51 c: lib.concatStrings (lib.flip lib.mapAttrsToList c (key: val: "${key}\t${configVal val}\n")); 52 53 mainConfig = pkgs.writeText "sympa.conf" (configGenerator cfg.settings); 54 robotConfig = fqdn: domain: pkgs.writeText "${fqdn}-robot.conf" (configGenerator domain.settings); 55 56 transport = pkgs.writeText "transport.sympa" ( 57 lib.concatStringsSep "\n" ( 58 lib.flip map fqdns (domain: '' 59 ${domain} error:User unknown in recipient table 60 sympa@${domain} sympa:sympa@${domain} 61 listmaster@${domain} sympa:listmaster@${domain} 62 bounce@${domain} sympabounce:sympa@${domain} 63 abuse-feedback-report@${domain} sympabounce:sympa@${domain} 64 '') 65 ) 66 ); 67 68 virtual = pkgs.writeText "virtual.sympa" ( 69 lib.concatStringsSep "\n" ( 70 lib.flip map fqdns (domain: '' 71 sympa-request@${domain} postmaster@localhost 72 sympa-owner@${domain} postmaster@localhost 73 '') 74 ) 75 ); 76 77 listAliases = pkgs.writeText "list_aliases.tt2" '' 78 #--- [% list.name %]@[% list.domain %]: list transport map created at [% date %] 79 [% list.name %]@[% list.domain %] sympa:[% list.name %]@[% list.domain %] 80 [% list.name %]-request@[% list.domain %] sympa:[% list.name %]-request@[% list.domain %] 81 [% list.name %]-editor@[% list.domain %] sympa:[% list.name %]-editor@[% list.domain %] 82 #[% list.name %]-subscribe@[% list.domain %] sympa:[% list.name %]-subscribe@[%list.domain %] 83 [% list.name %]-unsubscribe@[% list.domain %] sympa:[% list.name %]-unsubscribe@[% list.domain %] 84 [% list.name %][% return_path_suffix %]@[% list.domain %] sympabounce:[% list.name %]@[% list.domain %] 85 ''; 86 87 enabledFiles = lib.filterAttrs (n: v: v.enable) cfg.settingsFile; 88in 89{ 90 91 ###### interface 92 options.services.sympa = with lib.types; { 93 94 enable = lib.mkEnableOption "Sympa mailing list manager"; 95 96 lang = lib.mkOption { 97 type = str; 98 default = "en_US"; 99 example = "cs"; 100 description = '' 101 Default Sympa language. 102 See <https://github.com/sympa-community/sympa/tree/sympa-6.2/po/sympa> 103 for available options. 104 ''; 105 }; 106 107 listMasters = lib.mkOption { 108 type = listOf str; 109 example = [ "postmaster@sympa.example.org" ]; 110 description = '' 111 The list of the email addresses of the listmasters 112 (users authorized to perform global server commands). 113 ''; 114 }; 115 116 mainDomain = lib.mkOption { 117 type = nullOr str; 118 default = null; 119 example = "lists.example.org"; 120 description = '' 121 Main domain to be used in {file}`sympa.conf`. 122 If `null`, one of the {option}`services.sympa.domains` is chosen for you. 123 ''; 124 }; 125 126 domains = lib.mkOption { 127 type = attrsOf ( 128 submodule ( 129 { name, config, ... }: 130 { 131 options = { 132 webHost = lib.mkOption { 133 type = nullOr str; 134 default = null; 135 example = "archive.example.org"; 136 description = '' 137 Domain part of the web interface URL (no web interface for this domain if `null`). 138 DNS record of type A (or AAAA or CNAME) has to exist with this value. 139 ''; 140 }; 141 webLocation = lib.mkOption { 142 type = str; 143 default = "/"; 144 example = "/sympa"; 145 description = "URL path part of the web interface."; 146 }; 147 settings = lib.mkOption { 148 type = attrsOf (oneOf [ 149 str 150 int 151 bool 152 ]); 153 default = { }; 154 example = { 155 default_max_list_members = 3; 156 }; 157 description = '' 158 The {file}`robot.conf` configuration file as key value set. 159 See <https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html> 160 for list of configuration parameters. 161 ''; 162 }; 163 }; 164 165 config.settings = lib.mkIf (cfg.web.enable && config.webHost != null) { 166 wwsympa_url = lib.mkDefault "https://${config.webHost}${lib.removeSuffix "/" config.webLocation}"; 167 }; 168 } 169 ) 170 ); 171 172 description = '' 173 Email domains handled by this instance. There have 174 to be MX records for keys of this attribute set. 175 ''; 176 example = lib.literalExpression '' 177 { 178 "lists.example.org" = { 179 webHost = "lists.example.org"; 180 webLocation = "/"; 181 }; 182 "sympa.example.com" = { 183 webHost = "example.com"; 184 webLocation = "/sympa"; 185 }; 186 } 187 ''; 188 }; 189 190 database = { 191 type = lib.mkOption { 192 type = enum [ 193 "SQLite" 194 "PostgreSQL" 195 "MySQL" 196 ]; 197 default = "SQLite"; 198 example = "MySQL"; 199 description = "Database engine to use."; 200 }; 201 202 host = lib.mkOption { 203 type = nullOr str; 204 default = null; 205 description = '' 206 Database host address. 207 208 For MySQL, use `localhost` to connect using Unix domain socket. 209 210 For PostgreSQL, use path to directory (e.g. {file}`/run/postgresql`) 211 to connect using Unix domain socket located in this directory. 212 213 Use `null` to fall back on Sympa default, or when using 214 {option}`services.sympa.database.createLocally`. 215 ''; 216 }; 217 218 port = lib.mkOption { 219 type = nullOr port; 220 default = null; 221 description = "Database port. Use `null` for default port."; 222 }; 223 224 name = lib.mkOption { 225 type = str; 226 default = if cfg.database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa"; 227 defaultText = lib.literalExpression ''if database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa"''; 228 description = '' 229 Database name. When using SQLite this must be an absolute 230 path to the database file. 231 ''; 232 }; 233 234 user = lib.mkOption { 235 type = nullOr str; 236 default = user; 237 description = "Database user. The system user name is used as a default."; 238 }; 239 240 passwordFile = lib.mkOption { 241 type = nullOr path; 242 default = null; 243 example = "/run/keys/sympa-dbpassword"; 244 description = '' 245 A file containing the password for {option}`services.sympa.database.name`. 246 ''; 247 }; 248 249 createLocally = lib.mkOption { 250 type = bool; 251 default = true; 252 description = "Whether to create a local database automatically."; 253 }; 254 }; 255 256 web = { 257 enable = lib.mkOption { 258 type = bool; 259 default = true; 260 description = "Whether to enable Sympa web interface."; 261 }; 262 263 server = lib.mkOption { 264 type = enum [ 265 "nginx" 266 "none" 267 ]; 268 default = "nginx"; 269 description = '' 270 The webserver used for the Sympa web interface. Set it to `none` if you want to configure it yourself. 271 Further nginx configuration can be done by adapting 272 {option}`services.nginx.virtualHosts.«name»`. 273 ''; 274 }; 275 276 https = lib.mkOption { 277 type = bool; 278 default = true; 279 description = '' 280 Whether to use HTTPS. When nginx integration is enabled, this option forces SSL and enables ACME. 281 Please note that Sympa web interface always uses https links even when this option is disabled. 282 ''; 283 }; 284 285 fcgiProcs = lib.mkOption { 286 type = ints.positive; 287 default = 2; 288 description = "Number of FastCGI processes to fork."; 289 }; 290 }; 291 292 mta = { 293 type = lib.mkOption { 294 type = enum [ 295 "postfix" 296 "none" 297 ]; 298 default = "postfix"; 299 description = '' 300 Mail transfer agent (MTA) integration. Use `none` if you want to configure it yourself. 301 302 The `postfix` integration sets up local Postfix instance that will pass incoming 303 messages from configured domains to Sympa. You still need to configure at least outgoing message 304 handling using e.g. {option}`services.postfix.relayHost`. 305 ''; 306 }; 307 }; 308 309 settings = lib.mkOption { 310 type = attrsOf (oneOf [ 311 str 312 int 313 bool 314 ]); 315 default = { }; 316 example = lib.literalExpression '' 317 { 318 default_home = "lists"; 319 viewlogs_page_size = 50; 320 } 321 ''; 322 description = '' 323 The {file}`sympa.conf` configuration file as key value set. 324 See <https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html> 325 for list of configuration parameters. 326 ''; 327 }; 328 329 settingsFile = lib.mkOption { 330 type = attrsOf ( 331 submodule ( 332 { name, config, ... }: 333 { 334 options = { 335 enable = lib.mkOption { 336 type = bool; 337 default = true; 338 description = "Whether this file should be generated. This option allows specific files to be disabled."; 339 }; 340 text = lib.mkOption { 341 default = null; 342 type = nullOr lines; 343 description = "Text of the file."; 344 }; 345 source = lib.mkOption { 346 type = path; 347 description = "Path of the source file."; 348 }; 349 }; 350 351 config.source = lib.mkIf (config.text != null) ( 352 lib.mkDefault (pkgs.writeText "sympa-${baseNameOf name}" config.text) 353 ); 354 } 355 ) 356 ); 357 default = { }; 358 example = lib.literalExpression '' 359 { 360 "list_data/lists.example.org/help" = { 361 text = "subject This list provides help to users"; 362 }; 363 } 364 ''; 365 description = "Set of files to be linked in {file}`${dataDir}`."; 366 }; 367 }; 368 369 ###### implementation 370 371 config = lib.mkIf cfg.enable { 372 373 services.sympa.settings = ( 374 lib.mapAttrs (_: v: lib.mkDefault v) { 375 domain = if cfg.mainDomain != null then cfg.mainDomain else lib.head fqdns; 376 listmaster = lib.concatStringsSep "," cfg.listMasters; 377 lang = cfg.lang; 378 379 home = "${dataDir}/list_data"; 380 arc_path = "${dataDir}/arc"; 381 bounce_path = "${dataDir}/bounce"; 382 383 sendmail = "${pkgs.system-sendmail}/bin/sendmail"; 384 385 db_type = cfg.database.type; 386 db_name = cfg.database.name; 387 db_user = cfg.database.name; 388 } 389 // (lib.optionalAttrs (cfg.database.host != null) { 390 db_host = cfg.database.host; 391 }) 392 // (lib.optionalAttrs mysqlLocal { 393 db_host = "localhost"; # use unix domain socket 394 }) 395 // (lib.optionalAttrs pgsqlLocal { 396 db_host = "/run/postgresql"; # use unix domain socket 397 }) 398 // (lib.optionalAttrs (cfg.database.port != null) { 399 db_port = cfg.database.port; 400 }) 401 // (lib.optionalAttrs (cfg.mta.type == "postfix") { 402 sendmail_aliases = "${dataDir}/sympa_transport"; 403 aliases_program = "${pkgs.postfix}/bin/postmap"; 404 aliases_db_type = "hash"; 405 }) 406 // (lib.optionalAttrs cfg.web.enable { 407 static_content_path = "${dataDir}/static_content"; 408 css_path = "${dataDir}/static_content/css"; 409 pictures_path = "${dataDir}/static_content/pictures"; 410 mhonarc = "${pkgs.perlPackages.MHonArc}/bin/mhonarc"; 411 }) 412 ); 413 414 services.sympa.settingsFile = 415 { 416 "virtual.sympa" = lib.mkDefault { source = virtual; }; 417 "transport.sympa" = lib.mkDefault { source = transport; }; 418 "etc/list_aliases.tt2" = lib.mkDefault { source = listAliases; }; 419 } 420 // (lib.flip lib.mapAttrs' cfg.domains ( 421 fqdn: domain: 422 lib.nameValuePair "etc/${fqdn}/robot.conf" (lib.mkDefault { source = robotConfig fqdn domain; }) 423 )); 424 425 environment = { 426 systemPackages = [ pkg ]; 427 }; 428 429 users.users.${user} = { 430 description = "Sympa mailing list manager user"; 431 group = group; 432 home = dataDir; 433 createHome = false; 434 isSystemUser = true; 435 }; 436 437 users.groups.${group} = { }; 438 439 assertions = [ 440 { 441 assertion = 442 cfg.database.createLocally -> cfg.database.user == user && cfg.database.name == cfg.database.user; 443 message = "services.sympa.database.user must be set to ${user} if services.sympa.database.createLocally is set to true"; 444 } 445 { 446 assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; 447 message = "a password cannot be specified if services.sympa.database.createLocally is set to true"; 448 } 449 ]; 450 451 systemd.tmpfiles.rules = 452 [ 453 "d ${dataDir} 0711 ${user} ${group} - -" 454 "d ${dataDir}/etc 0700 ${user} ${group} - -" 455 "d ${dataDir}/spool 0700 ${user} ${group} - -" 456 "d ${dataDir}/list_data 0700 ${user} ${group} - -" 457 "d ${dataDir}/arc 0700 ${user} ${group} - -" 458 "d ${dataDir}/bounce 0700 ${user} ${group} - -" 459 "f ${dataDir}/sympa_transport 0600 ${user} ${group} - -" 460 461 # force-copy static_content so it's up to date with package 462 # set permissions for wwsympa which needs write access (...) 463 "R ${dataDir}/static_content - - - - -" 464 "C ${dataDir}/static_content 0711 ${user} ${group} - ${pkg}/var/lib/sympa/static_content" 465 "e ${dataDir}/static_content/* 0711 ${user} ${group} - -" 466 467 "d /run/sympa 0755 ${user} ${group} - -" 468 ] 469 ++ (lib.flip lib.concatMap fqdns (fqdn: [ 470 "d ${dataDir}/etc/${fqdn} 0700 ${user} ${group} - -" 471 "d ${dataDir}/list_data/${fqdn} 0700 ${user} ${group} - -" 472 ])) 473 #++ (lib.flip lib.mapAttrsToList enabledFiles (k: v: 474 # "L+ ${dataDir}/${k} - - - - ${v.source}" 475 #)) 476 ++ (lib.concatLists ( 477 lib.flip lib.mapAttrsToList enabledFiles ( 478 k: v: [ 479 # sympa doesn't handle symlinks well (e.g. fails to create locks) 480 # force-copy instead 481 "R ${dataDir}/${k} - - - - -" 482 "C ${dataDir}/${k} 0700 ${user} ${group} - ${v.source}" 483 ] 484 ) 485 )); 486 487 systemd.services.sympa = { 488 description = "Sympa mailing list manager"; 489 490 wantedBy = [ "multi-user.target" ]; 491 after = [ "network-online.target" ]; 492 wants = sympaSubServices ++ [ "network-online.target" ]; 493 before = sympaSubServices; 494 serviceConfig = sympaServiceConfig "sympa_msg"; 495 496 preStart = '' 497 umask 0077 498 499 cp -f ${mainConfig} ${dataDir}/etc/sympa.conf 500 ${lib.optionalString (cfg.database.passwordFile != null) '' 501 chmod u+w ${dataDir}/etc/sympa.conf 502 echo -n "db_passwd " >> ${dataDir}/etc/sympa.conf 503 cat ${cfg.database.passwordFile} >> ${dataDir}/etc/sympa.conf 504 ''} 505 506 ${lib.optionalString (cfg.mta.type == "postfix") '' 507 ${pkgs.postfix}/bin/postmap hash:${dataDir}/virtual.sympa 508 ${pkgs.postfix}/bin/postmap hash:${dataDir}/transport.sympa 509 ''} 510 ${pkg}/bin/sympa_newaliases.pl 511 ${pkg}/bin/sympa.pl --health_check 512 ''; 513 }; 514 systemd.services.sympa-archive = { 515 description = "Sympa mailing list manager (archiving)"; 516 bindsTo = [ "sympa.service" ]; 517 serviceConfig = sympaServiceConfig "archived"; 518 }; 519 systemd.services.sympa-bounce = { 520 description = "Sympa mailing list manager (bounce processing)"; 521 bindsTo = [ "sympa.service" ]; 522 serviceConfig = sympaServiceConfig "bounced"; 523 }; 524 systemd.services.sympa-bulk = { 525 description = "Sympa mailing list manager (message distribution)"; 526 bindsTo = [ "sympa.service" ]; 527 serviceConfig = sympaServiceConfig "bulk"; 528 }; 529 systemd.services.sympa-task = { 530 description = "Sympa mailing list manager (task management)"; 531 bindsTo = [ "sympa.service" ]; 532 serviceConfig = sympaServiceConfig "task_manager"; 533 }; 534 535 systemd.services.wwsympa = lib.mkIf usingNginx { 536 wantedBy = [ "multi-user.target" ]; 537 after = [ "sympa.service" ]; 538 serviceConfig = { 539 Type = "forking"; 540 PIDFile = "/run/sympa/wwsympa.pid"; 541 Restart = "always"; 542 ExecStart = '' 543 ${pkgs.spawn_fcgi}/bin/spawn-fcgi \ 544 -u ${user} \ 545 -g ${group} \ 546 -U nginx \ 547 -M 0600 \ 548 -F ${toString cfg.web.fcgiProcs} \ 549 -P /run/sympa/wwsympa.pid \ 550 -s /run/sympa/wwsympa.socket \ 551 -- ${pkg}/lib/sympa/cgi/wwsympa.fcgi 552 ''; 553 554 } // commonServiceConfig; 555 }; 556 557 services.nginx.enable = lib.mkIf usingNginx true; 558 services.nginx.virtualHosts = lib.mkIf usingNginx ( 559 let 560 vHosts = lib.unique (lib.remove null (lib.mapAttrsToList (_k: v: v.webHost) cfg.domains)); 561 hostLocations = 562 host: map (v: v.webLocation) (lib.filter (v: v.webHost == host) (lib.attrValues cfg.domains)); 563 httpsOpts = lib.optionalAttrs cfg.web.https { 564 forceSSL = lib.mkDefault true; 565 enableACME = lib.mkDefault true; 566 }; 567 in 568 lib.genAttrs vHosts ( 569 host: 570 { 571 locations = 572 lib.genAttrs (hostLocations host) (loc: { 573 extraConfig = '' 574 include ${config.services.nginx.package}/conf/fastcgi_params; 575 576 fastcgi_pass unix:/run/sympa/wwsympa.socket; 577 ''; 578 }) 579 // { 580 "/static-sympa/".alias = "${dataDir}/static_content/"; 581 }; 582 } 583 // httpsOpts 584 ) 585 ); 586 587 services.postfix = lib.mkIf (cfg.mta.type == "postfix") { 588 enable = true; 589 recipientDelimiter = "+"; 590 config = { 591 virtual_alias_maps = [ "hash:${dataDir}/virtual.sympa" ]; 592 virtual_mailbox_maps = [ 593 "hash:${dataDir}/transport.sympa" 594 "hash:${dataDir}/sympa_transport" 595 "hash:${dataDir}/virtual.sympa" 596 ]; 597 virtual_mailbox_domains = [ "hash:${dataDir}/transport.sympa" ]; 598 transport_maps = [ 599 "hash:${dataDir}/transport.sympa" 600 "hash:${dataDir}/sympa_transport" 601 ]; 602 }; 603 masterConfig = { 604 "sympa" = { 605 type = "unix"; 606 privileged = true; 607 chroot = false; 608 command = "pipe"; 609 args = [ 610 "flags=hqRu" 611 "user=${user}" 612 "argv=${pkg}/libexec/queue" 613 "\${nexthop}" 614 ]; 615 }; 616 "sympabounce" = { 617 type = "unix"; 618 privileged = true; 619 chroot = false; 620 command = "pipe"; 621 args = [ 622 "flags=hqRu" 623 "user=${user}" 624 "argv=${pkg}/libexec/bouncequeue" 625 "\${nexthop}" 626 ]; 627 }; 628 }; 629 }; 630 631 services.mysql = lib.optionalAttrs mysqlLocal { 632 enable = true; 633 package = lib.mkDefault pkgs.mariadb; 634 ensureDatabases = [ cfg.database.name ]; 635 ensureUsers = [ 636 { 637 name = cfg.database.user; 638 ensurePermissions = { 639 "${cfg.database.name}.*" = "ALL PRIVILEGES"; 640 }; 641 } 642 ]; 643 }; 644 645 services.postgresql = lib.optionalAttrs pgsqlLocal { 646 enable = true; 647 ensureDatabases = [ cfg.database.name ]; 648 ensureUsers = [ 649 { 650 name = cfg.database.user; 651 ensureDBOwnership = true; 652 } 653 ]; 654 }; 655 656 }; 657 658 meta.maintainers = with lib.maintainers; [ 659 sorki 660 ]; 661}