1 { config, lib, pkgs, ... }:
6 cfg = config.services.sympa;
7 dataDir = "/var/lib/sympa";
11 fqdns = attrNames cfg.domains;
12 usingNginx = cfg.web.enable && cfg.web.server == "nginx";
13 mysqlLocal = cfg.database.createLocally && cfg.database.type == "MySQL";
14 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "PostgreSQL";
17 "sympa-archive.service"
18 "sympa-bounce.service"
23 # common for all services including wwsympa
24 commonServiceConfig = {
25 StateDirectory = "sympa";
27 ProtectSystem = "full";
28 ProtectControlGroups = true;
31 # wwsympa has its own service config
32 sympaServiceConfig = srv: {
35 ExecStart = "${pkg}/bin/${srv}.pl --foreground";
36 PIDFile = "/run/sympa/${srv}.pid";
40 # avoid duplicating log messageges in journal
41 StandardError = "null";
42 } // commonServiceConfig;
46 if value then "on" else "off"
48 configGenerator = c: concatStrings (flip mapAttrsToList c (key: val: "${key}\t${configVal val}\n"));
50 mainConfig = pkgs.writeText "sympa.conf" (configGenerator cfg.settings);
51 robotConfig = fqdn: domain: pkgs.writeText "${fqdn}-robot.conf" (configGenerator domain.settings);
53 transport = pkgs.writeText "transport.sympa" (concatStringsSep "\n" (flip map fqdns (domain: ''
54 ${domain} error:User unknown in recipient table
55 sympa@${domain} sympa:sympa@${domain}
56 listmaster@${domain} sympa:listmaster@${domain}
57 bounce@${domain} sympabounce:sympa@${domain}
58 abuse-feedback-report@${domain} sympabounce:sympa@${domain}
61 virtual = pkgs.writeText "virtual.sympa" (concatStringsSep "\n" (flip map fqdns (domain: ''
62 sympa-request@${domain} postmaster@localhost
63 sympa-owner@${domain} postmaster@localhost
66 listAliases = pkgs.writeText "list_aliases.tt2" ''
67 #--- [% list.name %]@[% list.domain %]: list transport map created at [% date %]
68 [% list.name %]@[% list.domain %] sympa:[% list.name %]@[% list.domain %]
69 [% list.name %]-request@[% list.domain %] sympa:[% list.name %]-request@[% list.domain %]
70 [% list.name %]-editor@[% list.domain %] sympa:[% list.name %]-editor@[% list.domain %]
71 #[% list.name %]-subscribe@[% list.domain %] sympa:[% list.name %]-subscribe@[%list.domain %]
72 [% list.name %]-unsubscribe@[% list.domain %] sympa:[% list.name %]-unsubscribe@[% list.domain %]
73 [% list.name %][% return_path_suffix %]@[% list.domain %] sympabounce:[% list.name %]@[% list.domain %]
76 enabledFiles = filterAttrs (n: v: v.enable) cfg.settingsFile;
81 options.services.sympa = with types; {
83 enable = mkEnableOption (lib.mdDoc "Sympa mailing list manager");
89 description = lib.mdDoc ''
90 Default Sympa language.
91 See <https://github.com/sympa-community/sympa/tree/sympa-6.2/po/sympa>
92 for available options.
96 listMasters = mkOption {
98 example = [ "postmaster@sympa.example.org" ];
99 description = lib.mdDoc ''
100 The list of the email addresses of the listmasters
101 (users authorized to perform global server commands).
105 mainDomain = mkOption {
108 example = "lists.example.org";
109 description = lib.mdDoc ''
110 Main domain to be used in {file}`sympa.conf`.
111 If `null`, one of the {option}`services.sympa.domains` is chosen for you.
116 type = attrsOf (submodule ({ name, config, ... }: {
121 example = "archive.example.org";
122 description = lib.mdDoc ''
123 Domain part of the web interface URL (no web interface for this domain if `null`).
124 DNS record of type A (or AAAA or CNAME) has to exist with this value.
127 webLocation = mkOption {
131 description = lib.mdDoc "URL path part of the web interface.";
133 settings = mkOption {
134 type = attrsOf (oneOf [ str int bool ]);
137 default_max_list_members = 3;
139 description = lib.mdDoc ''
140 The {file}`robot.conf` configuration file as key value set.
141 See <https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html>
142 for list of configuration parameters.
147 config.settings = mkIf (cfg.web.enable && config.webHost != null) {
148 wwsympa_url = mkDefault "https://${config.webHost}${strings.removeSuffix "/" config.webLocation}";
152 description = lib.mdDoc ''
153 Email domains handled by this instance. There have
154 to be MX records for keys of this attribute set.
156 example = literalExpression ''
158 "lists.example.org" = {
159 webHost = "lists.example.org";
162 "sympa.example.com" = {
163 webHost = "example.com";
164 webLocation = "/sympa";
172 type = enum [ "SQLite" "PostgreSQL" "MySQL" ];
175 description = lib.mdDoc "Database engine to use.";
181 description = lib.mdDoc ''
182 Database host address.
184 For MySQL, use `localhost` to connect using Unix domain socket.
186 For PostgreSQL, use path to directory (e.g. {file}`/run/postgresql`)
187 to connect using Unix domain socket located in this directory.
189 Use `null` to fall back on Sympa default, or when using
190 {option}`services.sympa.database.createLocally`.
197 description = lib.mdDoc "Database port. Use `null` for default port.";
202 default = if cfg.database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa";
203 defaultText = literalExpression ''if database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa"'';
204 description = lib.mdDoc ''
205 Database name. When using SQLite this must be an absolute
206 path to the database file.
213 description = lib.mdDoc "Database user. The system user name is used as a default.";
216 passwordFile = mkOption {
219 example = "/run/keys/sympa-dbpassword";
220 description = lib.mdDoc ''
221 A file containing the password for {option}`services.sympa.database.user`.
225 createLocally = mkOption {
228 description = lib.mdDoc "Whether to create a local database automatically.";
236 description = lib.mdDoc "Whether to enable Sympa web interface.";
240 type = enum [ "nginx" "none" ];
242 description = lib.mdDoc ''
243 The webserver used for the Sympa web interface. Set it to `none` if you want to configure it yourself.
244 Further nginx configuration can be done by adapting
245 {option}`services.nginx.virtualHosts.«name»`.
252 description = lib.mdDoc ''
253 Whether to use HTTPS. When nginx integration is enabled, this option forces SSL and enables ACME.
254 Please note that Sympa web interface always uses https links even when this option is disabled.
258 fcgiProcs = mkOption {
259 type = ints.positive;
261 description = lib.mdDoc "Number of FastCGI processes to fork.";
267 type = enum [ "postfix" "none" ];
269 description = lib.mdDoc ''
270 Mail transfer agent (MTA) integration. Use `none` if you want to configure it yourself.
272 The `postfix` integration sets up local Postfix instance that will pass incoming
273 messages from configured domains to Sympa. You still need to configure at least outgoing message
274 handling using e.g. {option}`services.postfix.relayHost`.
279 settings = mkOption {
280 type = attrsOf (oneOf [ str int bool ]);
282 example = literalExpression ''
284 default_home = "lists";
285 viewlogs_page_size = 50;
288 description = lib.mdDoc ''
289 The {file}`sympa.conf` configuration file as key value set.
290 See <https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html>
291 for list of configuration parameters.
295 settingsFile = mkOption {
296 type = attrsOf (submodule ({ name, config, ... }: {
301 description = lib.mdDoc "Whether this file should be generated. This option allows specific files to be disabled.";
306 description = lib.mdDoc "Text of the file.";
310 description = lib.mdDoc "Path of the source file.";
314 config.source = mkIf (config.text != null) (mkDefault (pkgs.writeText "sympa-${baseNameOf name}" config.text));
317 example = literalExpression ''
319 "list_data/lists.example.org/help" = {
320 text = "subject This list provides help to users";
324 description = lib.mdDoc "Set of files to be linked in {file}`${dataDir}`.";
328 ###### implementation
330 config = mkIf cfg.enable {
332 services.sympa.settings = (mapAttrs (_: v: mkDefault v) {
333 domain = if cfg.mainDomain != null then cfg.mainDomain else head fqdns;
334 listmaster = concatStringsSep "," cfg.listMasters;
337 home = "${dataDir}/list_data";
338 arc_path = "${dataDir}/arc";
339 bounce_path = "${dataDir}/bounce";
341 sendmail = "${pkgs.system-sendmail}/bin/sendmail";
343 db_type = cfg.database.type;
344 db_name = cfg.database.name;
346 // (optionalAttrs (cfg.database.host != null) {
347 db_host = cfg.database.host;
349 // (optionalAttrs mysqlLocal {
350 db_host = "localhost"; # use unix domain socket
352 // (optionalAttrs pgsqlLocal {
353 db_host = "/run/postgresql"; # use unix domain socket
355 // (optionalAttrs (cfg.database.port != null) {
356 db_port = cfg.database.port;
358 // (optionalAttrs (cfg.database.user != null) {
359 db_user = cfg.database.user;
361 // (optionalAttrs (cfg.mta.type == "postfix") {
362 sendmail_aliases = "${dataDir}/sympa_transport";
363 aliases_program = "${pkgs.postfix}/bin/postmap";
364 aliases_db_type = "hash";
366 // (optionalAttrs cfg.web.enable {
367 static_content_path = "${dataDir}/static_content";
368 css_path = "${dataDir}/static_content/css";
369 pictures_path = "${dataDir}/static_content/pictures";
370 mhonarc = "${pkgs.perlPackages.MHonArc}/bin/mhonarc";
373 services.sympa.settingsFile = {
374 "virtual.sympa" = mkDefault { source = virtual; };
375 "transport.sympa" = mkDefault { source = transport; };
376 "etc/list_aliases.tt2" = mkDefault { source = listAliases; };
378 // (flip mapAttrs' cfg.domains (fqdn: domain:
379 nameValuePair "etc/${fqdn}/robot.conf" (mkDefault { source = robotConfig fqdn domain; })));
382 systemPackages = [ pkg ];
385 users.users.${user} = {
386 description = "Sympa mailing list manager user";
393 users.groups.${group} = {};
396 { assertion = cfg.database.createLocally -> cfg.database.user == user;
397 message = "services.sympa.database.user must be set to ${user} if services.sympa.database.createLocally is set to true";
399 { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
400 message = "a password cannot be specified if services.sympa.database.createLocally is set to true";
404 systemd.tmpfiles.rules = [
405 "d ${dataDir} 0711 ${user} ${group} - -"
406 "d ${dataDir}/etc 0700 ${user} ${group} - -"
407 "d ${dataDir}/spool 0700 ${user} ${group} - -"
408 "d ${dataDir}/list_data 0700 ${user} ${group} - -"
409 "d ${dataDir}/arc 0700 ${user} ${group} - -"
410 "d ${dataDir}/bounce 0700 ${user} ${group} - -"
411 "f ${dataDir}/sympa_transport 0600 ${user} ${group} - -"
413 # force-copy static_content so it's up to date with package
414 # set permissions for wwsympa which needs write access (...)
415 "R ${dataDir}/static_content - - - - -"
416 "C ${dataDir}/static_content 0711 ${user} ${group} - ${pkg}/var/lib/sympa/static_content"
417 "e ${dataDir}/static_content/* 0711 ${user} ${group} - -"
419 "d /run/sympa 0755 ${user} ${group} - -"
421 ++ (flip concatMap fqdns (fqdn: [
422 "d ${dataDir}/etc/${fqdn} 0700 ${user} ${group} - -"
423 "d ${dataDir}/list_data/${fqdn} 0700 ${user} ${group} - -"
425 #++ (flip mapAttrsToList enabledFiles (k: v:
426 # "L+ ${dataDir}/${k} - - - - ${v.source}"
428 ++ (concatLists (flip mapAttrsToList enabledFiles (k: v: [
429 # sympa doesn't handle symlinks well (e.g. fails to create locks)
431 "R ${dataDir}/${k} - - - - -"
432 "C ${dataDir}/${k} 0700 ${user} ${group} - ${v.source}"
435 systemd.services.sympa = {
436 description = "Sympa mailing list manager";
438 wantedBy = [ "multi-user.target" ];
439 after = [ "network-online.target" ];
440 wants = sympaSubServices;
441 before = sympaSubServices;
442 serviceConfig = sympaServiceConfig "sympa_msg";
447 cp -f ${mainConfig} ${dataDir}/etc/sympa.conf
448 ${optionalString (cfg.database.passwordFile != null) ''
449 chmod u+w ${dataDir}/etc/sympa.conf
450 echo -n "db_passwd " >> ${dataDir}/etc/sympa.conf
451 cat ${cfg.database.passwordFile} >> ${dataDir}/etc/sympa.conf
454 ${optionalString (cfg.mta.type == "postfix") ''
455 ${pkgs.postfix}/bin/postmap hash:${dataDir}/virtual.sympa
456 ${pkgs.postfix}/bin/postmap hash:${dataDir}/transport.sympa
458 ${pkg}/bin/sympa_newaliases.pl
459 ${pkg}/bin/sympa.pl --health_check
462 systemd.services.sympa-archive = {
463 description = "Sympa mailing list manager (archiving)";
464 bindsTo = [ "sympa.service" ];
465 serviceConfig = sympaServiceConfig "archived";
467 systemd.services.sympa-bounce = {
468 description = "Sympa mailing list manager (bounce processing)";
469 bindsTo = [ "sympa.service" ];
470 serviceConfig = sympaServiceConfig "bounced";
472 systemd.services.sympa-bulk = {
473 description = "Sympa mailing list manager (message distribution)";
474 bindsTo = [ "sympa.service" ];
475 serviceConfig = sympaServiceConfig "bulk";
477 systemd.services.sympa-task = {
478 description = "Sympa mailing list manager (task management)";
479 bindsTo = [ "sympa.service" ];
480 serviceConfig = sympaServiceConfig "task_manager";
483 systemd.services.wwsympa = mkIf usingNginx {
484 wantedBy = [ "multi-user.target" ];
485 after = [ "sympa.service" ];
488 PIDFile = "/run/sympa/wwsympa.pid";
490 ExecStart = ''${pkgs.spawn_fcgi}/bin/spawn-fcgi \
495 -F ${toString cfg.web.fcgiProcs} \
496 -P /run/sympa/wwsympa.pid \
497 -s /run/sympa/wwsympa.socket \
498 -- ${pkg}/lib/sympa/cgi/wwsympa.fcgi
501 } // commonServiceConfig;
504 services.nginx.enable = mkIf usingNginx true;
505 services.nginx.virtualHosts = mkIf usingNginx (let
506 vHosts = unique (remove null (mapAttrsToList (_k: v: v.webHost) cfg.domains));
507 hostLocations = host: map (v: v.webLocation) (filter (v: v.webHost == host) (attrValues cfg.domains));
508 httpsOpts = optionalAttrs cfg.web.https { forceSSL = mkDefault true; enableACME = mkDefault true; };
510 genAttrs vHosts (host: {
511 locations = genAttrs (hostLocations host) (loc: {
513 include ${config.services.nginx.package}/conf/fastcgi_params;
515 fastcgi_pass unix:/run/sympa/wwsympa.socket;
518 "/static-sympa/".alias = "${dataDir}/static_content/";
522 services.postfix = mkIf (cfg.mta.type == "postfix") {
524 recipientDelimiter = "+";
526 virtual_alias_maps = [ "hash:${dataDir}/virtual.sympa" ];
527 virtual_mailbox_maps = [
528 "hash:${dataDir}/transport.sympa"
529 "hash:${dataDir}/sympa_transport"
530 "hash:${dataDir}/virtual.sympa"
532 virtual_mailbox_domains = [ "hash:${dataDir}/transport.sympa" ];
534 "hash:${dataDir}/transport.sympa"
535 "hash:${dataDir}/sympa_transport"
547 "argv=${pkg}/libexec/queue"
559 "argv=${pkg}/libexec/bouncequeue"
566 services.mysql = optionalAttrs mysqlLocal {
568 package = mkDefault pkgs.mariadb;
569 ensureDatabases = [ cfg.database.name ];
571 { name = cfg.database.user;
572 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
577 services.postgresql = optionalAttrs pgsqlLocal {
579 ensureDatabases = [ cfg.database.name ];
581 { name = cfg.database.user;
582 ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
589 meta.maintainers = with maintainers; [ mmilata sorki ];