at master 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 = lib.getExe' config.services.postfix.package "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 "virtual.sympa" = lib.mkDefault { source = virtual; }; 416 "transport.sympa" = lib.mkDefault { source = transport; }; 417 "etc/list_aliases.tt2" = lib.mkDefault { source = listAliases; }; 418 } 419 // (lib.flip lib.mapAttrs' cfg.domains ( 420 fqdn: domain: 421 lib.nameValuePair "etc/${fqdn}/robot.conf" (lib.mkDefault { source = robotConfig fqdn domain; }) 422 )); 423 424 environment = { 425 systemPackages = [ pkg ]; 426 }; 427 428 users.users.${user} = { 429 description = "Sympa mailing list manager user"; 430 group = group; 431 home = dataDir; 432 createHome = false; 433 isSystemUser = true; 434 }; 435 436 users.groups.${group} = { }; 437 438 assertions = [ 439 { 440 assertion = 441 cfg.database.createLocally -> cfg.database.user == user && cfg.database.name == cfg.database.user; 442 message = "services.sympa.database.user must be set to ${user} if services.sympa.database.createLocally is set to true"; 443 } 444 { 445 assertion = cfg.database.createLocally -> cfg.database.passwordFile == null; 446 message = "a password cannot be specified if services.sympa.database.createLocally is set to true"; 447 } 448 ]; 449 450 systemd.tmpfiles.rules = [ 451 "d ${dataDir} 0711 ${user} ${group} - -" 452 "d ${dataDir}/etc 0700 ${user} ${group} - -" 453 "d ${dataDir}/spool 0700 ${user} ${group} - -" 454 "d ${dataDir}/list_data 0700 ${user} ${group} - -" 455 "d ${dataDir}/arc 0700 ${user} ${group} - -" 456 "d ${dataDir}/bounce 0700 ${user} ${group} - -" 457 "f ${dataDir}/sympa_transport 0600 ${user} ${group} - -" 458 459 # force-copy static_content so it's up to date with package 460 # set permissions for wwsympa which needs write access (...) 461 "R ${dataDir}/static_content - - - - -" 462 "C ${dataDir}/static_content 0711 ${user} ${group} - ${pkg}/var/lib/sympa/static_content" 463 "e ${dataDir}/static_content/* 0711 ${user} ${group} - -" 464 465 "d /run/sympa 0755 ${user} ${group} - -" 466 ] 467 ++ (lib.flip lib.concatMap fqdns (fqdn: [ 468 "d ${dataDir}/etc/${fqdn} 0700 ${user} ${group} - -" 469 "d ${dataDir}/list_data/${fqdn} 0700 ${user} ${group} - -" 470 ])) 471 #++ (lib.flip lib.mapAttrsToList enabledFiles (k: v: 472 # "L+ ${dataDir}/${k} - - - - ${v.source}" 473 #)) 474 ++ (lib.concatLists ( 475 lib.flip lib.mapAttrsToList enabledFiles ( 476 k: v: [ 477 # sympa doesn't handle symlinks well (e.g. fails to create locks) 478 # force-copy instead 479 "R ${dataDir}/${k} - - - - -" 480 "C ${dataDir}/${k} 0700 ${user} ${group} - ${v.source}" 481 ] 482 ) 483 )); 484 485 systemd.services.sympa = { 486 description = "Sympa mailing list manager"; 487 488 wantedBy = [ "multi-user.target" ]; 489 after = [ "network-online.target" ]; 490 wants = sympaSubServices ++ [ "network-online.target" ]; 491 before = sympaSubServices; 492 serviceConfig = sympaServiceConfig "sympa_msg"; 493 494 preStart = '' 495 umask 0077 496 497 cp -f ${mainConfig} ${dataDir}/etc/sympa.conf 498 ${lib.optionalString (cfg.database.passwordFile != null) '' 499 chmod u+w ${dataDir}/etc/sympa.conf 500 echo -n "db_passwd " >> ${dataDir}/etc/sympa.conf 501 cat ${cfg.database.passwordFile} >> ${dataDir}/etc/sympa.conf 502 ''} 503 504 ${lib.optionalString (cfg.mta.type == "postfix") '' 505 ${lib.getExe' config.services.postfix.package "postmap"} hash:${dataDir}/virtual.sympa 506 ${lib.getExe' config.services.postfix.package "postmap"} hash:${dataDir}/transport.sympa 507 ''} 508 ${pkg}/bin/sympa_newaliases.pl 509 ${pkg}/bin/sympa.pl --health_check 510 ''; 511 }; 512 systemd.services.sympa-archive = { 513 description = "Sympa mailing list manager (archiving)"; 514 bindsTo = [ "sympa.service" ]; 515 serviceConfig = sympaServiceConfig "archived"; 516 }; 517 systemd.services.sympa-bounce = { 518 description = "Sympa mailing list manager (bounce processing)"; 519 bindsTo = [ "sympa.service" ]; 520 serviceConfig = sympaServiceConfig "bounced"; 521 }; 522 systemd.services.sympa-bulk = { 523 description = "Sympa mailing list manager (message distribution)"; 524 bindsTo = [ "sympa.service" ]; 525 serviceConfig = sympaServiceConfig "bulk"; 526 }; 527 systemd.services.sympa-task = { 528 description = "Sympa mailing list manager (task management)"; 529 bindsTo = [ "sympa.service" ]; 530 serviceConfig = sympaServiceConfig "task_manager"; 531 }; 532 533 systemd.services.wwsympa = lib.mkIf usingNginx { 534 wantedBy = [ "multi-user.target" ]; 535 after = [ "sympa.service" ]; 536 serviceConfig = { 537 Type = "forking"; 538 PIDFile = "/run/sympa/wwsympa.pid"; 539 Restart = "always"; 540 ExecStart = '' 541 ${pkgs.spawn_fcgi}/bin/spawn-fcgi \ 542 -u ${user} \ 543 -g ${group} \ 544 -U nginx \ 545 -M 0600 \ 546 -F ${toString cfg.web.fcgiProcs} \ 547 -P /run/sympa/wwsympa.pid \ 548 -s /run/sympa/wwsympa.socket \ 549 -- ${pkg}/lib/sympa/cgi/wwsympa.fcgi 550 ''; 551 552 } 553 // commonServiceConfig; 554 }; 555 556 services.nginx.enable = lib.mkIf usingNginx true; 557 services.nginx.virtualHosts = lib.mkIf usingNginx ( 558 let 559 vHosts = lib.unique (lib.remove null (lib.mapAttrsToList (_k: v: v.webHost) cfg.domains)); 560 hostLocations = 561 host: map (v: v.webLocation) (lib.filter (v: v.webHost == host) (lib.attrValues cfg.domains)); 562 httpsOpts = lib.optionalAttrs cfg.web.https { 563 forceSSL = lib.mkDefault true; 564 enableACME = lib.mkDefault true; 565 }; 566 in 567 lib.genAttrs vHosts ( 568 host: 569 { 570 locations = 571 lib.genAttrs (hostLocations host) (loc: { 572 extraConfig = '' 573 include ${config.services.nginx.package}/conf/fastcgi_params; 574 575 fastcgi_pass unix:/run/sympa/wwsympa.socket; 576 ''; 577 }) 578 // { 579 "/static-sympa/".alias = "${dataDir}/static_content/"; 580 }; 581 } 582 // httpsOpts 583 ) 584 ); 585 586 services.postfix = lib.mkIf (cfg.mta.type == "postfix") { 587 enable = true; 588 settings = { 589 main = { 590 recipient_delimiter = "+"; 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 master = { 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 632 services.mysql = lib.optionalAttrs mysqlLocal { 633 enable = true; 634 package = lib.mkDefault pkgs.mariadb; 635 ensureDatabases = [ cfg.database.name ]; 636 ensureUsers = [ 637 { 638 name = cfg.database.user; 639 ensurePermissions = { 640 "${cfg.database.name}.*" = "ALL PRIVILEGES"; 641 }; 642 } 643 ]; 644 }; 645 646 services.postgresql = lib.optionalAttrs pgsqlLocal { 647 enable = true; 648 ensureDatabases = [ cfg.database.name ]; 649 ensureUsers = [ 650 { 651 name = cfg.database.user; 652 ensureDBOwnership = true; 653 } 654 ]; 655 }; 656 657 }; 658 659 meta.maintainers = with lib.maintainers; [ 660 sorki 661 ]; 662}