fdc36e2c89
Because ProtectKernelModules implies NoNewPrivileges, postfix's sendmail executable, which is setgid, wasn't able to send mail.
594 lines
20 KiB
Nix
594 lines
20 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
with lib;
|
|
|
|
let
|
|
cfg = config.services.sympa;
|
|
dataDir = "/var/lib/sympa";
|
|
user = "sympa";
|
|
group = "sympa";
|
|
pkg = pkgs.sympa;
|
|
fqdns = attrNames cfg.domains;
|
|
usingNginx = cfg.web.enable && cfg.web.server == "nginx";
|
|
mysqlLocal = cfg.database.createLocally && cfg.database.type == "MySQL";
|
|
pgsqlLocal = cfg.database.createLocally && cfg.database.type == "PostgreSQL";
|
|
|
|
sympaSubServices = [
|
|
"sympa-archive.service"
|
|
"sympa-bounce.service"
|
|
"sympa-bulk.service"
|
|
"sympa-task.service"
|
|
];
|
|
|
|
# common for all services including wwsympa
|
|
commonServiceConfig = {
|
|
StateDirectory = "sympa";
|
|
ProtectHome = true;
|
|
ProtectSystem = "full";
|
|
ProtectControlGroups = true;
|
|
};
|
|
|
|
# wwsympa has its own service config
|
|
sympaServiceConfig = srv: {
|
|
Type = "simple";
|
|
Restart = "always";
|
|
ExecStart = "${pkg}/bin/${srv}.pl --foreground";
|
|
PIDFile = "/run/sympa/${srv}.pid";
|
|
User = user;
|
|
Group = group;
|
|
|
|
# avoid duplicating log messageges in journal
|
|
StandardError = "null";
|
|
} // commonServiceConfig;
|
|
|
|
configVal = value:
|
|
if isBool value then
|
|
if value then "on" else "off"
|
|
else toString value;
|
|
configGenerator = c: concatStrings (flip mapAttrsToList c (key: val: "${key}\t${configVal val}\n"));
|
|
|
|
mainConfig = pkgs.writeText "sympa.conf" (configGenerator cfg.settings);
|
|
robotConfig = fqdn: domain: pkgs.writeText "${fqdn}-robot.conf" (configGenerator domain.settings);
|
|
|
|
transport = pkgs.writeText "transport.sympa" (concatStringsSep "\n" (flip map fqdns (domain: ''
|
|
${domain} error:User unknown in recipient table
|
|
sympa@${domain} sympa:sympa@${domain}
|
|
listmaster@${domain} sympa:listmaster@${domain}
|
|
bounce@${domain} sympabounce:sympa@${domain}
|
|
abuse-feedback-report@${domain} sympabounce:sympa@${domain}
|
|
'')));
|
|
|
|
virtual = pkgs.writeText "virtual.sympa" (concatStringsSep "\n" (flip map fqdns (domain: ''
|
|
sympa-request@${domain} postmaster@localhost
|
|
sympa-owner@${domain} postmaster@localhost
|
|
'')));
|
|
|
|
listAliases = pkgs.writeText "list_aliases.tt2" ''
|
|
#--- [% list.name %]@[% list.domain %]: list transport map created at [% date %]
|
|
[% list.name %]@[% list.domain %] sympa:[% list.name %]@[% list.domain %]
|
|
[% list.name %]-request@[% list.domain %] sympa:[% list.name %]-request@[% list.domain %]
|
|
[% list.name %]-editor@[% list.domain %] sympa:[% list.name %]-editor@[% list.domain %]
|
|
#[% list.name %]-subscribe@[% list.domain %] sympa:[% list.name %]-subscribe@[%list.domain %]
|
|
[% list.name %]-unsubscribe@[% list.domain %] sympa:[% list.name %]-unsubscribe@[% list.domain %]
|
|
[% list.name %][% return_path_suffix %]@[% list.domain %] sympabounce:[% list.name %]@[% list.domain %]
|
|
'';
|
|
|
|
enabledFiles = filterAttrs (n: v: v.enable) cfg.settingsFile;
|
|
in
|
|
{
|
|
|
|
###### interface
|
|
options.services.sympa = with types; {
|
|
|
|
enable = mkEnableOption "Sympa mailing list manager";
|
|
|
|
lang = mkOption {
|
|
type = str;
|
|
default = "en_US";
|
|
example = "cs";
|
|
description = ''
|
|
Default Sympa language.
|
|
See <link xlink:href='https://github.com/sympa-community/sympa/tree/sympa-6.2/po/sympa' />
|
|
for available options.
|
|
'';
|
|
};
|
|
|
|
listMasters = mkOption {
|
|
type = listOf str;
|
|
example = [ "postmaster@sympa.example.org" ];
|
|
description = ''
|
|
The list of the email addresses of the listmasters
|
|
(users authorized to perform global server commands).
|
|
'';
|
|
};
|
|
|
|
mainDomain = mkOption {
|
|
type = nullOr str;
|
|
default = null;
|
|
example = "lists.example.org";
|
|
description = ''
|
|
Main domain to be used in <filename>sympa.conf</filename>.
|
|
If <literal>null</literal>, one of the <option>services.sympa.domains</option> is chosen for you.
|
|
'';
|
|
};
|
|
|
|
domains = mkOption {
|
|
type = attrsOf (submodule ({ name, config, ... }: {
|
|
options = {
|
|
webHost = mkOption {
|
|
type = nullOr str;
|
|
default = null;
|
|
example = "archive.example.org";
|
|
description = ''
|
|
Domain part of the web interface URL (no web interface for this domain if <literal>null</literal>).
|
|
DNS record of type A (or AAAA or CNAME) has to exist with this value.
|
|
'';
|
|
};
|
|
webLocation = mkOption {
|
|
type = str;
|
|
default = "/";
|
|
example = "/sympa";
|
|
description = "URL path part of the web interface.";
|
|
};
|
|
settings = mkOption {
|
|
type = attrsOf (oneOf [ str int bool ]);
|
|
default = {};
|
|
example = {
|
|
default_max_list_members = 3;
|
|
};
|
|
description = ''
|
|
The <filename>robot.conf</filename> configuration file as key value set.
|
|
See <link xlink:href='https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html' />
|
|
for list of configuration parameters.
|
|
'';
|
|
};
|
|
};
|
|
|
|
config.settings = mkIf (cfg.web.enable && config.webHost != null) {
|
|
wwsympa_url = mkDefault "https://${config.webHost}${strings.removeSuffix "/" config.webLocation}";
|
|
};
|
|
}));
|
|
|
|
description = ''
|
|
Email domains handled by this instance. There have
|
|
to be MX records for keys of this attribute set.
|
|
'';
|
|
example = literalExample ''
|
|
{
|
|
"lists.example.org" = {
|
|
webHost = "lists.example.org";
|
|
webLocation = "/";
|
|
};
|
|
"sympa.example.com" = {
|
|
webHost = "example.com";
|
|
webLocation = "/sympa";
|
|
};
|
|
}
|
|
'';
|
|
};
|
|
|
|
database = {
|
|
type = mkOption {
|
|
type = enum [ "SQLite" "PostgreSQL" "MySQL" ];
|
|
default = "SQLite";
|
|
example = "MySQL";
|
|
description = "Database engine to use.";
|
|
};
|
|
|
|
host = mkOption {
|
|
type = nullOr str;
|
|
default = null;
|
|
description = ''
|
|
Database host address.
|
|
|
|
For MySQL, use <literal>localhost</literal> to connect using Unix domain socket.
|
|
|
|
For PostgreSQL, use path to directory (e.g. <filename>/run/postgresql</filename>)
|
|
to connect using Unix domain socket located in this directory.
|
|
|
|
Use <literal>null</literal> to fall back on Sympa default, or when using
|
|
<option>services.sympa.database.createLocally</option>.
|
|
'';
|
|
};
|
|
|
|
port = mkOption {
|
|
type = nullOr port;
|
|
default = null;
|
|
description = "Database port. Use <literal>null</literal> for default port.";
|
|
};
|
|
|
|
name = mkOption {
|
|
type = str;
|
|
default = if cfg.database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa";
|
|
defaultText = ''if database.type == "SQLite" then "${dataDir}/sympa.sqlite" else "sympa"'';
|
|
description = ''
|
|
Database name. When using SQLite this must be an absolute
|
|
path to the database file.
|
|
'';
|
|
};
|
|
|
|
user = mkOption {
|
|
type = nullOr str;
|
|
default = user;
|
|
description = "Database user. The system user name is used as a default.";
|
|
};
|
|
|
|
passwordFile = mkOption {
|
|
type = nullOr path;
|
|
default = null;
|
|
example = "/run/keys/sympa-dbpassword";
|
|
description = ''
|
|
A file containing the password for <option>services.sympa.database.user</option>.
|
|
'';
|
|
};
|
|
|
|
createLocally = mkOption {
|
|
type = bool;
|
|
default = true;
|
|
description = "Whether to create a local database automatically.";
|
|
};
|
|
};
|
|
|
|
web = {
|
|
enable = mkOption {
|
|
type = bool;
|
|
default = true;
|
|
description = "Whether to enable Sympa web interface.";
|
|
};
|
|
|
|
server = mkOption {
|
|
type = enum [ "nginx" "none" ];
|
|
default = "nginx";
|
|
description = ''
|
|
The webserver used for the Sympa web interface. Set it to `none` if you want to configure it yourself.
|
|
Further nginx configuration can be done by adapting
|
|
<option>services.nginx.virtualHosts.<replaceable>name</replaceable></option>.
|
|
'';
|
|
};
|
|
|
|
https = mkOption {
|
|
type = bool;
|
|
default = true;
|
|
description = ''
|
|
Whether to use HTTPS. When nginx integration is enabled, this option forces SSL and enables ACME.
|
|
Please note that Sympa web interface always uses https links even when this option is disabled.
|
|
'';
|
|
};
|
|
|
|
fcgiProcs = mkOption {
|
|
type = ints.positive;
|
|
default = 2;
|
|
description = "Number of FastCGI processes to fork.";
|
|
};
|
|
};
|
|
|
|
mta = {
|
|
type = mkOption {
|
|
type = enum [ "postfix" "none" ];
|
|
default = "postfix";
|
|
description = ''
|
|
Mail transfer agent (MTA) integration. Use <literal>none</literal> if you want to configure it yourself.
|
|
|
|
The <literal>postfix</literal> integration sets up local Postfix instance that will pass incoming
|
|
messages from configured domains to Sympa. You still need to configure at least outgoing message
|
|
handling using e.g. <option>services.postfix.relayHost</option>.
|
|
'';
|
|
};
|
|
};
|
|
|
|
settings = mkOption {
|
|
type = attrsOf (oneOf [ str int bool ]);
|
|
default = {};
|
|
example = literalExample ''
|
|
{
|
|
default_home = "lists";
|
|
viewlogs_page_size = 50;
|
|
}
|
|
'';
|
|
description = ''
|
|
The <filename>sympa.conf</filename> configuration file as key value set.
|
|
See <link xlink:href='https://sympa-community.github.io/gpldoc/man/sympa.conf.5.html' />
|
|
for list of configuration parameters.
|
|
'';
|
|
};
|
|
|
|
settingsFile = mkOption {
|
|
type = attrsOf (submodule ({ name, config, ... }: {
|
|
options = {
|
|
enable = mkOption {
|
|
type = bool;
|
|
default = true;
|
|
description = "Whether this file should be generated. This option allows specific files to be disabled.";
|
|
};
|
|
text = mkOption {
|
|
default = null;
|
|
type = nullOr lines;
|
|
description = "Text of the file.";
|
|
};
|
|
source = mkOption {
|
|
type = path;
|
|
description = "Path of the source file.";
|
|
};
|
|
};
|
|
|
|
config.source = mkIf (config.text != null) (mkDefault (pkgs.writeText "sympa-${baseNameOf name}" config.text));
|
|
}));
|
|
default = {};
|
|
example = literalExample ''
|
|
{
|
|
"list_data/lists.example.org/help" = {
|
|
text = "subject This list provides help to users";
|
|
};
|
|
}
|
|
'';
|
|
description = "Set of files to be linked in <filename>${dataDir}</filename>.";
|
|
};
|
|
};
|
|
|
|
###### implementation
|
|
|
|
config = mkIf cfg.enable {
|
|
|
|
services.sympa.settings = (mapAttrs (_: v: mkDefault v) {
|
|
domain = if cfg.mainDomain != null then cfg.mainDomain else head fqdns;
|
|
listmaster = concatStringsSep "," cfg.listMasters;
|
|
lang = cfg.lang;
|
|
|
|
home = "${dataDir}/list_data";
|
|
arc_path = "${dataDir}/arc";
|
|
bounce_path = "${dataDir}/bounce";
|
|
|
|
sendmail = "${pkgs.system-sendmail}/bin/sendmail";
|
|
|
|
db_type = cfg.database.type;
|
|
db_name = cfg.database.name;
|
|
}
|
|
// (optionalAttrs (cfg.database.host != null) {
|
|
db_host = cfg.database.host;
|
|
})
|
|
// (optionalAttrs mysqlLocal {
|
|
db_host = "localhost"; # use unix domain socket
|
|
})
|
|
// (optionalAttrs pgsqlLocal {
|
|
db_host = "/run/postgresql"; # use unix domain socket
|
|
})
|
|
// (optionalAttrs (cfg.database.port != null) {
|
|
db_port = cfg.database.port;
|
|
})
|
|
// (optionalAttrs (cfg.database.user != null) {
|
|
db_user = cfg.database.user;
|
|
})
|
|
// (optionalAttrs (cfg.mta.type == "postfix") {
|
|
sendmail_aliases = "${dataDir}/sympa_transport";
|
|
aliases_program = "${pkgs.postfix}/bin/postmap";
|
|
aliases_db_type = "hash";
|
|
})
|
|
// (optionalAttrs cfg.web.enable {
|
|
static_content_path = "${dataDir}/static_content";
|
|
css_path = "${dataDir}/static_content/css";
|
|
pictures_path = "${dataDir}/static_content/pictures";
|
|
mhonarc = "${pkgs.perlPackages.MHonArc}/bin/mhonarc";
|
|
}));
|
|
|
|
services.sympa.settingsFile = {
|
|
"virtual.sympa" = mkDefault { source = virtual; };
|
|
"transport.sympa" = mkDefault { source = transport; };
|
|
"etc/list_aliases.tt2" = mkDefault { source = listAliases; };
|
|
}
|
|
// (flip mapAttrs' cfg.domains (fqdn: domain:
|
|
nameValuePair "etc/${fqdn}/robot.conf" (mkDefault { source = robotConfig fqdn domain; })));
|
|
|
|
environment = {
|
|
systemPackages = [ pkg ];
|
|
};
|
|
|
|
users.users.${user} = {
|
|
description = "Sympa mailing list manager user";
|
|
group = group;
|
|
home = dataDir;
|
|
createHome = false;
|
|
isSystemUser = true;
|
|
};
|
|
|
|
users.groups.${group} = {};
|
|
|
|
assertions = [
|
|
{ assertion = cfg.database.createLocally -> cfg.database.user == user;
|
|
message = "services.sympa.database.user must be set to ${user} if services.sympa.database.createLocally is set to true";
|
|
}
|
|
{ assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
|
|
message = "a password cannot be specified if services.sympa.database.createLocally is set to true";
|
|
}
|
|
];
|
|
|
|
systemd.tmpfiles.rules = [
|
|
"d ${dataDir} 0711 ${user} ${group} - -"
|
|
"d ${dataDir}/etc 0700 ${user} ${group} - -"
|
|
"d ${dataDir}/spool 0700 ${user} ${group} - -"
|
|
"d ${dataDir}/list_data 0700 ${user} ${group} - -"
|
|
"d ${dataDir}/arc 0700 ${user} ${group} - -"
|
|
"d ${dataDir}/bounce 0700 ${user} ${group} - -"
|
|
"f ${dataDir}/sympa_transport 0600 ${user} ${group} - -"
|
|
|
|
# force-copy static_content so it's up to date with package
|
|
# set permissions for wwsympa which needs write access (...)
|
|
"R ${dataDir}/static_content - - - - -"
|
|
"C ${dataDir}/static_content 0711 ${user} ${group} - ${pkg}/var/lib/sympa/static_content"
|
|
"e ${dataDir}/static_content/* 0711 ${user} ${group} - -"
|
|
|
|
"d /run/sympa 0755 ${user} ${group} - -"
|
|
]
|
|
++ (flip concatMap fqdns (fqdn: [
|
|
"d ${dataDir}/etc/${fqdn} 0700 ${user} ${group} - -"
|
|
"d ${dataDir}/list_data/${fqdn} 0700 ${user} ${group} - -"
|
|
]))
|
|
#++ (flip mapAttrsToList enabledFiles (k: v:
|
|
# "L+ ${dataDir}/${k} - - - - ${v.source}"
|
|
#))
|
|
++ (concatLists (flip mapAttrsToList enabledFiles (k: v: [
|
|
# sympa doesn't handle symlinks well (e.g. fails to create locks)
|
|
# force-copy instead
|
|
"R ${dataDir}/${k} - - - - -"
|
|
"C ${dataDir}/${k} 0700 ${user} ${group} - ${v.source}"
|
|
])));
|
|
|
|
systemd.services.sympa = {
|
|
description = "Sympa mailing list manager";
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network-online.target" ];
|
|
wants = sympaSubServices;
|
|
before = sympaSubServices;
|
|
serviceConfig = sympaServiceConfig "sympa_msg";
|
|
|
|
preStart = ''
|
|
umask 0077
|
|
|
|
cp -f ${mainConfig} ${dataDir}/etc/sympa.conf
|
|
${optionalString (cfg.database.passwordFile != null) ''
|
|
chmod u+w ${dataDir}/etc/sympa.conf
|
|
echo -n "db_passwd " >> ${dataDir}/etc/sympa.conf
|
|
cat ${cfg.database.passwordFile} >> ${dataDir}/etc/sympa.conf
|
|
''}
|
|
|
|
${optionalString (cfg.mta.type == "postfix") ''
|
|
${pkgs.postfix}/bin/postmap hash:${dataDir}/virtual.sympa
|
|
${pkgs.postfix}/bin/postmap hash:${dataDir}/transport.sympa
|
|
''}
|
|
${pkg}/bin/sympa_newaliases.pl
|
|
${pkg}/bin/sympa.pl --health_check
|
|
'';
|
|
};
|
|
systemd.services.sympa-archive = {
|
|
description = "Sympa mailing list manager (archiving)";
|
|
bindsTo = [ "sympa.service" ];
|
|
serviceConfig = sympaServiceConfig "archived";
|
|
};
|
|
systemd.services.sympa-bounce = {
|
|
description = "Sympa mailing list manager (bounce processing)";
|
|
bindsTo = [ "sympa.service" ];
|
|
serviceConfig = sympaServiceConfig "bounced";
|
|
};
|
|
systemd.services.sympa-bulk = {
|
|
description = "Sympa mailing list manager (message distribution)";
|
|
bindsTo = [ "sympa.service" ];
|
|
serviceConfig = sympaServiceConfig "bulk";
|
|
};
|
|
systemd.services.sympa-task = {
|
|
description = "Sympa mailing list manager (task management)";
|
|
bindsTo = [ "sympa.service" ];
|
|
serviceConfig = sympaServiceConfig "task_manager";
|
|
};
|
|
|
|
systemd.services.wwsympa = mkIf usingNginx {
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "sympa.service" ];
|
|
serviceConfig = {
|
|
Type = "forking";
|
|
PIDFile = "/run/sympa/wwsympa.pid";
|
|
Restart = "always";
|
|
ExecStart = ''${pkgs.spawn_fcgi}/bin/spawn-fcgi \
|
|
-u ${user} \
|
|
-g ${group} \
|
|
-U nginx \
|
|
-M 0600 \
|
|
-F ${toString cfg.web.fcgiProcs} \
|
|
-P /run/sympa/wwsympa.pid \
|
|
-s /run/sympa/wwsympa.socket \
|
|
-- ${pkg}/lib/sympa/cgi/wwsympa.fcgi
|
|
'';
|
|
|
|
} // commonServiceConfig;
|
|
};
|
|
|
|
services.nginx.enable = mkIf usingNginx true;
|
|
services.nginx.virtualHosts = mkIf usingNginx (let
|
|
vHosts = unique (remove null (mapAttrsToList (_k: v: v.webHost) cfg.domains));
|
|
hostLocations = host: map (v: v.webLocation) (filter (v: v.webHost == host) (attrValues cfg.domains));
|
|
httpsOpts = optionalAttrs cfg.web.https { forceSSL = mkDefault true; enableACME = mkDefault true; };
|
|
in
|
|
genAttrs vHosts (host: {
|
|
locations = genAttrs (hostLocations host) (loc: {
|
|
extraConfig = ''
|
|
include ${config.services.nginx.package}/conf/fastcgi_params;
|
|
|
|
fastcgi_pass unix:/run/sympa/wwsympa.socket;
|
|
fastcgi_split_path_info ^(${loc})(.*)$;
|
|
|
|
fastcgi_param PATH_INFO $fastcgi_path_info;
|
|
fastcgi_param SCRIPT_FILENAME ${pkg}/lib/sympa/cgi/wwsympa.fcgi;
|
|
'';
|
|
}) // {
|
|
"/static-sympa/".alias = "${dataDir}/static_content/";
|
|
};
|
|
} // httpsOpts));
|
|
|
|
services.postfix = mkIf (cfg.mta.type == "postfix") {
|
|
enable = true;
|
|
recipientDelimiter = "+";
|
|
config = {
|
|
virtual_alias_maps = [ "hash:${dataDir}/virtual.sympa" ];
|
|
virtual_mailbox_maps = [
|
|
"hash:${dataDir}/transport.sympa"
|
|
"hash:${dataDir}/sympa_transport"
|
|
"hash:${dataDir}/virtual.sympa"
|
|
];
|
|
virtual_mailbox_domains = [ "hash:${dataDir}/transport.sympa" ];
|
|
transport_maps = [
|
|
"hash:${dataDir}/transport.sympa"
|
|
"hash:${dataDir}/sympa_transport"
|
|
];
|
|
};
|
|
masterConfig = {
|
|
"sympa" = {
|
|
type = "unix";
|
|
privileged = true;
|
|
chroot = false;
|
|
command = "pipe";
|
|
args = [
|
|
"flags=hqRu"
|
|
"user=${user}"
|
|
"argv=${pkg}/libexec/queue"
|
|
"\${nexthop}"
|
|
];
|
|
};
|
|
"sympabounce" = {
|
|
type = "unix";
|
|
privileged = true;
|
|
chroot = false;
|
|
command = "pipe";
|
|
args = [
|
|
"flags=hqRu"
|
|
"user=${user}"
|
|
"argv=${pkg}/libexec/bouncequeue"
|
|
"\${nexthop}"
|
|
];
|
|
};
|
|
};
|
|
};
|
|
|
|
services.mysql = optionalAttrs mysqlLocal {
|
|
enable = true;
|
|
package = mkDefault pkgs.mariadb;
|
|
ensureDatabases = [ cfg.database.name ];
|
|
ensureUsers = [
|
|
{ name = cfg.database.user;
|
|
ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
|
|
}
|
|
];
|
|
};
|
|
|
|
services.postgresql = optionalAttrs pgsqlLocal {
|
|
enable = true;
|
|
ensureDatabases = [ cfg.database.name ];
|
|
ensureUsers = [
|
|
{ name = cfg.database.user;
|
|
ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
|
|
}
|
|
];
|
|
};
|
|
|
|
};
|
|
|
|
meta.maintainers = with maintainers; [ mmilata sorki ];
|
|
}
|