1 { config, lib, pkgs, ... }:
3 cfg = config.services.sympa;
4 dataDir = "/var/lib/sympa";
8 fqdns = lib.attrNames cfg.domains;
9 usingNginx = cfg.web.enable && cfg.web.server == "nginx";
10 mysqlLocal = cfg.database.createLocally && cfg.database.type == "MySQL";
11 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "PostgreSQL";
14 "sympa-archive.service"
15 "sympa-bounce.service"
20 # common for all services including wwsympa
21 commonServiceConfig = {
22 StateDirectory = "sympa";
24 ProtectSystem = "full";
25 ProtectControlGroups = true;
28 # wwsympa has its own service config
29 sympaServiceConfig = srv: {
32 ExecStart = "${pkg}/bin/${srv}.pl --foreground";
33 PIDFile = "/run/sympa/${srv}.pid";
37 # avoid duplicating log messageges in journal
38 StandardError = "null";
39 } // commonServiceConfig;
42 if lib.isBool value then
43 if value then "on" else "off"
45 configGenerator = c: lib.concatStrings (lib.flip lib.mapAttrsToList c (key: val: "${key}\t${configVal val}\n"));
47 mainConfig = pkgs.writeText "sympa.conf" (configGenerator cfg.settings);
48 robotConfig = fqdn: domain: pkgs.writeText "${fqdn}-robot.conf" (configGenerator domain.settings);
50 transport = pkgs.writeText "transport.sympa" (lib.concatStringsSep "\n" (lib.flip map fqdns (domain: ''
51 ${domain} error:User unknown in recipient table
52 sympa@${domain} sympa:sympa@${domain}
53 listmaster@${domain} sympa:listmaster@${domain}
54 bounce@${domain} sympabounce:sympa@${domain}
55 abuse-feedback-report@${domain} sympabounce:sympa@${domain}
58 virtual = pkgs.writeText "virtual.sympa" (lib.concatStringsSep "\n" (lib.flip map fqdns (domain: ''
59 sympa-request@${domain} postmaster@localhost
60 sympa-owner@${domain} postmaster@localhost
63 listAliases = pkgs.writeText "list_aliases.tt2" ''
64 #--- [% list.name %]@[% list.domain %]: list transport map created at [% date %]
65 [% list.name %]@[% list.domain %] sympa:[% list.name %]@[% list.domain %]
66 [% list.name %]-request@[% list.domain %] sympa:[% list.name %]-request@[% list.domain %]
67 [% list.name %]-editor@[% list.domain %] sympa:[% list.name %]-editor@[% list.domain %]
68 #[% list.name %]-subscribe@[% list.domain %] sympa:[% list.name %]-subscribe@[%list.domain %]
69 [% list.name %]-unsubscribe@[% list.domain %] sympa:[% list.name %]-unsubscribe@[% list.domain %]
70 [% list.name %][% return_path_suffix %]@[% list.domain %] sympabounce:[% list.name %]@[% list.domain %]
73 enabledFiles = lib.filterAttrs (n: v: v.enable) cfg.settingsFile;
78 options.services.sympa = with lib.types; {
80 enable = lib.mkEnableOption "Sympa mailing list manager";
87 Default Sympa language.
88 See <https://github.com/sympa-community/sympa/tree/sympa-6.2/po/sympa>
89 for available options.
93 listMasters = lib.mkOption {
95 example = [ "postmaster@sympa.example.org" ];
97 The list of the email addresses of the listmasters
98 (users authorized to perform global server commands).
102 mainDomain = lib.mkOption {
105 example = "lists.example.org";
107 Main domain to be used in {file}`sympa.conf`.
108 If `null`, one of the {option}`services.sympa.domains` is chosen for you.
112 domains = lib.mkOption {
113 type = attrsOf (submodule ({ name, config, ... }: {
115 webHost = lib.mkOption {
118 example = "archive.example.org";
120 Domain part of the web interface URL (no web interface for this domain if `null`).
121 DNS record of type A (or AAAA or CNAME) has to exist with this value.
124 webLocation = lib.mkOption {
128 description = "URL path part of the web interface.";
130 settings = lib.mkOption {
131 type = attrsOf (oneOf [ str int bool ]);
134 default_max_list_members = 3;
137 The {file}`robot.conf` configuration file as key value set.
138 See <https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html>
139 for list of configuration parameters.
144 config.settings = lib.mkIf (cfg.web.enable && config.webHost != null) {
145 wwsympa_url = lib.mkDefault "https://${config.webHost}${lib.removeSuffix "/" config.webLocation}";
150 Email domains handled by this instance. There have
151 to be MX records for keys of this attribute set.
153 example = lib.literalExpression ''
155 "lists.example.org" = {
156 webHost = "lists.example.org";
159 "sympa.example.com" = {
160 webHost = "example.com";
161 webLocation = "/sympa";
168 type = lib.mkOption {
169 type = enum [ "SQLite" "PostgreSQL" "MySQL" ];
172 description = "Database engine to use.";
175 host = lib.mkOption {
179 Database host address.
181 For MySQL, use `localhost` to connect using Unix domain socket.
183 For PostgreSQL, use path to directory (e.g. {file}`/run/postgresql`)
184 to connect using Unix domain socket located in this directory.
186 Use `null` to fall back on Sympa default, or when using
187 {option}`services.sympa.database.createLocally`.
191 port = lib.mkOption {
194 description = "Database port. Use `null` for default port.";
197 name = lib.mkOption {
199 default = if cfg.database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa";
200 defaultText = lib.literalExpression ''if database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa"'';
202 Database name. When using SQLite this must be an absolute
203 path to the database file.
207 user = lib.mkOption {
210 description = "Database user. The system user name is used as a default.";
213 passwordFile = lib.mkOption {
216 example = "/run/keys/sympa-dbpassword";
218 A file containing the password for {option}`services.sympa.database.name`.
222 createLocally = lib.mkOption {
225 description = "Whether to create a local database automatically.";
230 enable = lib.mkOption {
233 description = "Whether to enable Sympa web interface.";
236 server = lib.mkOption {
237 type = enum [ "nginx" "none" ];
240 The webserver used for the Sympa web interface. Set it to `none` if you want to configure it yourself.
241 Further nginx configuration can be done by adapting
242 {option}`services.nginx.virtualHosts.«name»`.
246 https = lib.mkOption {
250 Whether to use HTTPS. When nginx integration is enabled, this option forces SSL and enables ACME.
251 Please note that Sympa web interface always uses https links even when this option is disabled.
255 fcgiProcs = lib.mkOption {
256 type = ints.positive;
258 description = "Number of FastCGI processes to fork.";
263 type = lib.mkOption {
264 type = enum [ "postfix" "none" ];
267 Mail transfer agent (MTA) integration. Use `none` if you want to configure it yourself.
269 The `postfix` integration sets up local Postfix instance that will pass incoming
270 messages from configured domains to Sympa. You still need to configure at least outgoing message
271 handling using e.g. {option}`services.postfix.relayHost`.
276 settings = lib.mkOption {
277 type = attrsOf (oneOf [ str int bool ]);
279 example = lib.literalExpression ''
281 default_home = "lists";
282 viewlogs_page_size = 50;
286 The {file}`sympa.conf` configuration file as key value set.
287 See <https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html>
288 for list of configuration parameters.
292 settingsFile = lib.mkOption {
293 type = attrsOf (submodule ({ name, config, ... }: {
295 enable = lib.mkOption {
298 description = "Whether this file should be generated. This option allows specific files to be disabled.";
300 text = lib.mkOption {
303 description = "Text of the file.";
305 source = lib.mkOption {
307 description = "Path of the source file.";
311 config.source = lib.mkIf (config.text != null) (lib.mkDefault (pkgs.writeText "sympa-${baseNameOf name}" config.text));
314 example = lib.literalExpression ''
316 "list_data/lists.example.org/help" = {
317 text = "subject This list provides help to users";
321 description = "Set of files to be linked in {file}`${dataDir}`.";
325 ###### implementation
327 config = lib.mkIf cfg.enable {
329 services.sympa.settings = (lib.mapAttrs (_: v: lib.mkDefault v) {
330 domain = if cfg.mainDomain != null then cfg.mainDomain else lib.head fqdns;
331 listmaster = lib.concatStringsSep "," cfg.listMasters;
334 home = "${dataDir}/list_data";
335 arc_path = "${dataDir}/arc";
336 bounce_path = "${dataDir}/bounce";
338 sendmail = "${pkgs.system-sendmail}/bin/sendmail";
340 db_type = cfg.database.type;
341 db_name = cfg.database.name;
342 db_user = cfg.database.name;
344 // (lib.optionalAttrs (cfg.database.host != null) {
345 db_host = cfg.database.host;
347 // (lib.optionalAttrs mysqlLocal {
348 db_host = "localhost"; # use unix domain socket
350 // (lib.optionalAttrs pgsqlLocal {
351 db_host = "/run/postgresql"; # use unix domain socket
353 // (lib.optionalAttrs (cfg.database.port != null) {
354 db_port = cfg.database.port;
356 // (lib.optionalAttrs (cfg.mta.type == "postfix") {
357 sendmail_aliases = "${dataDir}/sympa_transport";
358 aliases_program = "${pkgs.postfix}/bin/postmap";
359 aliases_db_type = "hash";
361 // (lib.optionalAttrs cfg.web.enable {
362 static_content_path = "${dataDir}/static_content";
363 css_path = "${dataDir}/static_content/css";
364 pictures_path = "${dataDir}/static_content/pictures";
365 mhonarc = "${pkgs.perlPackages.MHonArc}/bin/mhonarc";
368 services.sympa.settingsFile = {
369 "virtual.sympa" = lib.mkDefault { source = virtual; };
370 "transport.sympa" = lib.mkDefault { source = transport; };
371 "etc/list_aliases.tt2" = lib.mkDefault { source = listAliases; };
373 // (lib.flip lib.mapAttrs' cfg.domains (fqdn: domain:
374 lib.nameValuePair "etc/${fqdn}/robot.conf" (lib.mkDefault { source = robotConfig fqdn domain; })));
377 systemPackages = [ pkg ];
380 users.users.${user} = {
381 description = "Sympa mailing list manager user";
388 users.groups.${group} = {};
391 { assertion = cfg.database.createLocally -> cfg.database.user == user && cfg.database.name == cfg.database.user;
392 message = "services.sympa.database.user must be set to ${user} if services.sympa.database.createLocally is set to true";
394 { assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
395 message = "a password cannot be specified if services.sympa.database.createLocally is set to true";
399 systemd.tmpfiles.rules = [
400 "d ${dataDir} 0711 ${user} ${group} - -"
401 "d ${dataDir}/etc 0700 ${user} ${group} - -"
402 "d ${dataDir}/spool 0700 ${user} ${group} - -"
403 "d ${dataDir}/list_data 0700 ${user} ${group} - -"
404 "d ${dataDir}/arc 0700 ${user} ${group} - -"
405 "d ${dataDir}/bounce 0700 ${user} ${group} - -"
406 "f ${dataDir}/sympa_transport 0600 ${user} ${group} - -"
408 # force-copy static_content so it's up to date with package
409 # set permissions for wwsympa which needs write access (...)
410 "R ${dataDir}/static_content - - - - -"
411 "C ${dataDir}/static_content 0711 ${user} ${group} - ${pkg}/var/lib/sympa/static_content"
412 "e ${dataDir}/static_content/* 0711 ${user} ${group} - -"
414 "d /run/sympa 0755 ${user} ${group} - -"
416 ++ (lib.flip lib.concatMap fqdns (fqdn: [
417 "d ${dataDir}/etc/${fqdn} 0700 ${user} ${group} - -"
418 "d ${dataDir}/list_data/${fqdn} 0700 ${user} ${group} - -"
420 #++ (lib.flip lib.mapAttrsToList enabledFiles (k: v:
421 # "L+ ${dataDir}/${k} - - - - ${v.source}"
423 ++ (lib.concatLists (lib.flip lib.mapAttrsToList enabledFiles (k: v: [
424 # sympa doesn't handle symlinks well (e.g. fails to create locks)
426 "R ${dataDir}/${k} - - - - -"
427 "C ${dataDir}/${k} 0700 ${user} ${group} - ${v.source}"
430 systemd.services.sympa = {
431 description = "Sympa mailing list manager";
433 wantedBy = [ "multi-user.target" ];
434 after = [ "network-online.target" ];
435 wants = sympaSubServices ++ [ "network-online.target" ];
436 before = sympaSubServices;
437 serviceConfig = sympaServiceConfig "sympa_msg";
442 cp -f ${mainConfig} ${dataDir}/etc/sympa.conf
443 ${lib.optionalString (cfg.database.passwordFile != null) ''
444 chmod u+w ${dataDir}/etc/sympa.conf
445 echo -n "db_passwd " >> ${dataDir}/etc/sympa.conf
446 cat ${cfg.database.passwordFile} >> ${dataDir}/etc/sympa.conf
449 ${lib.optionalString (cfg.mta.type == "postfix") ''
450 ${pkgs.postfix}/bin/postmap hash:${dataDir}/virtual.sympa
451 ${pkgs.postfix}/bin/postmap hash:${dataDir}/transport.sympa
453 ${pkg}/bin/sympa_newaliases.pl
454 ${pkg}/bin/sympa.pl --health_check
457 systemd.services.sympa-archive = {
458 description = "Sympa mailing list manager (archiving)";
459 bindsTo = [ "sympa.service" ];
460 serviceConfig = sympaServiceConfig "archived";
462 systemd.services.sympa-bounce = {
463 description = "Sympa mailing list manager (bounce processing)";
464 bindsTo = [ "sympa.service" ];
465 serviceConfig = sympaServiceConfig "bounced";
467 systemd.services.sympa-bulk = {
468 description = "Sympa mailing list manager (message distribution)";
469 bindsTo = [ "sympa.service" ];
470 serviceConfig = sympaServiceConfig "bulk";
472 systemd.services.sympa-task = {
473 description = "Sympa mailing list manager (task management)";
474 bindsTo = [ "sympa.service" ];
475 serviceConfig = sympaServiceConfig "task_manager";
478 systemd.services.wwsympa = lib.mkIf usingNginx {
479 wantedBy = [ "multi-user.target" ];
480 after = [ "sympa.service" ];
483 PIDFile = "/run/sympa/wwsympa.pid";
485 ExecStart = ''${pkgs.spawn_fcgi}/bin/spawn-fcgi \
490 -F ${toString cfg.web.fcgiProcs} \
491 -P /run/sympa/wwsympa.pid \
492 -s /run/sympa/wwsympa.socket \
493 -- ${pkg}/lib/sympa/cgi/wwsympa.fcgi
496 } // commonServiceConfig;
499 services.nginx.enable = lib.mkIf usingNginx true;
500 services.nginx.virtualHosts = lib.mkIf usingNginx (let
501 vHosts = lib.unique (lib.remove null (lib.mapAttrsToList (_k: v: v.webHost) cfg.domains));
502 hostLocations = host: map (v: v.webLocation) (lib.filter (v: v.webHost == host) (lib.attrValues cfg.domains));
503 httpsOpts = lib.optionalAttrs cfg.web.https { forceSSL = lib.mkDefault true; enableACME = lib.mkDefault true; };
505 lib.genAttrs vHosts (host: {
506 locations = lib.genAttrs (hostLocations host) (loc: {
508 include ${config.services.nginx.package}/conf/fastcgi_params;
510 fastcgi_pass unix:/run/sympa/wwsympa.socket;
513 "/static-sympa/".alias = "${dataDir}/static_content/";
517 services.postfix = lib.mkIf (cfg.mta.type == "postfix") {
519 recipientDelimiter = "+";
521 virtual_alias_maps = [ "hash:${dataDir}/virtual.sympa" ];
522 virtual_mailbox_maps = [
523 "hash:${dataDir}/transport.sympa"
524 "hash:${dataDir}/sympa_transport"
525 "hash:${dataDir}/virtual.sympa"
527 virtual_mailbox_domains = [ "hash:${dataDir}/transport.sympa" ];
529 "hash:${dataDir}/transport.sympa"
530 "hash:${dataDir}/sympa_transport"
542 "argv=${pkg}/libexec/queue"
554 "argv=${pkg}/libexec/bouncequeue"
561 services.mysql = lib.optionalAttrs mysqlLocal {
563 package = lib.mkDefault pkgs.mariadb;
564 ensureDatabases = [ cfg.database.name ];
566 { name = cfg.database.user;
567 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
572 services.postgresql = lib.optionalAttrs pgsqlLocal {
574 ensureDatabases = [ cfg.database.name ];
576 { name = cfg.database.user;
577 ensureDBOwnership = true;
584 meta.maintainers = with lib.maintainers; [ mmilata sorki ];