uboot: (firmwareOdroidC2/C4) don't invoke patch tool, use patches = [] instead
https://github.com/NixOS/nixpkgs/blob/master/pkgs/stdenv/generic/setup.sh#L948 this can do it nicely. Signed-off-by: Anton Arapov <anton@deadbeef.mx>
This commit is contained in:
commit
56de2bcd43
30691 changed files with 3076956 additions and 0 deletions
181
nixos/modules/services/mail/clamsmtp.nix
Normal file
181
nixos/modules/services/mail/clamsmtp.nix
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.clamsmtp;
|
||||
clamdSocket = "/run/clamav/clamd.ctl"; # See services/security/clamav.nix
|
||||
in
|
||||
{
|
||||
##### interface
|
||||
options = {
|
||||
services.clamsmtp = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable clamsmtp.";
|
||||
};
|
||||
|
||||
instances = mkOption {
|
||||
description = "Instances of clamsmtp to run.";
|
||||
type = types.listOf (types.submodule { options = {
|
||||
action = mkOption {
|
||||
type = types.enum [ "bounce" "drop" "pass" ];
|
||||
default = "drop";
|
||||
description =
|
||||
''
|
||||
Action to take when a virus is detected.
|
||||
|
||||
Note that viruses often spoof sender addresses, so bouncing is
|
||||
in most cases not a good idea.
|
||||
'';
|
||||
};
|
||||
|
||||
header = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
example = "X-Virus-Scanned: ClamAV using ClamSMTP";
|
||||
description =
|
||||
''
|
||||
A header to add to scanned messages. See clamsmtpd.conf(5) for
|
||||
more details. Empty means no header.
|
||||
'';
|
||||
};
|
||||
|
||||
keepAlives = mkOption {
|
||||
type = types.int;
|
||||
default = 0;
|
||||
description =
|
||||
''
|
||||
Number of seconds to wait between each NOOP sent to the sending
|
||||
server. 0 to disable.
|
||||
|
||||
This is meant for slow servers where the sending MTA times out
|
||||
waiting for clamd to scan the file.
|
||||
'';
|
||||
};
|
||||
|
||||
listen = mkOption {
|
||||
type = types.str;
|
||||
example = "127.0.0.1:10025";
|
||||
description =
|
||||
''
|
||||
Address to wait for incoming SMTP connections on. See
|
||||
clamsmtpd.conf(5) for more details.
|
||||
'';
|
||||
};
|
||||
|
||||
quarantine = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
''
|
||||
Whether to quarantine files that contain viruses by leaving them
|
||||
in the temporary directory.
|
||||
'';
|
||||
};
|
||||
|
||||
maxConnections = mkOption {
|
||||
type = types.int;
|
||||
default = 64;
|
||||
description = "Maximum number of connections to accept at once.";
|
||||
};
|
||||
|
||||
outAddress = mkOption {
|
||||
type = types.str;
|
||||
description =
|
||||
''
|
||||
Address of the SMTP server to send email to once it has been
|
||||
scanned.
|
||||
'';
|
||||
};
|
||||
|
||||
tempDirectory = mkOption {
|
||||
type = types.str;
|
||||
default = "/tmp";
|
||||
description =
|
||||
''
|
||||
Temporary directory that needs to be accessible to both clamd
|
||||
and clamsmtpd.
|
||||
'';
|
||||
};
|
||||
|
||||
timeout = mkOption {
|
||||
type = types.int;
|
||||
default = 180;
|
||||
description = "Time-out for network connections.";
|
||||
};
|
||||
|
||||
transparentProxy = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable clamsmtp's transparent proxy support.";
|
||||
};
|
||||
|
||||
virusAction = mkOption {
|
||||
type = with types; nullOr path;
|
||||
default = null;
|
||||
description =
|
||||
''
|
||||
Command to run when a virus is found. Please see VIRUS ACTION in
|
||||
clamsmtpd(8) for a discussion of this option and its safe use.
|
||||
'';
|
||||
};
|
||||
|
||||
xClient = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
''
|
||||
Send the XCLIENT command to the receiving server, for forwarding
|
||||
client addresses and connection information if the receiving
|
||||
server supports this feature.
|
||||
'';
|
||||
};
|
||||
};});
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
##### implementation
|
||||
config = let
|
||||
configfile = conf: pkgs.writeText "clamsmtpd.conf"
|
||||
''
|
||||
Action: ${conf.action}
|
||||
ClamAddress: ${clamdSocket}
|
||||
Header: ${conf.header}
|
||||
KeepAlives: ${toString conf.keepAlives}
|
||||
Listen: ${conf.listen}
|
||||
Quarantine: ${if conf.quarantine then "on" else "off"}
|
||||
MaxConnections: ${toString conf.maxConnections}
|
||||
OutAddress: ${conf.outAddress}
|
||||
TempDirectory: ${conf.tempDirectory}
|
||||
TimeOut: ${toString conf.timeout}
|
||||
TransparentProxy: ${if conf.transparentProxy then "on" else "off"}
|
||||
User: clamav
|
||||
${optionalString (conf.virusAction != null) "VirusAction: ${conf.virusAction}"}
|
||||
XClient: ${if conf.xClient then "on" else "off"}
|
||||
'';
|
||||
in
|
||||
mkIf cfg.enable {
|
||||
assertions = [
|
||||
{ assertion = config.services.clamav.daemon.enable;
|
||||
message = "clamsmtp requires clamav to be enabled";
|
||||
}
|
||||
];
|
||||
|
||||
systemd.services = listToAttrs (imap1 (i: conf:
|
||||
nameValuePair "clamsmtp-${toString i}" {
|
||||
description = "ClamSMTP instance ${toString i}";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
script = "exec ${pkgs.clamsmtp}/bin/clamsmtpd -f ${configfile conf}";
|
||||
after = [ "clamav-daemon.service" ];
|
||||
requires = [ "clamav-daemon.service" ];
|
||||
serviceConfig.Type = "forking";
|
||||
serviceConfig.PrivateTmp = "yes";
|
||||
unitConfig.JoinsNamespaceOf = "clamav-daemon.service";
|
||||
}
|
||||
) cfg.instances);
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ ekleog ];
|
||||
}
|
||||
99
nixos/modules/services/mail/davmail.nix
Normal file
99
nixos/modules/services/mail/davmail.nix
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.davmail;
|
||||
|
||||
configType = with types;
|
||||
oneOf [ (attrsOf configType) str int bool ] // {
|
||||
description = "davmail config type (str, int, bool or attribute set thereof)";
|
||||
};
|
||||
|
||||
toStr = val: if isBool val then boolToString val else toString val;
|
||||
|
||||
linesForAttrs = attrs: concatMap (name: let value = attrs.${name}; in
|
||||
if isAttrs value
|
||||
then map (line: name + "." + line) (linesForAttrs value)
|
||||
else [ "${name}=${toStr value}" ]
|
||||
) (attrNames attrs);
|
||||
|
||||
configFile = pkgs.writeText "davmail.properties" (concatStringsSep "\n" (linesForAttrs cfg.config));
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options.services.davmail = {
|
||||
enable = mkEnableOption "davmail, an MS Exchange gateway";
|
||||
|
||||
url = mkOption {
|
||||
type = types.str;
|
||||
description = "Outlook Web Access URL to access the exchange server, i.e. the base webmail URL.";
|
||||
example = "https://outlook.office365.com/EWS/Exchange.asmx";
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = configType;
|
||||
default = {};
|
||||
description = ''
|
||||
Davmail configuration. Refer to
|
||||
<link xlink:href="http://davmail.sourceforge.net/serversetup.html"/>
|
||||
and <link xlink:href="http://davmail.sourceforge.net/advanced.html"/>
|
||||
for details on supported values.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{
|
||||
davmail.allowRemote = true;
|
||||
davmail.imapPort = 55555;
|
||||
davmail.bindAddress = "10.0.1.2";
|
||||
davmail.smtpSaveInSent = true;
|
||||
davmail.folderSizeLimit = 10;
|
||||
davmail.caldavAutoSchedule = false;
|
||||
log4j.logger.rootLogger = "DEBUG";
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
services.davmail.config = {
|
||||
davmail = mapAttrs (name: mkDefault) {
|
||||
server = true;
|
||||
disableUpdateCheck = true;
|
||||
logFilePath = "/var/log/davmail/davmail.log";
|
||||
logFileSize = "1MB";
|
||||
mode = "auto";
|
||||
url = cfg.url;
|
||||
caldavPort = 1080;
|
||||
imapPort = 1143;
|
||||
ldapPort = 1389;
|
||||
popPort = 1110;
|
||||
smtpPort = 1025;
|
||||
};
|
||||
log4j = {
|
||||
logger.davmail = mkDefault "WARN";
|
||||
logger.httpclient.wire = mkDefault "WARN";
|
||||
logger.org.apache.commons.httpclient = mkDefault "WARN";
|
||||
rootLogger = mkDefault "WARN";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.davmail = {
|
||||
description = "DavMail POP/IMAP/SMTP Exchange Gateway";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = "${pkgs.davmail}/bin/davmail ${configFile}";
|
||||
Restart = "on-failure";
|
||||
DynamicUser = "yes";
|
||||
LogsDirectory = "davmail";
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [ pkgs.davmail ];
|
||||
};
|
||||
}
|
||||
120
nixos/modules/services/mail/dkimproxy-out.nix
Normal file
120
nixos/modules/services/mail/dkimproxy-out.nix
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
let
|
||||
cfg = config.services.dkimproxy-out;
|
||||
keydir = "/var/lib/dkimproxy-out";
|
||||
privkey = "${keydir}/private.key";
|
||||
pubkey = "${keydir}/public.key";
|
||||
in
|
||||
{
|
||||
##### interface
|
||||
options = {
|
||||
services.dkimproxy-out = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description =
|
||||
''
|
||||
Whether to enable dkimproxy_out.
|
||||
|
||||
Note that a key will be auto-generated, and can be found in
|
||||
${keydir}.
|
||||
'';
|
||||
};
|
||||
|
||||
listen = mkOption {
|
||||
type = types.str;
|
||||
example = "127.0.0.1:10027";
|
||||
description = "Address:port DKIMproxy should listen on.";
|
||||
};
|
||||
|
||||
relay = mkOption {
|
||||
type = types.str;
|
||||
example = "127.0.0.1:10028";
|
||||
description = "Address:port DKIMproxy should forward mail to.";
|
||||
};
|
||||
|
||||
domains = mkOption {
|
||||
type = with types; listOf str;
|
||||
example = [ "example.org" "example.com" ];
|
||||
description = "List of domains DKIMproxy can sign for.";
|
||||
};
|
||||
|
||||
selector = mkOption {
|
||||
type = types.str;
|
||||
example = "selector1";
|
||||
description =
|
||||
''
|
||||
The selector to use for DKIM key identification.
|
||||
|
||||
For example, if 'selector1' is used here, then for each domain
|
||||
'example.org' given in `domain`, 'selector1._domainkey.example.org'
|
||||
should contain the TXT record indicating the public key is the one
|
||||
in ${pubkey}: "v=DKIM1; t=s; p=[THE PUBLIC KEY]".
|
||||
'';
|
||||
};
|
||||
|
||||
keySize = mkOption {
|
||||
type = types.int;
|
||||
default = 2048;
|
||||
description =
|
||||
''
|
||||
Size of the RSA key to use to sign outgoing emails. Note that the
|
||||
maximum mandatorily verified as per RFC6376 is 2048.
|
||||
'';
|
||||
};
|
||||
|
||||
# TODO: allow signature for other schemes than dkim(c=relaxed/relaxed)?
|
||||
# This being the scheme used by gmail, maybe nothing more is needed for
|
||||
# reasonable use.
|
||||
};
|
||||
};
|
||||
|
||||
##### implementation
|
||||
config = let
|
||||
configfile = pkgs.writeText "dkimproxy_out.conf"
|
||||
''
|
||||
listen ${cfg.listen}
|
||||
relay ${cfg.relay}
|
||||
|
||||
domain ${concatStringsSep "," cfg.domains}
|
||||
selector ${cfg.selector}
|
||||
|
||||
signature dkim(c=relaxed/relaxed)
|
||||
|
||||
keyfile ${privkey}
|
||||
'';
|
||||
in
|
||||
mkIf cfg.enable {
|
||||
users.groups.dkimproxy-out = {};
|
||||
users.users.dkimproxy-out = {
|
||||
description = "DKIMproxy_out daemon";
|
||||
group = "dkimproxy-out";
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
systemd.services.dkimproxy-out = {
|
||||
description = "DKIMproxy_out";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
preStart = ''
|
||||
if [ ! -d "${keydir}" ]; then
|
||||
mkdir -p "${keydir}"
|
||||
chmod 0700 "${keydir}"
|
||||
${pkgs.openssl}/bin/openssl genrsa -out "${privkey}" ${toString cfg.keySize}
|
||||
${pkgs.openssl}/bin/openssl rsa -in "${privkey}" -pubout -out "${pubkey}"
|
||||
chown -R dkimproxy-out:dkimproxy-out "${keydir}"
|
||||
fi
|
||||
'';
|
||||
script = ''
|
||||
exec ${pkgs.dkimproxy}/bin/dkimproxy.out --conf_file=${configfile}
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = "dkimproxy-out";
|
||||
PermissionsStartOnly = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ ekleog ];
|
||||
}
|
||||
462
nixos/modules/services/mail/dovecot.nix
Normal file
462
nixos/modules/services/mail/dovecot.nix
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
{ options, config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.dovecot2;
|
||||
dovecotPkg = pkgs.dovecot;
|
||||
|
||||
baseDir = "/run/dovecot2";
|
||||
stateDir = "/var/lib/dovecot";
|
||||
|
||||
dovecotConf = concatStrings [
|
||||
''
|
||||
base_dir = ${baseDir}
|
||||
protocols = ${concatStringsSep " " cfg.protocols}
|
||||
sendmail_path = /run/wrappers/bin/sendmail
|
||||
# defining mail_plugins must be done before the first protocol {} filter because of https://doc.dovecot.org/configuration_manual/config_file/config_file_syntax/#variable-expansion
|
||||
mail_plugins = $mail_plugins ${concatStringsSep " " cfg.mailPlugins.globally.enable}
|
||||
''
|
||||
|
||||
(
|
||||
concatStringsSep "\n" (
|
||||
mapAttrsToList (
|
||||
protocol: plugins: ''
|
||||
protocol ${protocol} {
|
||||
mail_plugins = $mail_plugins ${concatStringsSep " " plugins.enable}
|
||||
}
|
||||
''
|
||||
) cfg.mailPlugins.perProtocol
|
||||
)
|
||||
)
|
||||
|
||||
(
|
||||
if cfg.sslServerCert == null then ''
|
||||
ssl = no
|
||||
disable_plaintext_auth = no
|
||||
'' else ''
|
||||
ssl_cert = <${cfg.sslServerCert}
|
||||
ssl_key = <${cfg.sslServerKey}
|
||||
${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)}
|
||||
${optionalString cfg.enableDHE ''ssl_dh = <${config.security.dhparams.params.dovecot2.path}''}
|
||||
disable_plaintext_auth = yes
|
||||
''
|
||||
)
|
||||
|
||||
''
|
||||
default_internal_user = ${cfg.user}
|
||||
default_internal_group = ${cfg.group}
|
||||
${optionalString (cfg.mailUser != null) "mail_uid = ${cfg.mailUser}"}
|
||||
${optionalString (cfg.mailGroup != null) "mail_gid = ${cfg.mailGroup}"}
|
||||
|
||||
mail_location = ${cfg.mailLocation}
|
||||
|
||||
maildir_copy_with_hardlinks = yes
|
||||
pop3_uidl_format = %08Xv%08Xu
|
||||
|
||||
auth_mechanisms = plain login
|
||||
|
||||
service auth {
|
||||
user = root
|
||||
}
|
||||
''
|
||||
|
||||
(
|
||||
optionalString cfg.enablePAM ''
|
||||
userdb {
|
||||
driver = passwd
|
||||
}
|
||||
|
||||
passdb {
|
||||
driver = pam
|
||||
args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
|
||||
}
|
||||
''
|
||||
)
|
||||
|
||||
(
|
||||
optionalString (cfg.sieveScripts != {}) ''
|
||||
plugin {
|
||||
${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieveScripts)}
|
||||
}
|
||||
''
|
||||
)
|
||||
|
||||
(
|
||||
optionalString (cfg.mailboxes != {}) ''
|
||||
namespace inbox {
|
||||
inbox=yes
|
||||
${concatStringsSep "\n" (map mailboxConfig (attrValues cfg.mailboxes))}
|
||||
}
|
||||
''
|
||||
)
|
||||
|
||||
(
|
||||
optionalString cfg.enableQuota ''
|
||||
service quota-status {
|
||||
executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
|
||||
inet_listener {
|
||||
port = ${cfg.quotaPort}
|
||||
}
|
||||
client_limit = 1
|
||||
}
|
||||
|
||||
plugin {
|
||||
quota_rule = *:storage=${cfg.quotaGlobalPerUser}
|
||||
quota = count:User quota # per virtual mail user quota
|
||||
quota_status_success = DUNNO
|
||||
quota_status_nouser = DUNNO
|
||||
quota_status_overquota = "552 5.2.2 Mailbox is full"
|
||||
quota_grace = 10%%
|
||||
quota_vsizes = yes
|
||||
}
|
||||
''
|
||||
)
|
||||
|
||||
cfg.extraConfig
|
||||
];
|
||||
|
||||
modulesDir = pkgs.symlinkJoin {
|
||||
name = "dovecot-modules";
|
||||
paths = map (pkg: "${pkg}/lib/dovecot") ([ dovecotPkg ] ++ map (module: module.override { dovecot = dovecotPkg; }) cfg.modules);
|
||||
};
|
||||
|
||||
mailboxConfig = mailbox: ''
|
||||
mailbox "${mailbox.name}" {
|
||||
auto = ${toString mailbox.auto}
|
||||
'' + optionalString (mailbox.autoexpunge != null) ''
|
||||
autoexpunge = ${mailbox.autoexpunge}
|
||||
'' + optionalString (mailbox.specialUse != null) ''
|
||||
special_use = \${toString mailbox.specialUse}
|
||||
'' + "}";
|
||||
|
||||
mailboxes = { name, ... }: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.strMatching ''[^"]+'';
|
||||
example = "Spam";
|
||||
default = name;
|
||||
readOnly = true;
|
||||
description = "The name of the mailbox.";
|
||||
};
|
||||
auto = mkOption {
|
||||
type = types.enum [ "no" "create" "subscribe" ];
|
||||
default = "no";
|
||||
example = "subscribe";
|
||||
description = "Whether to automatically create or create and subscribe to the mailbox or not.";
|
||||
};
|
||||
specialUse = mkOption {
|
||||
type = types.nullOr (types.enum [ "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash" ]);
|
||||
default = null;
|
||||
example = "Junk";
|
||||
description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
|
||||
};
|
||||
autoexpunge = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "60d";
|
||||
description = ''
|
||||
To automatically remove all email from the mailbox which is older than the
|
||||
specified time.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
(mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
|
||||
];
|
||||
|
||||
options.services.dovecot2 = {
|
||||
enable = mkEnableOption "the dovecot 2.x POP3/IMAP server";
|
||||
|
||||
enablePop3 = mkEnableOption "starting the POP3 listener (when Dovecot is enabled).";
|
||||
|
||||
enableImap = mkEnableOption "starting the IMAP listener (when Dovecot is enabled)." // { default = true; };
|
||||
|
||||
enableLmtp = mkEnableOption "starting the LMTP listener (when Dovecot is enabled).";
|
||||
|
||||
protocols = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Additional listeners to start when Dovecot is enabled.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "dovecot2";
|
||||
description = "Dovecot user name.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "dovecot2";
|
||||
description = "Dovecot group name.";
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
example = "mail_debug = yes";
|
||||
description = "Additional entries to put verbatim into Dovecot's config file.";
|
||||
};
|
||||
|
||||
mailPlugins =
|
||||
let
|
||||
plugins = hint: types.submodule {
|
||||
options = {
|
||||
enable = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "mail plugins to enable as a list of strings to append to the ${hint} <literal>$mail_plugins</literal> configuration variable";
|
||||
};
|
||||
};
|
||||
};
|
||||
in
|
||||
mkOption {
|
||||
type = with types; submodule {
|
||||
options = {
|
||||
globally = mkOption {
|
||||
description = "Additional entries to add to the mail_plugins variable for all protocols";
|
||||
type = plugins "top-level";
|
||||
example = { enable = [ "virtual" ]; };
|
||||
default = { enable = []; };
|
||||
};
|
||||
perProtocol = mkOption {
|
||||
description = "Additional entries to add to the mail_plugins variable, per protocol";
|
||||
type = attrsOf (plugins "corresponding per-protocol");
|
||||
default = {};
|
||||
example = { imap = [ "imap_acl" ]; };
|
||||
};
|
||||
};
|
||||
};
|
||||
description = "Additional entries to add to the mail_plugins variable, globally and per protocol";
|
||||
example = {
|
||||
globally.enable = [ "acl" ];
|
||||
perProtocol.imap.enable = [ "imap_acl" ];
|
||||
};
|
||||
default = { globally.enable = []; perProtocol = {}; };
|
||||
};
|
||||
|
||||
configFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = "Config file used for the whole dovecot configuration.";
|
||||
apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf;
|
||||
};
|
||||
|
||||
mailLocation = mkOption {
|
||||
type = types.str;
|
||||
default = "maildir:/var/spool/mail/%u"; /* Same as inbox, as postfix */
|
||||
example = "maildir:~/mail:INBOX=/var/spool/mail/%u";
|
||||
description = ''
|
||||
Location that dovecot will use for mail folders. Dovecot mail_location option.
|
||||
'';
|
||||
};
|
||||
|
||||
mailUser = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Default user to store mail for virtual users.";
|
||||
};
|
||||
|
||||
mailGroup = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Default group to store mail for virtual users.";
|
||||
};
|
||||
|
||||
createMailUser = mkEnableOption ''automatically creating the user
|
||||
given in <option>services.dovecot.user</option> and the group
|
||||
given in <option>services.dovecot.group</option>.'' // { default = true; };
|
||||
|
||||
modules = mkOption {
|
||||
type = types.listOf types.package;
|
||||
default = [];
|
||||
example = literalExpression "[ pkgs.dovecot_pigeonhole ]";
|
||||
description = ''
|
||||
Symlinks the contents of lib/dovecot of every given package into
|
||||
/etc/dovecot/modules. This will make the given modules available
|
||||
if a dovecot package with the module_dir patch applied is being used.
|
||||
'';
|
||||
};
|
||||
|
||||
sslCACert = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Path to the server's CA certificate key.";
|
||||
};
|
||||
|
||||
sslServerCert = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Path to the server's public key.";
|
||||
};
|
||||
|
||||
sslServerKey = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Path to the server's private key.";
|
||||
};
|
||||
|
||||
enablePAM = mkEnableOption "creating a own Dovecot PAM service and configure PAM user logins." // { default = true; };
|
||||
|
||||
enableDHE = mkEnableOption "enable ssl_dh and generation of primes for the key exchange." // { default = true; };
|
||||
|
||||
sieveScripts = mkOption {
|
||||
type = types.attrsOf types.path;
|
||||
default = {};
|
||||
description = "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
|
||||
};
|
||||
|
||||
showPAMFailure = mkEnableOption "showing the PAM failure message on authentication error (useful for OTPW).";
|
||||
|
||||
mailboxes = mkOption {
|
||||
type = with types; coercedTo
|
||||
(listOf unspecified)
|
||||
(list: listToAttrs (map (entry: { name = entry.name; value = removeAttrs entry ["name"]; }) list))
|
||||
(attrsOf (submodule mailboxes));
|
||||
default = {};
|
||||
example = literalExpression ''
|
||||
{
|
||||
Spam = { specialUse = "Junk"; auto = "create"; };
|
||||
}
|
||||
'';
|
||||
description = "Configure mailboxes and auto create or subscribe them.";
|
||||
};
|
||||
|
||||
enableQuota = mkEnableOption "the dovecot quota service.";
|
||||
|
||||
quotaPort = mkOption {
|
||||
type = types.str;
|
||||
default = "12340";
|
||||
description = ''
|
||||
The Port the dovecot quota service binds to.
|
||||
If using postfix, add check_policy_service inet:localhost:12340 to your smtpd_recipient_restrictions in your postfix config.
|
||||
'';
|
||||
};
|
||||
quotaGlobalPerUser = mkOption {
|
||||
type = types.str;
|
||||
default = "100G";
|
||||
example = "10G";
|
||||
description = "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
|
||||
|
||||
security.dhparams = mkIf (cfg.sslServerCert != null && cfg.enableDHE) {
|
||||
enable = true;
|
||||
params.dovecot2 = {};
|
||||
};
|
||||
services.dovecot2.protocols =
|
||||
optional cfg.enableImap "imap"
|
||||
++ optional cfg.enablePop3 "pop3"
|
||||
++ optional cfg.enableLmtp "lmtp";
|
||||
|
||||
services.dovecot2.mailPlugins = mkIf cfg.enableQuota {
|
||||
globally.enable = [ "quota" ];
|
||||
perProtocol.imap.enable = [ "imap_quota" ];
|
||||
};
|
||||
|
||||
users.users = {
|
||||
dovenull =
|
||||
{
|
||||
uid = config.ids.uids.dovenull2;
|
||||
description = "Dovecot user for untrusted logins";
|
||||
group = "dovenull";
|
||||
};
|
||||
} // optionalAttrs (cfg.user == "dovecot2") {
|
||||
dovecot2 =
|
||||
{
|
||||
uid = config.ids.uids.dovecot2;
|
||||
description = "Dovecot user";
|
||||
group = cfg.group;
|
||||
};
|
||||
} // optionalAttrs (cfg.createMailUser && cfg.mailUser != null) {
|
||||
${cfg.mailUser} =
|
||||
{ description = "Virtual Mail User"; isSystemUser = true; } // optionalAttrs (cfg.mailGroup != null)
|
||||
{ group = cfg.mailGroup; };
|
||||
};
|
||||
|
||||
users.groups = {
|
||||
dovenull.gid = config.ids.gids.dovenull2;
|
||||
} // optionalAttrs (cfg.group == "dovecot2") {
|
||||
dovecot2.gid = config.ids.gids.dovecot2;
|
||||
} // optionalAttrs (cfg.createMailUser && cfg.mailGroup != null) {
|
||||
${cfg.mailGroup} = {};
|
||||
};
|
||||
|
||||
environment.etc."dovecot/modules".source = modulesDir;
|
||||
environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
|
||||
|
||||
systemd.services.dovecot2 = {
|
||||
description = "Dovecot IMAP/POP3 server";
|
||||
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
restartTriggers = [ cfg.configFile modulesDir ];
|
||||
|
||||
startLimitIntervalSec = 60; # 1 min
|
||||
serviceConfig = {
|
||||
Type = "notify";
|
||||
ExecStart = "${dovecotPkg}/sbin/dovecot -F";
|
||||
ExecReload = "${dovecotPkg}/sbin/doveadm reload";
|
||||
Restart = "on-failure";
|
||||
RestartSec = "1s";
|
||||
RuntimeDirectory = [ "dovecot2" ];
|
||||
};
|
||||
|
||||
# When copying sieve scripts preserve the original time stamp
|
||||
# (should be 0) so that the compiled sieve script is newer than
|
||||
# the source file and Dovecot won't try to compile it.
|
||||
preStart = ''
|
||||
rm -rf ${stateDir}/sieve
|
||||
'' + optionalString (cfg.sieveScripts != {}) ''
|
||||
mkdir -p ${stateDir}/sieve
|
||||
${concatStringsSep "\n" (
|
||||
mapAttrsToList (
|
||||
to: from: ''
|
||||
if [ -d '${from}' ]; then
|
||||
mkdir '${stateDir}/sieve/${to}'
|
||||
cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
|
||||
else
|
||||
cp -p '${from}' '${stateDir}/sieve/${to}'
|
||||
fi
|
||||
${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
|
||||
''
|
||||
) cfg.sieveScripts
|
||||
)}
|
||||
chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
|
||||
'';
|
||||
};
|
||||
|
||||
environment.systemPackages = [ dovecotPkg ];
|
||||
|
||||
warnings = mkIf (any isList options.services.dovecot2.mailboxes.definitions) [
|
||||
"Declaring `services.dovecot2.mailboxes' as a list is deprecated and will break eval in 21.05! See the release notes for more info for migration."
|
||||
];
|
||||
|
||||
assertions = [
|
||||
{
|
||||
assertion = (cfg.sslServerCert == null) == (cfg.sslServerKey == null)
|
||||
&& (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
|
||||
message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
|
||||
}
|
||||
{
|
||||
assertion = cfg.showPAMFailure -> cfg.enablePAM;
|
||||
message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
|
||||
}
|
||||
{
|
||||
assertion = cfg.sieveScripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
|
||||
message = "dovecot requires mailUser and mailGroup to be set when sieveScripts is set";
|
||||
}
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
150
nixos/modules/services/mail/dspam.nix
Normal file
150
nixos/modules/services/mail/dspam.nix
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.dspam;
|
||||
|
||||
dspam = pkgs.dspam;
|
||||
|
||||
defaultSock = "/run/dspam/dspam.sock";
|
||||
|
||||
cfgfile = pkgs.writeText "dspam.conf" ''
|
||||
Home /var/lib/dspam
|
||||
StorageDriver ${dspam}/lib/dspam/lib${cfg.storageDriver}_drv.so
|
||||
|
||||
Trust root
|
||||
Trust ${cfg.user}
|
||||
SystemLog on
|
||||
UserLog on
|
||||
|
||||
${optionalString (cfg.domainSocket != null) ''
|
||||
ServerDomainSocketPath "${cfg.domainSocket}"
|
||||
ClientHost "${cfg.domainSocket}"
|
||||
''}
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
|
||||
in {
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.dspam = {
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable the dspam spam filter.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "dspam";
|
||||
description = "User for the dspam daemon.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "dspam";
|
||||
description = "Group for the dspam daemon.";
|
||||
};
|
||||
|
||||
storageDriver = mkOption {
|
||||
type = types.str;
|
||||
default = "hash";
|
||||
description = "Storage driver backend to use for dspam.";
|
||||
};
|
||||
|
||||
domainSocket = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = defaultSock;
|
||||
description = "Path to local domain socket which is used for communication with the daemon. Set to null to disable UNIX socket.";
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = "Additional dspam configuration.";
|
||||
};
|
||||
|
||||
maintenanceInterval = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "If set, maintenance script will be run at specified (in systemd.timer format) interval";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable (mkMerge [
|
||||
{
|
||||
users.users = optionalAttrs (cfg.user == "dspam") {
|
||||
dspam = {
|
||||
group = cfg.group;
|
||||
uid = config.ids.uids.dspam;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups = optionalAttrs (cfg.group == "dspam") {
|
||||
dspam.gid = config.ids.gids.dspam;
|
||||
};
|
||||
|
||||
environment.systemPackages = [ dspam ];
|
||||
|
||||
environment.etc."dspam/dspam.conf".source = cfgfile;
|
||||
|
||||
systemd.services.dspam = {
|
||||
description = "dspam spam filtering daemon";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "postgresql.service" ];
|
||||
restartTriggers = [ cfgfile ];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${dspam}/bin/dspam --daemon --nofork";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
RuntimeDirectory = optional (cfg.domainSocket == defaultSock) "dspam";
|
||||
RuntimeDirectoryMode = optional (cfg.domainSocket == defaultSock) "0750";
|
||||
StateDirectory = "dspam";
|
||||
StateDirectoryMode = "0750";
|
||||
LogsDirectory = "dspam";
|
||||
LogsDirectoryMode = "0750";
|
||||
# DSPAM segfaults on just about every error
|
||||
Restart = "on-abort";
|
||||
RestartSec = "1s";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
(mkIf (cfg.maintenanceInterval != null) {
|
||||
systemd.timers.dspam-maintenance = {
|
||||
description = "Timer for dspam maintenance script";
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = cfg.maintenanceInterval;
|
||||
Unit = "dspam-maintenance.service";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.dspam-maintenance = {
|
||||
description = "dspam maintenance script";
|
||||
restartTriggers = [ cfgfile ];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${dspam}/bin/dspam_maintenance --verbose";
|
||||
Type = "oneshot";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
};
|
||||
};
|
||||
})
|
||||
]);
|
||||
}
|
||||
132
nixos/modules/services/mail/exim.nix
Normal file
132
nixos/modules/services/mail/exim.nix
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) literalExpression mkIf mkOption singleton types;
|
||||
inherit (pkgs) coreutils;
|
||||
cfg = config.services.exim;
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.exim = {
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable the Exim mail transfer agent.";
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Verbatim Exim configuration. This should not contain exim_user,
|
||||
exim_group, exim_path, or spool_directory.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "exim";
|
||||
description = ''
|
||||
User to use when no root privileges are required.
|
||||
In particular, this applies when receiving messages and when doing
|
||||
remote deliveries. (Local deliveries run as various non-root users,
|
||||
typically as the owner of a local mailbox.) Specifying this value
|
||||
as root is not supported.
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "exim";
|
||||
description = ''
|
||||
Group to use when no root privileges are required.
|
||||
'';
|
||||
};
|
||||
|
||||
spoolDir = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/spool/exim";
|
||||
description = ''
|
||||
Location of the spool directory of exim.
|
||||
'';
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.exim;
|
||||
defaultText = literalExpression "pkgs.exim";
|
||||
description = ''
|
||||
The Exim derivation to use.
|
||||
This can be used to enable features such as LDAP or PAM support.
|
||||
'';
|
||||
};
|
||||
|
||||
queueRunnerInterval = mkOption {
|
||||
type = types.str;
|
||||
default = "5m";
|
||||
description = ''
|
||||
How often to spawn a new queue runner.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
environment = {
|
||||
etc."exim.conf".text = ''
|
||||
exim_user = ${cfg.user}
|
||||
exim_group = ${cfg.group}
|
||||
exim_path = /run/wrappers/bin/exim
|
||||
spool_directory = ${cfg.spoolDir}
|
||||
${cfg.config}
|
||||
'';
|
||||
systemPackages = [ cfg.package ];
|
||||
};
|
||||
|
||||
users.users.${cfg.user} = {
|
||||
description = "Exim mail transfer agent user";
|
||||
uid = config.ids.uids.exim;
|
||||
group = cfg.group;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = {
|
||||
gid = config.ids.gids.exim;
|
||||
};
|
||||
|
||||
security.wrappers.exim =
|
||||
{ setuid = true;
|
||||
owner = "root";
|
||||
group = "root";
|
||||
source = "${cfg.package}/bin/exim";
|
||||
};
|
||||
|
||||
systemd.services.exim = {
|
||||
description = "Exim Mail Daemon";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
restartTriggers = [ config.environment.etc."exim.conf".source ];
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/exim -bdf -q${cfg.queueRunnerInterval}";
|
||||
ExecReload = "${coreutils}/bin/kill -HUP $MAINPID";
|
||||
};
|
||||
preStart = ''
|
||||
if ! test -d ${cfg.spoolDir}; then
|
||||
${coreutils}/bin/mkdir -p ${cfg.spoolDir}
|
||||
${coreutils}/bin/chown ${cfg.user}:${cfg.group} ${cfg.spoolDir}
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
273
nixos/modules/services/mail/maddy.nix
Normal file
273
nixos/modules/services/mail/maddy.nix
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
name = "maddy";
|
||||
|
||||
cfg = config.services.maddy;
|
||||
|
||||
defaultConfig = ''
|
||||
# Minimal configuration with TLS disabled, adapted from upstream example
|
||||
# configuration here https://github.com/foxcpp/maddy/blob/master/maddy.conf
|
||||
# Do not use this in production!
|
||||
|
||||
tls off
|
||||
|
||||
auth.pass_table local_authdb {
|
||||
table sql_table {
|
||||
driver sqlite3
|
||||
dsn credentials.db
|
||||
table_name passwords
|
||||
}
|
||||
}
|
||||
|
||||
storage.imapsql local_mailboxes {
|
||||
driver sqlite3
|
||||
dsn imapsql.db
|
||||
}
|
||||
|
||||
table.chain local_rewrites {
|
||||
optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3"
|
||||
optional_step static {
|
||||
entry postmaster postmaster@$(primary_domain)
|
||||
}
|
||||
optional_step file /etc/maddy/aliases
|
||||
}
|
||||
msgpipeline local_routing {
|
||||
destination postmaster $(local_domains) {
|
||||
modify {
|
||||
replace_rcpt &local_rewrites
|
||||
}
|
||||
deliver_to &local_mailboxes
|
||||
}
|
||||
default_destination {
|
||||
reject 550 5.1.1 "User doesn't exist"
|
||||
}
|
||||
}
|
||||
|
||||
smtp tcp://0.0.0.0:25 {
|
||||
limits {
|
||||
all rate 20 1s
|
||||
all concurrency 10
|
||||
}
|
||||
dmarc yes
|
||||
check {
|
||||
require_mx_record
|
||||
dkim
|
||||
spf
|
||||
}
|
||||
source $(local_domains) {
|
||||
reject 501 5.1.8 "Use Submission for outgoing SMTP"
|
||||
}
|
||||
default_source {
|
||||
destination postmaster $(local_domains) {
|
||||
deliver_to &local_routing
|
||||
}
|
||||
default_destination {
|
||||
reject 550 5.1.1 "User doesn't exist"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
submission tcp://0.0.0.0:587 {
|
||||
limits {
|
||||
all rate 50 1s
|
||||
}
|
||||
auth &local_authdb
|
||||
source $(local_domains) {
|
||||
check {
|
||||
authorize_sender {
|
||||
prepare_email &local_rewrites
|
||||
user_to_email identity
|
||||
}
|
||||
}
|
||||
destination postmaster $(local_domains) {
|
||||
deliver_to &local_routing
|
||||
}
|
||||
default_destination {
|
||||
modify {
|
||||
dkim $(primary_domain) $(local_domains) default
|
||||
}
|
||||
deliver_to &remote_queue
|
||||
}
|
||||
}
|
||||
default_source {
|
||||
reject 501 5.1.8 "Non-local sender domain"
|
||||
}
|
||||
}
|
||||
|
||||
target.remote outbound_delivery {
|
||||
limits {
|
||||
destination rate 20 1s
|
||||
destination concurrency 10
|
||||
}
|
||||
mx_auth {
|
||||
dane
|
||||
mtasts {
|
||||
cache fs
|
||||
fs_dir mtasts_cache/
|
||||
}
|
||||
local_policy {
|
||||
min_tls_level encrypted
|
||||
min_mx_level none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target.queue remote_queue {
|
||||
target &outbound_delivery
|
||||
autogenerated_msg_domain $(primary_domain)
|
||||
bounce {
|
||||
destination postmaster $(local_domains) {
|
||||
deliver_to &local_routing
|
||||
}
|
||||
default_destination {
|
||||
reject 550 5.0.0 "Refusing to send DSNs to non-local addresses"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
imap tcp://0.0.0.0:143 {
|
||||
auth &local_authdb
|
||||
storage &local_mailboxes
|
||||
}
|
||||
'';
|
||||
|
||||
in {
|
||||
options = {
|
||||
services.maddy = {
|
||||
|
||||
enable = mkEnableOption "Maddy, a free an open source mail server";
|
||||
|
||||
user = mkOption {
|
||||
default = "maddy";
|
||||
type = with types; uniq string;
|
||||
description = ''
|
||||
User account under which maddy runs.
|
||||
|
||||
<note><para>
|
||||
If left as the default value this user will automatically be created
|
||||
on system activation, otherwise the sysadmin is responsible for
|
||||
ensuring the user exists before the maddy service starts.
|
||||
</para></note>
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
default = "maddy";
|
||||
type = with types; uniq string;
|
||||
description = ''
|
||||
Group account under which maddy runs.
|
||||
|
||||
<note><para>
|
||||
If left as the default value this group will automatically be created
|
||||
on system activation, otherwise the sysadmin is responsible for
|
||||
ensuring the group exists before the maddy service starts.
|
||||
</para></note>
|
||||
'';
|
||||
};
|
||||
|
||||
hostname = mkOption {
|
||||
default = "localhost";
|
||||
type = with types; uniq string;
|
||||
example = ''example.com'';
|
||||
description = ''
|
||||
Hostname to use. It should be FQDN.
|
||||
'';
|
||||
};
|
||||
|
||||
primaryDomain = mkOption {
|
||||
default = "localhost";
|
||||
type = with types; uniq string;
|
||||
example = ''mail.example.com'';
|
||||
description = ''
|
||||
Primary MX domain to use. It should be FQDN.
|
||||
'';
|
||||
};
|
||||
|
||||
localDomains = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = ["$(primary_domain)"];
|
||||
example = [
|
||||
"$(primary_domain)"
|
||||
"example.com"
|
||||
"other.example.com"
|
||||
];
|
||||
description = ''
|
||||
Define list of allowed domains.
|
||||
'';
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = with types; nullOr lines;
|
||||
default = defaultConfig;
|
||||
description = ''
|
||||
Server configuration, see
|
||||
<link xlink:href="https://maddy.email">https://maddy.email</link> for
|
||||
more information. The default configuration of this module will setup
|
||||
minimal maddy instance for mail transfer without TLS encryption.
|
||||
<note><para>
|
||||
This should not be used in a production environment.
|
||||
</para></note>
|
||||
'';
|
||||
};
|
||||
|
||||
openFirewall = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Open the configured incoming and outgoing mail server ports.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
systemd = {
|
||||
packages = [ pkgs.maddy ];
|
||||
services.maddy = {
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
StateDirectory = [ "maddy" ];
|
||||
};
|
||||
restartTriggers = [ config.environment.etc."maddy/maddy.conf".source ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
};
|
||||
};
|
||||
|
||||
environment.etc."maddy/maddy.conf" = {
|
||||
text = ''
|
||||
$(hostname) = ${cfg.hostname}
|
||||
$(primary_domain) = ${cfg.primaryDomain}
|
||||
$(local_domains) = ${toString cfg.localDomains}
|
||||
hostname ${cfg.hostname}
|
||||
${cfg.config}
|
||||
'';
|
||||
};
|
||||
|
||||
users.users = optionalAttrs (cfg.user == name) {
|
||||
${name} = {
|
||||
isSystemUser = true;
|
||||
group = cfg.group;
|
||||
description = "Maddy mail transfer agent user";
|
||||
};
|
||||
};
|
||||
|
||||
users.groups = optionalAttrs (cfg.group == name) {
|
||||
${cfg.group} = { };
|
||||
};
|
||||
|
||||
networking.firewall = mkIf cfg.openFirewall {
|
||||
allowedTCPPorts = [ 25 143 587 ];
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
pkgs.maddy
|
||||
];
|
||||
};
|
||||
}
|
||||
34
nixos/modules/services/mail/mail.nix
Normal file
34
nixos/modules/services/mail/mail.nix
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{ config, options, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
{
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.mail = {
|
||||
|
||||
sendmailSetuidWrapper = mkOption {
|
||||
type = types.nullOr options.security.wrappers.type.nestedTypes.elemType;
|
||||
default = null;
|
||||
internal = true;
|
||||
description = ''
|
||||
Configuration for the sendmail setuid wapper.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf (config.services.mail.sendmailSetuidWrapper != null) {
|
||||
|
||||
security.wrappers.sendmail = config.services.mail.sendmailSetuidWrapper;
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
68
nixos/modules/services/mail/mailcatcher.nix
Normal file
68
nixos/modules/services/mail/mailcatcher.nix
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.mailcatcher;
|
||||
|
||||
inherit (lib) mkEnableOption mkIf mkOption types optionalString;
|
||||
in
|
||||
{
|
||||
# interface
|
||||
|
||||
options = {
|
||||
|
||||
services.mailcatcher = {
|
||||
enable = mkEnableOption "MailCatcher";
|
||||
|
||||
http.ip = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "The ip address of the http server.";
|
||||
};
|
||||
|
||||
http.port = mkOption {
|
||||
type = types.port;
|
||||
default = 1080;
|
||||
description = "The port address of the http server.";
|
||||
};
|
||||
|
||||
http.path = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
description = "Prefix to all HTTP paths.";
|
||||
example = "/mailcatcher";
|
||||
};
|
||||
|
||||
smtp.ip = mkOption {
|
||||
type = types.str;
|
||||
default = "127.0.0.1";
|
||||
description = "The ip address of the smtp server.";
|
||||
};
|
||||
|
||||
smtp.port = mkOption {
|
||||
type = types.port;
|
||||
default = 1025;
|
||||
description = "The port address of the smtp server.";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
# implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
environment.systemPackages = [ pkgs.mailcatcher ];
|
||||
|
||||
systemd.services.mailcatcher = {
|
||||
description = "MailCatcher Service";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
DynamicUser = true;
|
||||
Restart = "always";
|
||||
ExecStart = "${pkgs.mailcatcher}/bin/mailcatcher --foreground --no-quit --http-ip ${cfg.http.ip} --http-port ${toString cfg.http.port} --smtp-ip ${cfg.smtp.ip} --smtp-port ${toString cfg.smtp.port}" + optionalString (cfg.http.path != null) " --http-path ${cfg.http.path}";
|
||||
AmbientCapabilities = optionalString (cfg.http.port < 1024 || cfg.smtp.port < 1024) "cap_net_bind_service";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
82
nixos/modules/services/mail/mailhog.nix
Normal file
82
nixos/modules/services/mail/mailhog.nix
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.mailhog;
|
||||
|
||||
args = lib.concatStringsSep " " (
|
||||
[
|
||||
"-api-bind-addr :${toString cfg.apiPort}"
|
||||
"-smtp-bind-addr :${toString cfg.smtpPort}"
|
||||
"-ui-bind-addr :${toString cfg.uiPort}"
|
||||
"-storage ${cfg.storage}"
|
||||
] ++ lib.optional (cfg.storage == "maildir")
|
||||
"-maildir-path $STATE_DIRECTORY"
|
||||
++ cfg.extraArgs
|
||||
);
|
||||
|
||||
in
|
||||
{
|
||||
###### interface
|
||||
|
||||
imports = [
|
||||
(mkRemovedOptionModule [ "services" "mailhog" "user" ] "")
|
||||
];
|
||||
|
||||
options = {
|
||||
|
||||
services.mailhog = {
|
||||
enable = mkEnableOption "MailHog";
|
||||
|
||||
storage = mkOption {
|
||||
type = types.enum [ "maildir" "memory" ];
|
||||
default = "memory";
|
||||
description = "Store mails on disk or in memory.";
|
||||
};
|
||||
|
||||
apiPort = mkOption {
|
||||
type = types.port;
|
||||
default = 8025;
|
||||
description = "Port on which the API endpoint will listen.";
|
||||
};
|
||||
|
||||
smtpPort = mkOption {
|
||||
type = types.port;
|
||||
default = 1025;
|
||||
description = "Port on which the SMTP endpoint will listen.";
|
||||
};
|
||||
|
||||
uiPort = mkOption {
|
||||
type = types.port;
|
||||
default = 8025;
|
||||
description = "Port on which the HTTP UI will listen.";
|
||||
};
|
||||
|
||||
extraArgs = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "List of additional arguments to pass to the MailHog process.";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
systemd.services.mailhog = {
|
||||
description = "MailHog - Web and API based SMTP testing";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
Type = "exec";
|
||||
ExecStart = "${pkgs.mailhog}/bin/MailHog ${args}";
|
||||
DynamicUser = true;
|
||||
Restart = "on-failure";
|
||||
StateDirectory = "mailhog";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
473
nixos/modules/services/mail/mailman.nix
Normal file
473
nixos/modules/services/mail/mailman.nix
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
{ config, pkgs, lib, ... }: # mailman.nix
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.mailman;
|
||||
|
||||
inherit (pkgs.mailmanPackages.buildEnvs { withHyperkitty = cfg.hyperkitty.enable; })
|
||||
mailmanEnv webEnv;
|
||||
|
||||
withPostgresql = config.services.postgresql.enable;
|
||||
|
||||
# This deliberately doesn't use recursiveUpdate so users can
|
||||
# override the defaults.
|
||||
webSettings = {
|
||||
DEFAULT_FROM_EMAIL = cfg.siteOwner;
|
||||
SERVER_EMAIL = cfg.siteOwner;
|
||||
ALLOWED_HOSTS = [ "localhost" "127.0.0.1" ] ++ cfg.webHosts;
|
||||
COMPRESS_OFFLINE = true;
|
||||
STATIC_ROOT = "/var/lib/mailman-web-static";
|
||||
MEDIA_ROOT = "/var/lib/mailman-web/media";
|
||||
LOGGING = {
|
||||
version = 1;
|
||||
disable_existing_loggers = true;
|
||||
handlers.console.class = "logging.StreamHandler";
|
||||
loggers.django = {
|
||||
handlers = [ "console" ];
|
||||
level = "INFO";
|
||||
};
|
||||
};
|
||||
HAYSTACK_CONNECTIONS.default = {
|
||||
ENGINE = "haystack.backends.whoosh_backend.WhooshEngine";
|
||||
PATH = "/var/lib/mailman-web/fulltext-index";
|
||||
};
|
||||
} // cfg.webSettings;
|
||||
|
||||
webSettingsJSON = pkgs.writeText "settings.json" (builtins.toJSON webSettings);
|
||||
|
||||
# TODO: Should this be RFC42-ised so that users can set additional options without modifying the module?
|
||||
postfixMtaConfig = pkgs.writeText "mailman-postfix.cfg" ''
|
||||
[postfix]
|
||||
postmap_command: ${pkgs.postfix}/bin/postmap
|
||||
transport_file_type: hash
|
||||
'';
|
||||
|
||||
mailmanCfg = lib.generators.toINI {} cfg.settings;
|
||||
|
||||
mailmanHyperkittyCfg = pkgs.writeText "mailman-hyperkitty.cfg" ''
|
||||
[general]
|
||||
# This is your HyperKitty installation, preferably on the localhost. This
|
||||
# address will be used by Mailman to forward incoming emails to HyperKitty
|
||||
# for archiving. It does not need to be publicly available, in fact it's
|
||||
# better if it is not.
|
||||
base_url: ${cfg.hyperkitty.baseUrl}
|
||||
|
||||
# Shared API key, must be the identical to the value in HyperKitty's
|
||||
# settings.
|
||||
api_key: @API_KEY@
|
||||
'';
|
||||
|
||||
in {
|
||||
|
||||
###### interface
|
||||
|
||||
imports = [
|
||||
(mkRenamedOptionModule [ "services" "mailman" "hyperkittyBaseUrl" ]
|
||||
[ "services" "mailman" "hyperkitty" "baseUrl" ])
|
||||
|
||||
(mkRemovedOptionModule [ "services" "mailman" "hyperkittyApiKey" ] ''
|
||||
The Hyperkitty API key is now generated on first run, and not
|
||||
stored in the world-readable Nix store. To continue using
|
||||
Hyperkitty, you must set services.mailman.hyperkitty.enable = true.
|
||||
'')
|
||||
(mkRemovedOptionModule [ "services" "mailman" "package" ] ''
|
||||
Didn't have an effect for several years.
|
||||
'')
|
||||
];
|
||||
|
||||
options = {
|
||||
|
||||
services.mailman = {
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable Mailman on this host. Requires an active MTA on the host (e.g. Postfix).";
|
||||
};
|
||||
|
||||
enablePostfix = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
example = false;
|
||||
description = ''
|
||||
Enable Postfix integration. Requires an active Postfix installation.
|
||||
|
||||
If you want to use another MTA, set this option to false and configure
|
||||
settings in services.mailman.settings.mta.
|
||||
|
||||
Refer to the Mailman manual for more info.
|
||||
'';
|
||||
};
|
||||
|
||||
siteOwner = mkOption {
|
||||
type = types.str;
|
||||
example = "postmaster@example.org";
|
||||
description = ''
|
||||
Certain messages that must be delivered to a human, but which can't
|
||||
be delivered to a list owner (e.g. a bounce from a list owner), will
|
||||
be sent to this address. It should point to a human.
|
||||
'';
|
||||
};
|
||||
|
||||
webHosts = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = ''
|
||||
The list of hostnames and/or IP addresses from which the Mailman Web
|
||||
UI will accept requests. By default, "localhost" and "127.0.0.1" are
|
||||
enabled. All additional names under which your web server accepts
|
||||
requests for the UI must be listed here or incoming requests will be
|
||||
rejected.
|
||||
'';
|
||||
};
|
||||
|
||||
webUser = mkOption {
|
||||
type = types.str;
|
||||
default = "mailman-web";
|
||||
description = ''
|
||||
User to run mailman-web as
|
||||
'';
|
||||
};
|
||||
|
||||
webSettings = mkOption {
|
||||
type = types.attrs;
|
||||
default = {};
|
||||
description = ''
|
||||
Overrides for the default mailman-web Django settings.
|
||||
'';
|
||||
};
|
||||
|
||||
serve = {
|
||||
enable = mkEnableOption "Automatic nginx and uwsgi setup for mailman-web";
|
||||
};
|
||||
|
||||
extraPythonPackages = mkOption {
|
||||
description = "Packages to add to the python environment used by mailman and mailman-web";
|
||||
type = types.listOf types.package;
|
||||
default = [];
|
||||
};
|
||||
|
||||
settings = mkOption {
|
||||
description = "Settings for mailman.cfg";
|
||||
type = types.attrsOf (types.attrsOf types.str);
|
||||
default = {};
|
||||
};
|
||||
|
||||
hyperkitty = {
|
||||
enable = mkEnableOption "the Hyperkitty archiver for Mailman";
|
||||
|
||||
baseUrl = mkOption {
|
||||
type = types.str;
|
||||
default = "http://localhost:18507/archives/";
|
||||
description = ''
|
||||
Where can Mailman connect to Hyperkitty's internal API, preferably on
|
||||
localhost?
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
services.mailman.settings = {
|
||||
mailman.site_owner = lib.mkDefault cfg.siteOwner;
|
||||
mailman.layout = "fhs";
|
||||
|
||||
"paths.fhs" = {
|
||||
bin_dir = "${pkgs.mailmanPackages.mailman}/bin";
|
||||
var_dir = "/var/lib/mailman";
|
||||
queue_dir = "$var_dir/queue";
|
||||
template_dir = "$var_dir/templates";
|
||||
log_dir = "/var/log/mailman";
|
||||
lock_dir = "$var_dir/lock";
|
||||
etc_dir = "/etc";
|
||||
pid_file = "/run/mailman/master.pid";
|
||||
};
|
||||
|
||||
mta.configuration = lib.mkDefault (if cfg.enablePostfix then "${postfixMtaConfig}" else throw "When Mailman Postfix integration is disabled, set `services.mailman.settings.mta.configuration` to the path of the config file required to integrate with your MTA.");
|
||||
|
||||
"archiver.hyperkitty" = lib.mkIf cfg.hyperkitty.enable {
|
||||
class = "mailman_hyperkitty.Archiver";
|
||||
enable = "yes";
|
||||
configuration = "/var/lib/mailman/mailman-hyperkitty.cfg";
|
||||
};
|
||||
} // (let
|
||||
loggerNames = ["root" "archiver" "bounce" "config" "database" "debug" "error" "fromusenet" "http" "locks" "mischief" "plugins" "runner" "smtp"];
|
||||
loggerSectionNames = map (n: "logging.${n}") loggerNames;
|
||||
in lib.genAttrs loggerSectionNames(name: { handler = "stderr"; })
|
||||
);
|
||||
|
||||
assertions = let
|
||||
inherit (config.services) postfix;
|
||||
|
||||
requirePostfixHash = optionPath: dataFile:
|
||||
with lib;
|
||||
let
|
||||
expected = "hash:/var/lib/mailman/data/${dataFile}";
|
||||
value = attrByPath optionPath [] postfix;
|
||||
in
|
||||
{ assertion = postfix.enable -> isList value && elem expected value;
|
||||
message = ''
|
||||
services.postfix.${concatStringsSep "." optionPath} must contain
|
||||
"${expected}".
|
||||
See <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>.
|
||||
'';
|
||||
};
|
||||
in [
|
||||
{ assertion = cfg.webHosts != [];
|
||||
message = ''
|
||||
services.mailman.serve.enable requires there to be at least one entry
|
||||
in services.mailman.webHosts.
|
||||
'';
|
||||
}
|
||||
] ++ (lib.optionals cfg.enablePostfix [
|
||||
{ assertion = postfix.enable;
|
||||
message = ''
|
||||
Mailman's default NixOS configuration requires Postfix to be enabled.
|
||||
|
||||
If you want to use another MTA, set services.mailman.enablePostfix
|
||||
to false and configure settings in services.mailman.settings.mta.
|
||||
|
||||
Refer to <https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html>
|
||||
for more info.
|
||||
'';
|
||||
}
|
||||
(requirePostfixHash [ "relayDomains" ] "postfix_domains")
|
||||
(requirePostfixHash [ "config" "transport_maps" ] "postfix_lmtp")
|
||||
(requirePostfixHash [ "config" "local_recipient_maps" ] "postfix_lmtp")
|
||||
]);
|
||||
|
||||
users.users.mailman = {
|
||||
description = "GNU Mailman";
|
||||
isSystemUser = true;
|
||||
group = "mailman";
|
||||
};
|
||||
users.users.mailman-web = lib.mkIf (cfg.webUser == "mailman-web") {
|
||||
description = "GNU Mailman web interface";
|
||||
isSystemUser = true;
|
||||
group = "mailman";
|
||||
};
|
||||
users.groups.mailman = {};
|
||||
|
||||
environment.etc."mailman.cfg".text = mailmanCfg;
|
||||
|
||||
environment.etc."mailman3/settings.py".text = ''
|
||||
import os
|
||||
|
||||
# Required by mailman_web.settings, but will be overridden when
|
||||
# settings_local.json is loaded.
|
||||
os.environ["SECRET_KEY"] = ""
|
||||
|
||||
from mailman_web.settings.base import *
|
||||
from mailman_web.settings.mailman import *
|
||||
|
||||
import json
|
||||
|
||||
with open('${webSettingsJSON}') as f:
|
||||
globals().update(json.load(f))
|
||||
|
||||
with open('/var/lib/mailman-web/settings_local.json') as f:
|
||||
globals().update(json.load(f))
|
||||
'';
|
||||
|
||||
services.nginx = mkIf (cfg.serve.enable && cfg.webHosts != []) {
|
||||
enable = mkDefault true;
|
||||
virtualHosts = lib.genAttrs cfg.webHosts (webHost: {
|
||||
locations = {
|
||||
"/".extraConfig = "uwsgi_pass unix:/run/mailman-web.socket;";
|
||||
"/static/".alias = webSettings.STATIC_ROOT + "/";
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
environment.systemPackages = [ (pkgs.buildEnv {
|
||||
name = "mailman-tools";
|
||||
# We don't want to pollute the system PATH with a python
|
||||
# interpreter etc. so let's pick only the stuff we actually
|
||||
# want from {web,mailman}Env
|
||||
pathsToLink = ["/bin"];
|
||||
paths = [ mailmanEnv webEnv ];
|
||||
# Only mailman-related stuff is installed, the rest is removed
|
||||
# in `postBuild`.
|
||||
ignoreCollisions = true;
|
||||
postBuild = ''
|
||||
find $out/bin/ -mindepth 1 -not -name "mailman*" -delete
|
||||
'';
|
||||
}) ];
|
||||
|
||||
services.postfix = lib.mkIf cfg.enablePostfix {
|
||||
recipientDelimiter = "+"; # bake recipient addresses in mail envelopes via VERP
|
||||
config = {
|
||||
owner_request_special = "no"; # Mailman handles -owner addresses on its own
|
||||
};
|
||||
};
|
||||
|
||||
systemd.sockets.mailman-uwsgi = lib.mkIf cfg.serve.enable {
|
||||
wantedBy = ["sockets.target"];
|
||||
before = ["nginx.service"];
|
||||
socketConfig.ListenStream = "/run/mailman-web.socket";
|
||||
};
|
||||
systemd.services = {
|
||||
mailman = {
|
||||
description = "GNU Mailman Master Process";
|
||||
before = lib.optional cfg.enablePostfix "postfix.service";
|
||||
after = [ "network.target" ]
|
||||
++ lib.optional cfg.enablePostfix "postfix-setup.service"
|
||||
++ lib.optional withPostgresql "postgresql.service";
|
||||
restartTriggers = [ config.environment.etc."mailman.cfg".source ];
|
||||
requires = optional withPostgresql "postgresql.service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = "${mailmanEnv}/bin/mailman start";
|
||||
ExecStop = "${mailmanEnv}/bin/mailman stop";
|
||||
User = "mailman";
|
||||
Group = "mailman";
|
||||
Type = "forking";
|
||||
RuntimeDirectory = "mailman";
|
||||
LogsDirectory = "mailman";
|
||||
PIDFile = "/run/mailman/master.pid";
|
||||
};
|
||||
};
|
||||
|
||||
mailman-settings = {
|
||||
description = "Generate settings files (including secrets) for Mailman";
|
||||
before = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
|
||||
requiredBy = [ "mailman.service" "mailman-web-setup.service" "mailman-uwsgi.service" "hyperkitty.service" ];
|
||||
path = with pkgs; [ jq ];
|
||||
after = optional withPostgresql "postgresql.service";
|
||||
requires = optional withPostgresql "postgresql.service";
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = ''
|
||||
mailmanDir=/var/lib/mailman
|
||||
mailmanWebDir=/var/lib/mailman-web
|
||||
|
||||
mailmanCfg=$mailmanDir/mailman-hyperkitty.cfg
|
||||
mailmanWebCfg=$mailmanWebDir/settings_local.json
|
||||
|
||||
install -m 0775 -o mailman -g mailman -d /var/lib/mailman-web-static
|
||||
install -m 0770 -o mailman -g mailman -d $mailmanDir
|
||||
install -m 0770 -o ${cfg.webUser} -g mailman -d $mailmanWebDir
|
||||
|
||||
if [ ! -e $mailmanWebCfg ]; then
|
||||
hyperkittyApiKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
|
||||
secretKey=$(tr -dc A-Za-z0-9 < /dev/urandom | head -c 64)
|
||||
|
||||
mailmanWebCfgTmp=$(mktemp)
|
||||
jq -n '.MAILMAN_ARCHIVER_KEY=$archiver_key | .SECRET_KEY=$secret_key' \
|
||||
--arg archiver_key "$hyperkittyApiKey" \
|
||||
--arg secret_key "$secretKey" \
|
||||
>"$mailmanWebCfgTmp"
|
||||
chown root:mailman "$mailmanWebCfgTmp"
|
||||
chmod 440 "$mailmanWebCfgTmp"
|
||||
mv -n "$mailmanWebCfgTmp" "$mailmanWebCfg"
|
||||
fi
|
||||
|
||||
hyperkittyApiKey="$(jq -r .MAILMAN_ARCHIVER_KEY "$mailmanWebCfg")"
|
||||
mailmanCfgTmp=$(mktemp)
|
||||
sed "s/@API_KEY@/$hyperkittyApiKey/g" ${mailmanHyperkittyCfg} >"$mailmanCfgTmp"
|
||||
chown mailman:mailman "$mailmanCfgTmp"
|
||||
mv "$mailmanCfgTmp" "$mailmanCfg"
|
||||
'';
|
||||
};
|
||||
|
||||
mailman-web-setup = {
|
||||
description = "Prepare mailman-web files and database";
|
||||
before = [ "mailman-uwsgi.service" ];
|
||||
requiredBy = [ "mailman-uwsgi.service" ];
|
||||
restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
|
||||
script = ''
|
||||
[[ -e "${webSettings.STATIC_ROOT}" ]] && find "${webSettings.STATIC_ROOT}/" -mindepth 1 -delete
|
||||
${webEnv}/bin/mailman-web migrate
|
||||
${webEnv}/bin/mailman-web collectstatic
|
||||
${webEnv}/bin/mailman-web compress
|
||||
'';
|
||||
serviceConfig = {
|
||||
User = cfg.webUser;
|
||||
Group = "mailman";
|
||||
Type = "oneshot";
|
||||
WorkingDirectory = "/var/lib/mailman-web";
|
||||
};
|
||||
};
|
||||
|
||||
mailman-uwsgi = mkIf cfg.serve.enable (let
|
||||
uwsgiConfig.uwsgi = {
|
||||
type = "normal";
|
||||
plugins = ["python3"];
|
||||
home = webEnv;
|
||||
module = "mailman_web.wsgi";
|
||||
http = "127.0.0.1:18507";
|
||||
};
|
||||
uwsgiConfigFile = pkgs.writeText "uwsgi-mailman.json" (builtins.toJSON uwsgiConfig);
|
||||
in {
|
||||
wantedBy = ["multi-user.target"];
|
||||
after = optional withPostgresql "postgresql.service";
|
||||
requires = ["mailman-uwsgi.socket" "mailman-web-setup.service"]
|
||||
++ optional withPostgresql "postgresql.service";
|
||||
restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
|
||||
serviceConfig = {
|
||||
# Since the mailman-web settings.py obstinately creates a logs
|
||||
# dir in the cwd, change to the (writable) runtime directory before
|
||||
# starting uwsgi.
|
||||
ExecStart = "${pkgs.coreutils}/bin/env -C $RUNTIME_DIRECTORY ${pkgs.uwsgi.override { plugins = ["python3"]; }}/bin/uwsgi --json ${uwsgiConfigFile}";
|
||||
User = cfg.webUser;
|
||||
Group = "mailman";
|
||||
RuntimeDirectory = "mailman-uwsgi";
|
||||
};
|
||||
});
|
||||
|
||||
mailman-daily = {
|
||||
description = "Trigger daily Mailman events";
|
||||
startAt = "daily";
|
||||
restartTriggers = [ config.environment.etc."mailman.cfg".source ];
|
||||
serviceConfig = {
|
||||
ExecStart = "${mailmanEnv}/bin/mailman digests --send";
|
||||
User = "mailman";
|
||||
Group = "mailman";
|
||||
};
|
||||
};
|
||||
|
||||
hyperkitty = lib.mkIf cfg.hyperkitty.enable {
|
||||
description = "GNU Hyperkitty QCluster Process";
|
||||
after = [ "network.target" ];
|
||||
restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
|
||||
wantedBy = [ "mailman.service" "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = "${webEnv}/bin/mailman-web qcluster";
|
||||
User = cfg.webUser;
|
||||
Group = "mailman";
|
||||
WorkingDirectory = "/var/lib/mailman-web";
|
||||
};
|
||||
};
|
||||
} // flip lib.mapAttrs' {
|
||||
"minutely" = "minutely";
|
||||
"quarter_hourly" = "*:00/15";
|
||||
"hourly" = "hourly";
|
||||
"daily" = "daily";
|
||||
"weekly" = "weekly";
|
||||
"yearly" = "yearly";
|
||||
} (name: startAt:
|
||||
lib.nameValuePair "hyperkitty-${name}" (lib.mkIf cfg.hyperkitty.enable {
|
||||
description = "Trigger ${name} Hyperkitty events";
|
||||
inherit startAt;
|
||||
restartTriggers = [ config.environment.etc."mailman3/settings.py".source ];
|
||||
serviceConfig = {
|
||||
ExecStart = "${webEnv}/bin/mailman-web runjobs ${name}";
|
||||
User = cfg.webUser;
|
||||
Group = "mailman";
|
||||
WorkingDirectory = "/var/lib/mailman-web";
|
||||
};
|
||||
}));
|
||||
};
|
||||
|
||||
meta = {
|
||||
maintainers = with lib.maintainers; [ lheckemann qyliss ma27 ];
|
||||
doc = ./mailman.xml;
|
||||
};
|
||||
|
||||
}
|
||||
94
nixos/modules/services/mail/mailman.xml
Normal file
94
nixos/modules/services/mail/mailman.xml
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
<chapter xmlns="http://docbook.org/ns/docbook"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:xi="http://www.w3.org/2001/XInclude"
|
||||
version="5.0"
|
||||
xml:id="module-services-mailman">
|
||||
<title>Mailman</title>
|
||||
<para>
|
||||
<link xlink:href="https://www.list.org">Mailman</link> is free
|
||||
software for managing electronic mail discussion and e-newsletter
|
||||
lists. Mailman and its web interface can be configured using the
|
||||
corresponding NixOS module. Note that this service is best used with
|
||||
an existing, securely configured Postfix setup, as it does not automatically configure this.
|
||||
</para>
|
||||
|
||||
<section xml:id="module-services-mailman-basic-usage">
|
||||
<title>Basic usage with Postfix</title>
|
||||
<para>
|
||||
For a basic configuration with Postfix as the MTA, the following settings are suggested:
|
||||
<programlisting>{ config, ... }: {
|
||||
services.postfix = {
|
||||
enable = true;
|
||||
relayDomains = ["hash:/var/lib/mailman/data/postfix_domains"];
|
||||
sslCert = config.security.acme.certs."lists.example.org".directory + "/full.pem";
|
||||
sslKey = config.security.acme.certs."lists.example.org".directory + "/key.pem";
|
||||
config = {
|
||||
transport_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
|
||||
local_recipient_maps = ["hash:/var/lib/mailman/data/postfix_lmtp"];
|
||||
};
|
||||
};
|
||||
services.mailman = {
|
||||
<link linkend="opt-services.mailman.enable">enable</link> = true;
|
||||
<link linkend="opt-services.mailman.serve.enable">serve.enable</link> = true;
|
||||
<link linkend="opt-services.mailman.hyperkitty.enable">hyperkitty.enable</link> = true;
|
||||
<link linkend="opt-services.mailman.webHosts">webHosts</link> = ["lists.example.org"];
|
||||
<link linkend="opt-services.mailman.siteOwner">siteOwner</link> = "mailman@example.org";
|
||||
};
|
||||
<link linkend="opt-services.nginx.virtualHosts._name_.enableACME">services.nginx.virtualHosts."lists.example.org".enableACME</link> = true;
|
||||
<link linkend="opt-networking.firewall.allowedTCPPorts">networking.firewall.allowedTCPPorts</link> = [ 25 80 443 ];
|
||||
}</programlisting>
|
||||
</para>
|
||||
<para>
|
||||
DNS records will also be required:
|
||||
<itemizedlist>
|
||||
<listitem><para><literal>AAAA</literal> and <literal>A</literal> records pointing to the host in question, in order for browsers to be able to discover the address of the web server;</para></listitem>
|
||||
<listitem><para>An <literal>MX</literal> record pointing to a domain name at which the host is reachable, in order for other mail servers to be able to deliver emails to the mailing lists it hosts.</para></listitem>
|
||||
</itemizedlist>
|
||||
</para>
|
||||
<para>
|
||||
After this has been done and appropriate DNS records have been
|
||||
set up, the Postorius mailing list manager and the Hyperkitty
|
||||
archive browser will be available at
|
||||
https://lists.example.org/. Note that this setup is not
|
||||
sufficient to deliver emails to most email providers nor to
|
||||
avoid spam -- a number of additional measures for authenticating
|
||||
incoming and outgoing mails, such as SPF, DMARC and DKIM are
|
||||
necessary, but outside the scope of the Mailman module.
|
||||
</para>
|
||||
</section>
|
||||
<section xml:id="module-services-mailman-other-mtas">
|
||||
<title>Using with other MTAs</title>
|
||||
<para>
|
||||
Mailman also supports other MTA, though with a little bit more configuration. For example, to use Mailman with Exim, you can use the following settings:
|
||||
<programlisting>{ config, ... }: {
|
||||
services = {
|
||||
mailman = {
|
||||
enable = true;
|
||||
siteOwner = "mailman@example.org";
|
||||
<link linkend="opt-services.mailman.enablePostfix">enablePostfix</link> = false;
|
||||
settings.mta = {
|
||||
incoming = "mailman.mta.exim4.LMTP";
|
||||
outgoing = "mailman.mta.deliver.deliver";
|
||||
lmtp_host = "localhost";
|
||||
lmtp_port = "8024";
|
||||
smtp_host = "localhost";
|
||||
smtp_port = "25";
|
||||
configuration = "python:mailman.config.exim4";
|
||||
};
|
||||
};
|
||||
exim = {
|
||||
enable = true;
|
||||
# You can configure Exim in a separate file to reduce configuration.nix clutter
|
||||
config = builtins.readFile ./exim.conf;
|
||||
};
|
||||
};
|
||||
}</programlisting>
|
||||
</para>
|
||||
<para>
|
||||
The exim config needs some special additions to work with Mailman. Currently
|
||||
NixOS can't manage Exim config with such granularity. Please refer to
|
||||
<link xlink:href="https://mailman.readthedocs.io/en/latest/src/mailman/docs/mta.html">Mailman documentation</link>
|
||||
for more info on configuring Mailman for working with Exim.
|
||||
</para>
|
||||
</section>
|
||||
</chapter>
|
||||
171
nixos/modules/services/mail/mlmmj.nix
Normal file
171
nixos/modules/services/mail/mlmmj.nix
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
concatMapLines = f: l: lib.concatStringsSep "\n" (map f l);
|
||||
|
||||
cfg = config.services.mlmmj;
|
||||
stateDir = "/var/lib/mlmmj";
|
||||
spoolDir = "/var/spool/mlmmj";
|
||||
listDir = domain: list: "${spoolDir}/${domain}/${list}";
|
||||
listCtl = domain: list: "${listDir domain list}/control";
|
||||
transport = domain: list: "${domain}--${list}@local.list.mlmmj mlmmj:${domain}/${list}";
|
||||
virtual = domain: list: "${list}@${domain} ${domain}--${list}@local.list.mlmmj";
|
||||
alias = domain: list: "${list}: \"|${pkgs.mlmmj}/bin/mlmmj-receive -L ${listDir domain list}/\"";
|
||||
subjectPrefix = list: "[${list}]";
|
||||
listAddress = domain: list: "${list}@${domain}";
|
||||
customHeaders = domain: list: [
|
||||
"List-Id: ${list}"
|
||||
"Reply-To: ${list}@${domain}"
|
||||
"List-Post: <mailto:${list}@${domain}>"
|
||||
"List-Help: <mailto:${list}+help@${domain}>"
|
||||
"List-Subscribe: <mailto:${list}+subscribe@${domain}>"
|
||||
"List-Unsubscribe: <mailto:${list}+unsubscribe@${domain}>"
|
||||
];
|
||||
footer = domain: list: "To unsubscribe send a mail to ${list}+unsubscribe@${domain}";
|
||||
createList = d: l:
|
||||
let ctlDir = listCtl d l; in
|
||||
''
|
||||
for DIR in incoming queue queue/discarded archive text subconf unsubconf \
|
||||
bounce control moderation subscribers.d digesters.d requeue \
|
||||
nomailsubs.d
|
||||
do
|
||||
mkdir -p '${listDir d l}'/"$DIR"
|
||||
done
|
||||
${pkgs.coreutils}/bin/mkdir -p ${ctlDir}
|
||||
echo ${listAddress d l} > '${ctlDir}/listaddress'
|
||||
[ ! -e ${ctlDir}/customheaders ] && \
|
||||
echo "${lib.concatStringsSep "\n" (customHeaders d l)}" > '${ctlDir}/customheaders'
|
||||
[ ! -e ${ctlDir}/footer ] && \
|
||||
echo ${footer d l} > '${ctlDir}/footer'
|
||||
[ ! -e ${ctlDir}/prefix ] && \
|
||||
echo ${subjectPrefix l} > '${ctlDir}/prefix'
|
||||
'';
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.mlmmj = {
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Enable mlmmj";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "mlmmj";
|
||||
description = "mailinglist local user";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "mlmmj";
|
||||
description = "mailinglist local group";
|
||||
};
|
||||
|
||||
listDomain = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = "Set the mailing list domain";
|
||||
};
|
||||
|
||||
mailLists = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "The collection of hosted maillists";
|
||||
};
|
||||
|
||||
maintInterval = mkOption {
|
||||
type = types.str;
|
||||
default = "20min";
|
||||
description = ''
|
||||
Time interval between mlmmj-maintd runs, see
|
||||
<citerefentry><refentrytitle>systemd.time</refentrytitle>
|
||||
<manvolnum>7</manvolnum></citerefentry> for format information.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
users.users.${cfg.user} = {
|
||||
description = "mlmmj user";
|
||||
home = stateDir;
|
||||
createHome = true;
|
||||
uid = config.ids.uids.mlmmj;
|
||||
group = cfg.group;
|
||||
useDefaultShell = true;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = {
|
||||
gid = config.ids.gids.mlmmj;
|
||||
};
|
||||
|
||||
services.postfix = {
|
||||
enable = true;
|
||||
recipientDelimiter= "+";
|
||||
masterConfig.mlmmj = {
|
||||
type = "unix";
|
||||
private = true;
|
||||
privileged = true;
|
||||
chroot = false;
|
||||
wakeup = 0;
|
||||
command = "pipe";
|
||||
args = [
|
||||
"flags=ORhu"
|
||||
"user=mlmmj"
|
||||
"argv=${pkgs.mlmmj}/bin/mlmmj-receive"
|
||||
"-F"
|
||||
"-L"
|
||||
"${spoolDir}/$nexthop"
|
||||
];
|
||||
};
|
||||
|
||||
extraAliases = concatMapLines (alias cfg.listDomain) cfg.mailLists;
|
||||
|
||||
extraConfig = "propagate_unmatched_extensions = virtual";
|
||||
|
||||
virtual = concatMapLines (virtual cfg.listDomain) cfg.mailLists;
|
||||
transport = concatMapLines (transport cfg.listDomain) cfg.mailLists;
|
||||
};
|
||||
|
||||
environment.systemPackages = [ pkgs.mlmmj ];
|
||||
|
||||
system.activationScripts.mlmmj = ''
|
||||
${pkgs.coreutils}/bin/mkdir -p ${stateDir} ${spoolDir}/${cfg.listDomain}
|
||||
${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${spoolDir}
|
||||
${concatMapLines (createList cfg.listDomain) cfg.mailLists}
|
||||
${pkgs.postfix}/bin/postmap /etc/postfix/virtual
|
||||
${pkgs.postfix}/bin/postmap /etc/postfix/transport
|
||||
'';
|
||||
|
||||
systemd.services.mlmmj-maintd = {
|
||||
description = "mlmmj maintenance daemon";
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${pkgs.mlmmj}/bin/mlmmj-maintd -F -d ${spoolDir}/${cfg.listDomain}";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.mlmmj-maintd = {
|
||||
description = "mlmmj maintenance timer";
|
||||
timerConfig.OnUnitActiveSec = cfg.maintInterval;
|
||||
wantedBy = [ "timers.target" ];
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
244
nixos/modules/services/mail/nullmailer.nix
Normal file
244
nixos/modules/services/mail/nullmailer.nix
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
{
|
||||
|
||||
options = {
|
||||
|
||||
services.nullmailer = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable nullmailer daemon.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "nullmailer";
|
||||
description = ''
|
||||
User to use to run nullmailer-send.
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "nullmailer";
|
||||
description = ''
|
||||
Group to use to run nullmailer-send.
|
||||
'';
|
||||
};
|
||||
|
||||
setSendmail = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether to set the system sendmail to nullmailer's.";
|
||||
};
|
||||
|
||||
remotesFile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to the <code>remotes</code> control file. This file contains a
|
||||
list of remote servers to which to send each message.
|
||||
|
||||
See <code>man 8 nullmailer-send</code> for syntax and available
|
||||
options.
|
||||
'';
|
||||
};
|
||||
|
||||
config = {
|
||||
adminaddr = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
If set, all recipients to users at either "localhost" (the literal string)
|
||||
or the canonical host name (from the me control attribute) are remapped to this address.
|
||||
This is provided to allow local daemons to be able to send email to
|
||||
"somebody@localhost" and have it go somewhere sensible instead of being bounced
|
||||
by your relay host. To send to multiple addresses,
|
||||
put them all on one line separated by a comma.
|
||||
'';
|
||||
};
|
||||
|
||||
allmailfrom = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
If set, content will override the envelope sender on all messages.
|
||||
'';
|
||||
};
|
||||
|
||||
defaultdomain = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The content of this attribute is appended to any host name that
|
||||
does not contain a period (except localhost), including defaulthost
|
||||
and idhost. Defaults to the value of the me attribute, if it exists,
|
||||
otherwise the literal name defauldomain.
|
||||
'';
|
||||
};
|
||||
|
||||
defaulthost = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The content of this attribute is appended to any address that
|
||||
is missing a host name. Defaults to the value of the me control
|
||||
attribute, if it exists, otherwise the literal name defaulthost.
|
||||
'';
|
||||
};
|
||||
|
||||
doublebounceto = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
If the original sender was empty (the original message was a
|
||||
delivery status or disposition notification), the double bounce
|
||||
is sent to the address in this attribute.
|
||||
'';
|
||||
};
|
||||
|
||||
helohost = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
Sets the environment variable $HELOHOST which is used by the
|
||||
SMTP protocol module to set the parameter given to the HELO command.
|
||||
Defaults to the value of the me configuration attribute.
|
||||
'';
|
||||
};
|
||||
|
||||
idhost = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The content of this attribute is used when building the message-id
|
||||
string for the message. Defaults to the canonicalized value of defaulthost.
|
||||
'';
|
||||
};
|
||||
|
||||
maxpause = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The maximum time to pause between successive queue runs, in seconds.
|
||||
Defaults to 24 hours (86400).
|
||||
'';
|
||||
};
|
||||
|
||||
me = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The fully-qualifiled host name of the computer running nullmailer.
|
||||
Defaults to the literal name me.
|
||||
'';
|
||||
};
|
||||
|
||||
pausetime = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The minimum time to pause between successive queue runs when there
|
||||
are messages in the queue, in seconds. Defaults to 1 minute (60).
|
||||
Each time this timeout is reached, the timeout is doubled to a
|
||||
maximum of maxpause. After new messages are injected, the timeout
|
||||
is reset. If this is set to 0, nullmailer-send will exit
|
||||
immediately after going through the queue once (one-shot mode).
|
||||
'';
|
||||
};
|
||||
|
||||
remotes = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
A list of remote servers to which to send each message. Each line
|
||||
contains a remote host name or address followed by an optional
|
||||
protocol string, separated by white space.
|
||||
|
||||
See <code>man 8 nullmailer-send</code> for syntax and available
|
||||
options.
|
||||
|
||||
WARNING: This is stored world-readable in the nix store. If you need
|
||||
to specify any secret credentials here, consider using the
|
||||
<code>remotesFile</code> option instead.
|
||||
'';
|
||||
};
|
||||
|
||||
sendtimeout = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = ''
|
||||
The time to wait for a remote module listed above to complete sending
|
||||
a message before killing it and trying again, in seconds.
|
||||
Defaults to 1 hour (3600). If this is set to 0, nullmailer-send
|
||||
will wait forever for messages to complete sending.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = let
|
||||
cfg = config.services.nullmailer;
|
||||
in mkIf cfg.enable {
|
||||
|
||||
assertions = [
|
||||
{ assertion = cfg.config.remotes == null || cfg.remotesFile == null;
|
||||
message = "Only one of `remotesFile` or `config.remotes` may be used at a time.";
|
||||
}
|
||||
];
|
||||
|
||||
environment = {
|
||||
systemPackages = [ pkgs.nullmailer ];
|
||||
etc = let
|
||||
validAttrs = filterAttrs (name: value: value != null) cfg.config;
|
||||
in
|
||||
(foldl' (as: name: as // { "nullmailer/${name}".text = validAttrs.${name}; }) {} (attrNames validAttrs))
|
||||
// optionalAttrs (cfg.remotesFile != null) { "nullmailer/remotes".source = cfg.remotesFile; };
|
||||
};
|
||||
|
||||
users = {
|
||||
users.${cfg.user} = {
|
||||
description = "Nullmailer relay-only mta user";
|
||||
group = cfg.group;
|
||||
isSystemUser = true;
|
||||
};
|
||||
|
||||
groups.${cfg.group} = { };
|
||||
};
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/spool/nullmailer - ${cfg.user} - - -"
|
||||
];
|
||||
|
||||
systemd.services.nullmailer = {
|
||||
description = "nullmailer";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
|
||||
preStart = ''
|
||||
mkdir -p /var/spool/nullmailer/{queue,tmp,failed}
|
||||
rm -f /var/spool/nullmailer/trigger && mkfifo -m 660 /var/spool/nullmailer/trigger
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
ExecStart = "${pkgs.nullmailer}/bin/nullmailer-send";
|
||||
Restart = "always";
|
||||
};
|
||||
};
|
||||
|
||||
services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail {
|
||||
program = "sendmail";
|
||||
source = "${pkgs.nullmailer}/bin/sendmail";
|
||||
owner = cfg.user;
|
||||
group = cfg.group;
|
||||
setuid = true;
|
||||
setgid = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
72
nixos/modules/services/mail/offlineimap.nix
Normal file
72
nixos/modules/services/mail/offlineimap.nix
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.offlineimap;
|
||||
in {
|
||||
|
||||
options.services.offlineimap = {
|
||||
enable = mkEnableOption "OfflineIMAP, a software to dispose your mailbox(es) as a local Maildir(s)";
|
||||
|
||||
install = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to install a user service for Offlineimap. Once
|
||||
the service is started, emails will be fetched automatically.
|
||||
|
||||
The service must be manually started for each user with
|
||||
"systemctl --user start offlineimap" or globally through
|
||||
<varname>services.offlineimap.enable</varname>.
|
||||
'';
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.offlineimap;
|
||||
defaultText = literalExpression "pkgs.offlineimap";
|
||||
description = "Offlineimap derivation to use.";
|
||||
};
|
||||
|
||||
path = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [];
|
||||
example = literalExpression "[ pkgs.pass pkgs.bash pkgs.notmuch ]";
|
||||
description = "List of derivations to put in Offlineimap's path.";
|
||||
};
|
||||
|
||||
onCalendar = mkOption {
|
||||
type = types.str;
|
||||
default = "*:0/3"; # every 3 minutes
|
||||
description = "How often is offlineimap started. Default is '*:0/3' meaning every 3 minutes. See systemd.time(7) for more information about the format.";
|
||||
};
|
||||
|
||||
timeoutStartSec = mkOption {
|
||||
type = types.str;
|
||||
default = "120sec"; # Kill if still alive after 2 minutes
|
||||
description = "How long waiting for offlineimap before killing it. Default is '120sec' meaning every 2 minutes. See systemd.time(7) for more information about the format.";
|
||||
};
|
||||
};
|
||||
config = mkIf (cfg.enable || cfg.install) {
|
||||
systemd.user.services.offlineimap = {
|
||||
description = "Offlineimap: a software to dispose your mailbox(es) as a local Maildir(s)";
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${cfg.package}/bin/offlineimap -u syslog -o -1";
|
||||
TimeoutStartSec = cfg.timeoutStartSec;
|
||||
};
|
||||
path = cfg.path;
|
||||
};
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
systemd.user.timers.offlineimap = {
|
||||
description = "offlineimap timer";
|
||||
timerConfig = {
|
||||
Unit = "offlineimap.service";
|
||||
OnCalendar = cfg.onCalendar;
|
||||
# start immediately after computer is started:
|
||||
Persistent = "true";
|
||||
};
|
||||
} // optionalAttrs cfg.enable { wantedBy = [ "default.target" ]; };
|
||||
};
|
||||
}
|
||||
167
nixos/modules/services/mail/opendkim.nix
Normal file
167
nixos/modules/services/mail/opendkim.nix
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.opendkim;
|
||||
|
||||
defaultSock = "local:/run/opendkim/opendkim.sock";
|
||||
|
||||
keyFile = "${cfg.keyPath}/${cfg.selector}.private";
|
||||
|
||||
args = [ "-f" "-l"
|
||||
"-p" cfg.socket
|
||||
"-d" cfg.domains
|
||||
"-k" keyFile
|
||||
"-s" cfg.selector
|
||||
] ++ optionals (cfg.configFile != null) [ "-x" cfg.configFile ];
|
||||
|
||||
in {
|
||||
imports = [
|
||||
(mkRenamedOptionModule [ "services" "opendkim" "keyFile" ] [ "services" "opendkim" "keyPath" ])
|
||||
];
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.opendkim = {
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable the OpenDKIM sender authentication system.";
|
||||
};
|
||||
|
||||
socket = mkOption {
|
||||
type = types.str;
|
||||
default = defaultSock;
|
||||
description = "Socket which is used for communication with OpenDKIM.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "opendkim";
|
||||
description = "User for the daemon.";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "opendkim";
|
||||
description = "Group for the daemon.";
|
||||
};
|
||||
|
||||
domains = mkOption {
|
||||
type = types.str;
|
||||
default = "csl:${config.networking.hostName}";
|
||||
defaultText = literalExpression ''"csl:''${config.networking.hostName}"'';
|
||||
example = "csl:example.com,mydomain.net";
|
||||
description = ''
|
||||
Local domains set (see <literal>opendkim(8)</literal> for more information on datasets).
|
||||
Messages from them are signed, not verified.
|
||||
'';
|
||||
};
|
||||
|
||||
keyPath = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
The path that opendkim should put its generated private keys into.
|
||||
The DNS settings will be found in this directory with the name selector.txt.
|
||||
'';
|
||||
default = "/var/lib/opendkim/keys";
|
||||
};
|
||||
|
||||
selector = mkOption {
|
||||
type = types.str;
|
||||
description = "Selector to use when signing.";
|
||||
};
|
||||
|
||||
configFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = "Additional opendkim configuration.";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
users.users = optionalAttrs (cfg.user == "opendkim") {
|
||||
opendkim = {
|
||||
group = cfg.group;
|
||||
uid = config.ids.uids.opendkim;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups = optionalAttrs (cfg.group == "opendkim") {
|
||||
opendkim.gid = config.ids.gids.opendkim;
|
||||
};
|
||||
|
||||
environment.systemPackages = [ pkgs.opendkim ];
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d '${cfg.keyPath}' - ${cfg.user} ${cfg.group} - -"
|
||||
];
|
||||
|
||||
systemd.services.opendkim = {
|
||||
description = "OpenDKIM signing and verification daemon";
|
||||
after = [ "network.target" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
preStart = ''
|
||||
cd "${cfg.keyPath}"
|
||||
if ! test -f ${cfg.selector}.private; then
|
||||
${pkgs.opendkim}/bin/opendkim-genkey -s ${cfg.selector} -d all-domains-generic-key
|
||||
echo "Generated OpenDKIM key! Please update your DNS settings:\n"
|
||||
echo "-------------------------------------------------------------"
|
||||
cat ${cfg.selector}.txt
|
||||
echo "-------------------------------------------------------------"
|
||||
fi
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
RuntimeDirectory = optional (cfg.socket == defaultSock) "opendkim";
|
||||
StateDirectory = "opendkim";
|
||||
StateDirectoryMode = "0700";
|
||||
ReadWritePaths = [ cfg.keyPath ];
|
||||
|
||||
AmbientCapabilities = [];
|
||||
CapabilityBoundingSet = "";
|
||||
DevicePolicy = "closed";
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectSystem = "strict";
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6 AF_UNIX" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [ "@system-service" "~@privileged @resources" ];
|
||||
UMask = "0077";
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
135
nixos/modules/services/mail/opensmtpd.nix
Normal file
135
nixos/modules/services/mail/opensmtpd.nix
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.opensmtpd;
|
||||
conf = pkgs.writeText "smtpd.conf" cfg.serverConfiguration;
|
||||
args = concatStringsSep " " cfg.extraServerArgs;
|
||||
|
||||
sendmail = pkgs.runCommand "opensmtpd-sendmail" { preferLocalBuild = true; } ''
|
||||
mkdir -p $out/bin
|
||||
ln -s ${cfg.package}/sbin/smtpctl $out/bin/sendmail
|
||||
'';
|
||||
|
||||
in {
|
||||
|
||||
###### interface
|
||||
|
||||
imports = [
|
||||
(mkRenamedOptionModule [ "services" "opensmtpd" "addSendmailToSystemPath" ] [ "services" "opensmtpd" "setSendmail" ])
|
||||
];
|
||||
|
||||
options = {
|
||||
|
||||
services.opensmtpd = {
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable the OpenSMTPD server.";
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.opensmtpd;
|
||||
defaultText = literalExpression "pkgs.opensmtpd";
|
||||
description = "The OpenSMTPD package to use.";
|
||||
};
|
||||
|
||||
setSendmail = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether to set the system sendmail to OpenSMTPD's.";
|
||||
};
|
||||
|
||||
extraServerArgs = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "-v" "-P mta" ];
|
||||
description = ''
|
||||
Extra command line arguments provided when the smtpd process
|
||||
is started.
|
||||
'';
|
||||
};
|
||||
|
||||
serverConfiguration = mkOption {
|
||||
type = types.lines;
|
||||
example = ''
|
||||
listen on lo
|
||||
accept for any deliver to lmtp localhost:24
|
||||
'';
|
||||
description = ''
|
||||
The contents of the smtpd.conf configuration file. See the
|
||||
OpenSMTPD documentation for syntax information.
|
||||
'';
|
||||
};
|
||||
|
||||
procPackages = mkOption {
|
||||
type = types.listOf types.package;
|
||||
default = [];
|
||||
description = ''
|
||||
Packages to search for filters, tables, queues, and schedulers.
|
||||
|
||||
Add OpenSMTPD-extras here if you want to use the filters, etc. from
|
||||
that package.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable rec {
|
||||
users.groups = {
|
||||
smtpd.gid = config.ids.gids.smtpd;
|
||||
smtpq.gid = config.ids.gids.smtpq;
|
||||
};
|
||||
|
||||
users.users = {
|
||||
smtpd = {
|
||||
description = "OpenSMTPD process user";
|
||||
uid = config.ids.uids.smtpd;
|
||||
group = "smtpd";
|
||||
};
|
||||
smtpq = {
|
||||
description = "OpenSMTPD queue user";
|
||||
uid = config.ids.uids.smtpq;
|
||||
group = "smtpq";
|
||||
};
|
||||
};
|
||||
|
||||
security.wrappers.smtpctl = {
|
||||
owner = "root";
|
||||
group = "smtpq";
|
||||
setuid = false;
|
||||
setgid = true;
|
||||
source = "${cfg.package}/bin/smtpctl";
|
||||
};
|
||||
|
||||
services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail
|
||||
(security.wrappers.smtpctl // { program = "sendmail"; });
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/spool/smtpd 711 root - - -"
|
||||
"d /var/spool/smtpd/offline 770 root smtpq - -"
|
||||
"d /var/spool/smtpd/purge 700 smtpq root - -"
|
||||
];
|
||||
|
||||
systemd.services.opensmtpd = let
|
||||
procEnv = pkgs.buildEnv {
|
||||
name = "opensmtpd-procs";
|
||||
paths = [ cfg.package ] ++ cfg.procPackages;
|
||||
pathsToLink = [ "/libexec/opensmtpd" ];
|
||||
};
|
||||
in {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
serviceConfig.ExecStart = "${cfg.package}/sbin/smtpd -d -f ${conf} ${args}";
|
||||
environment.OPENSMTPD_PROC_PATH = "${procEnv}/libexec/opensmtpd";
|
||||
};
|
||||
};
|
||||
}
|
||||
56
nixos/modules/services/mail/pfix-srsd.nix
Normal file
56
nixos/modules/services/mail/pfix-srsd.nix
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
{
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.pfix-srsd = {
|
||||
enable = mkOption {
|
||||
default = false;
|
||||
type = types.bool;
|
||||
description = "Whether to run the postfix sender rewriting scheme daemon.";
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
description = "The domain for which to enable srs";
|
||||
type = types.str;
|
||||
example = "example.com";
|
||||
};
|
||||
|
||||
secretsFile = mkOption {
|
||||
description = ''
|
||||
The secret data used to encode the SRS address.
|
||||
to generate, use a command like:
|
||||
<literal>for n in $(seq 5); do dd if=/dev/urandom count=1 bs=1024 status=none | sha256sum | sed 's/ -$//' | sed 's/^/ /'; done</literal>
|
||||
'';
|
||||
type = types.path;
|
||||
default = "/var/lib/pfix-srsd/secrets";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf config.services.pfix-srsd.enable {
|
||||
environment = {
|
||||
systemPackages = [ pkgs.pfixtools ];
|
||||
};
|
||||
|
||||
systemd.services.pfix-srsd = {
|
||||
description = "Postfix sender rewriting scheme daemon";
|
||||
before = [ "postfix.service" ];
|
||||
#note that we use requires rather than wants because postfix
|
||||
#is unable to process (almost) all mail without srsd
|
||||
requiredBy = [ "postfix.service" ];
|
||||
serviceConfig = {
|
||||
Type = "forking";
|
||||
PIDFile = "/run/pfix-srsd.pid";
|
||||
ExecStart = "${pkgs.pfixtools}/bin/pfix-srsd -p /run/pfix-srsd.pid -I ${config.services.pfix-srsd.domain} ${config.services.pfix-srsd.secretsFile}";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
994
nixos/modules/services/mail/postfix.nix
Normal file
994
nixos/modules/services/mail/postfix.nix
Normal file
|
|
@ -0,0 +1,994 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.postfix;
|
||||
user = cfg.user;
|
||||
group = cfg.group;
|
||||
setgidGroup = cfg.setgidGroup;
|
||||
|
||||
haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != ""
|
||||
|| cfg.extraAliases != "";
|
||||
haveCanonical = cfg.canonical != "";
|
||||
haveTransport = cfg.transport != "";
|
||||
haveVirtual = cfg.virtual != "";
|
||||
haveLocalRecipients = cfg.localRecipients != null;
|
||||
|
||||
clientAccess =
|
||||
optional (cfg.dnsBlacklistOverrides != "")
|
||||
"check_client_access hash:/etc/postfix/client_access";
|
||||
|
||||
dnsBl =
|
||||
optionals (cfg.dnsBlacklists != [])
|
||||
(map (s: "reject_rbl_client " + s) cfg.dnsBlacklists);
|
||||
|
||||
clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl);
|
||||
|
||||
mainCf = let
|
||||
escape = replaceStrings ["$"] ["$$"];
|
||||
mkList = items: "\n " + concatStringsSep ",\n " items;
|
||||
mkVal = value:
|
||||
if isList value then mkList value
|
||||
else " " + (if value == true then "yes"
|
||||
else if value == false then "no"
|
||||
else toString value);
|
||||
mkEntry = name: value: "${escape name} =${mkVal value}";
|
||||
in
|
||||
concatStringsSep "\n" (mapAttrsToList mkEntry cfg.config)
|
||||
+ "\n" + cfg.extraConfig;
|
||||
|
||||
masterCfOptions = { options, config, name, ... }: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
example = "smtp";
|
||||
description = ''
|
||||
The name of the service to run. Defaults to the attribute set key.
|
||||
'';
|
||||
};
|
||||
|
||||
type = mkOption {
|
||||
type = types.enum [ "inet" "unix" "unix-dgram" "fifo" "pass" ];
|
||||
default = "unix";
|
||||
example = "inet";
|
||||
description = "The type of the service";
|
||||
};
|
||||
|
||||
private = mkOption {
|
||||
type = types.bool;
|
||||
example = false;
|
||||
description = ''
|
||||
Whether the service's sockets and storage directory is restricted to
|
||||
be only available via the mail system. If <literal>null</literal> is
|
||||
given it uses the postfix default <literal>true</literal>.
|
||||
'';
|
||||
};
|
||||
|
||||
privileged = mkOption {
|
||||
type = types.bool;
|
||||
example = true;
|
||||
description = "";
|
||||
};
|
||||
|
||||
chroot = mkOption {
|
||||
type = types.bool;
|
||||
example = true;
|
||||
description = ''
|
||||
Whether the service is chrooted to have only access to the
|
||||
<option>services.postfix.queueDir</option> and the closure of
|
||||
store paths specified by the <option>program</option> option.
|
||||
'';
|
||||
};
|
||||
|
||||
wakeup = mkOption {
|
||||
type = types.int;
|
||||
example = 60;
|
||||
description = ''
|
||||
Automatically wake up the service after the specified number of
|
||||
seconds. If <literal>0</literal> is given, never wake the service
|
||||
up.
|
||||
'';
|
||||
};
|
||||
|
||||
wakeupUnusedComponent = mkOption {
|
||||
type = types.bool;
|
||||
example = false;
|
||||
description = ''
|
||||
If set to <literal>false</literal> the component will only be woken
|
||||
up if it is used. This is equivalent to postfix' notion of adding a
|
||||
question mark behind the wakeup time in
|
||||
<filename>master.cf</filename>
|
||||
'';
|
||||
};
|
||||
|
||||
maxproc = mkOption {
|
||||
type = types.int;
|
||||
example = 1;
|
||||
description = ''
|
||||
The maximum number of processes to spawn for this service. If the
|
||||
value is <literal>0</literal> it doesn't have any limit. If
|
||||
<literal>null</literal> is given it uses the postfix default of
|
||||
<literal>100</literal>.
|
||||
'';
|
||||
};
|
||||
|
||||
command = mkOption {
|
||||
type = types.str;
|
||||
default = name;
|
||||
example = "smtpd";
|
||||
description = ''
|
||||
A program name specifying a Postfix service/daemon process.
|
||||
By default it's the attribute <option>name</option>.
|
||||
'';
|
||||
};
|
||||
|
||||
args = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
example = [ "-o" "smtp_helo_timeout=5" ];
|
||||
description = ''
|
||||
Arguments to pass to the <option>command</option>. There is no shell
|
||||
processing involved and shell syntax is passed verbatim to the
|
||||
process.
|
||||
'';
|
||||
};
|
||||
|
||||
rawEntry = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
internal = true;
|
||||
description = ''
|
||||
The raw configuration line for the <filename>master.cf</filename>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config.rawEntry = let
|
||||
mkBool = bool: if bool then "y" else "n";
|
||||
mkArg = arg: "${optionalString (hasPrefix "-" arg) "\n "}${arg}";
|
||||
|
||||
maybeOption = fun: option:
|
||||
if options.${option}.isDefined then fun config.${option} else "-";
|
||||
|
||||
# This is special, because we have two options for this value.
|
||||
wakeup = let
|
||||
wakeupDefined = options.wakeup.isDefined;
|
||||
wakeupUCDefined = options.wakeupUnusedComponent.isDefined;
|
||||
finalValue = toString config.wakeup
|
||||
+ optionalString (wakeupUCDefined && !config.wakeupUnusedComponent) "?";
|
||||
in if wakeupDefined then finalValue else "-";
|
||||
|
||||
in [
|
||||
config.name
|
||||
config.type
|
||||
(maybeOption mkBool "private")
|
||||
(maybeOption (b: mkBool (!b)) "privileged")
|
||||
(maybeOption mkBool "chroot")
|
||||
wakeup
|
||||
(maybeOption toString "maxproc")
|
||||
(config.command + " " + concatMapStringsSep " " mkArg config.args)
|
||||
];
|
||||
};
|
||||
|
||||
masterCfContent = let
|
||||
|
||||
labels = [
|
||||
"# service" "type" "private" "unpriv" "chroot" "wakeup" "maxproc"
|
||||
"command + args"
|
||||
];
|
||||
|
||||
labelDefaults = [
|
||||
"# " "" "(yes)" "(yes)" "(no)" "(never)" "(100)" "" ""
|
||||
];
|
||||
|
||||
masterCf = mapAttrsToList (const (getAttr "rawEntry")) cfg.masterConfig;
|
||||
|
||||
# A list of the maximum width of the columns across all lines and labels
|
||||
maxWidths = let
|
||||
foldLine = line: acc: let
|
||||
columnLengths = map stringLength line;
|
||||
in zipListsWith max acc columnLengths;
|
||||
# We need to handle the last column specially here, because it's
|
||||
# open-ended (command + args).
|
||||
lines = [ labels labelDefaults ] ++ (map (l: init l ++ [""]) masterCf);
|
||||
in foldr foldLine (genList (const 0) (length labels)) lines;
|
||||
|
||||
# Pad a string with spaces from the right (opposite of fixedWidthString).
|
||||
pad = width: str: let
|
||||
padWidth = width - stringLength str;
|
||||
padding = concatStrings (genList (const " ") padWidth);
|
||||
in str + optionalString (padWidth > 0) padding;
|
||||
|
||||
# It's + 2 here, because that's the amount of spacing between columns.
|
||||
fullWidth = foldr (width: acc: acc + width + 2) 0 maxWidths;
|
||||
|
||||
formatLine = line: concatStringsSep " " (zipListsWith pad maxWidths line);
|
||||
|
||||
formattedLabels = let
|
||||
sep = "# " + concatStrings (genList (const "=") (fullWidth + 5));
|
||||
lines = [ sep (formatLine labels) (formatLine labelDefaults) sep ];
|
||||
in concatStringsSep "\n" lines;
|
||||
|
||||
in formattedLabels + "\n" + concatMapStringsSep "\n" formatLine masterCf + "\n" + cfg.extraMasterConf;
|
||||
|
||||
headerCheckOptions = { ... }:
|
||||
{
|
||||
options = {
|
||||
pattern = mkOption {
|
||||
type = types.str;
|
||||
default = "/^.*/";
|
||||
example = "/^X-Mailer:/";
|
||||
description = "A regexp pattern matching the header";
|
||||
};
|
||||
action = mkOption {
|
||||
type = types.str;
|
||||
default = "DUNNO";
|
||||
example = "BCC mail@example.com";
|
||||
description = "The action to be executed when the pattern is matched";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
headerChecks = concatStringsSep "\n" (map (x: "${x.pattern} ${x.action}") cfg.headerChecks) + cfg.extraHeaderChecks;
|
||||
|
||||
aliases = let seperator = if cfg.aliasMapType == "hash" then ":" else ""; in
|
||||
optionalString (cfg.postmasterAlias != "") ''
|
||||
postmaster${seperator} ${cfg.postmasterAlias}
|
||||
''
|
||||
+ optionalString (cfg.rootAlias != "") ''
|
||||
root${seperator} ${cfg.rootAlias}
|
||||
''
|
||||
+ cfg.extraAliases
|
||||
;
|
||||
|
||||
aliasesFile = pkgs.writeText "postfix-aliases" aliases;
|
||||
canonicalFile = pkgs.writeText "postfix-canonical" cfg.canonical;
|
||||
virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual;
|
||||
localRecipientMapFile = pkgs.writeText "postfix-local-recipient-map" (concatMapStrings (x: x + " ACCEPT\n") cfg.localRecipients);
|
||||
checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides;
|
||||
mainCfFile = pkgs.writeText "postfix-main.cf" mainCf;
|
||||
masterCfFile = pkgs.writeText "postfix-master.cf" masterCfContent;
|
||||
transportFile = pkgs.writeText "postfix-transport" cfg.transport;
|
||||
headerChecksFile = pkgs.writeText "postfix-header-checks" headerChecks;
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.postfix = {
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to run the Postfix mail server.";
|
||||
};
|
||||
|
||||
enableSmtp = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether to enable smtp in master.cf.";
|
||||
};
|
||||
|
||||
enableSubmission = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable smtp submission.";
|
||||
};
|
||||
|
||||
enableSubmissions = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to enable smtp submission via smtps.
|
||||
|
||||
According to RFC 8314 this should be preferred
|
||||
over STARTTLS for submission of messages by end user clients.
|
||||
'';
|
||||
};
|
||||
|
||||
submissionOptions = mkOption {
|
||||
type = with types; attrsOf str;
|
||||
default = {
|
||||
smtpd_tls_security_level = "encrypt";
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||
milter_macro_daemon_name = "ORIGINATING";
|
||||
};
|
||||
example = {
|
||||
smtpd_tls_security_level = "encrypt";
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_sasl_type = "dovecot";
|
||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||
milter_macro_daemon_name = "ORIGINATING";
|
||||
};
|
||||
description = "Options for the submission config in master.cf";
|
||||
};
|
||||
|
||||
submissionsOptions = mkOption {
|
||||
type = with types; attrsOf str;
|
||||
default = {
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||
milter_macro_daemon_name = "ORIGINATING";
|
||||
};
|
||||
example = {
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_sasl_type = "dovecot";
|
||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||
milter_macro_daemon_name = "ORIGINATING";
|
||||
};
|
||||
description = ''
|
||||
Options for the submission config via smtps in master.cf.
|
||||
|
||||
smtpd_tls_security_level will be set to encrypt, if it is missing
|
||||
or has one of the values "may" or "none".
|
||||
|
||||
smtpd_tls_wrappermode with value "yes" will be added automatically.
|
||||
'';
|
||||
};
|
||||
|
||||
setSendmail = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether to set the system sendmail to postfix's.";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "postfix";
|
||||
description = "What to call the Postfix user (must be used only for postfix).";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "postfix";
|
||||
description = "What to call the Postfix group (must be used only for postfix).";
|
||||
};
|
||||
|
||||
setgidGroup = mkOption {
|
||||
type = types.str;
|
||||
default = "postdrop";
|
||||
description = "
|
||||
How to call postfix setgid group (for postdrop). Should
|
||||
be uniquely used group.
|
||||
";
|
||||
};
|
||||
|
||||
networks = mkOption {
|
||||
type = types.nullOr (types.listOf types.str);
|
||||
default = null;
|
||||
example = ["192.168.0.1/24"];
|
||||
description = "
|
||||
Net masks for trusted - allowed to relay mail to third parties -
|
||||
hosts. Leave empty to use mynetworks_style configuration or use
|
||||
default (localhost-only).
|
||||
";
|
||||
};
|
||||
|
||||
networksStyle = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "
|
||||
Name of standard way of trusted network specification to use,
|
||||
leave blank if you specify it explicitly or if you want to use
|
||||
default (localhost-only).
|
||||
";
|
||||
};
|
||||
|
||||
hostname = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description ="
|
||||
Hostname to use. Leave blank to use just the hostname of machine.
|
||||
It should be FQDN.
|
||||
";
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description ="
|
||||
Domain to use. Leave blank to use hostname minus first component.
|
||||
";
|
||||
};
|
||||
|
||||
origin = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description ="
|
||||
Origin to use in outgoing e-mail. Leave blank to use hostname.
|
||||
";
|
||||
};
|
||||
|
||||
destination = mkOption {
|
||||
type = types.nullOr (types.listOf types.str);
|
||||
default = null;
|
||||
example = ["localhost"];
|
||||
description = "
|
||||
Full (!) list of domains we deliver locally. Leave blank for
|
||||
acceptable Postfix default.
|
||||
";
|
||||
};
|
||||
|
||||
relayDomains = mkOption {
|
||||
type = types.nullOr (types.listOf types.str);
|
||||
default = null;
|
||||
example = ["localdomain"];
|
||||
description = "
|
||||
List of domains we agree to relay to. Default is empty.
|
||||
";
|
||||
};
|
||||
|
||||
relayHost = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "
|
||||
Mail relay for outbound mail.
|
||||
";
|
||||
};
|
||||
|
||||
relayPort = mkOption {
|
||||
type = types.int;
|
||||
default = 25;
|
||||
description = "
|
||||
SMTP port for relay mail relay.
|
||||
";
|
||||
};
|
||||
|
||||
lookupMX = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "
|
||||
Whether relay specified is just domain whose MX must be used.
|
||||
";
|
||||
};
|
||||
|
||||
postmasterAlias = mkOption {
|
||||
type = types.str;
|
||||
default = "root";
|
||||
description = "
|
||||
Who should receive postmaster e-mail. Multiple values can be added by
|
||||
separating values with comma.
|
||||
";
|
||||
};
|
||||
|
||||
rootAlias = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "
|
||||
Who should receive root e-mail. Blank for no redirection.
|
||||
Multiple values can be added by separating values with comma.
|
||||
";
|
||||
};
|
||||
|
||||
extraAliases = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = "
|
||||
Additional entries to put verbatim into aliases file, cf. man-page aliases(8).
|
||||
";
|
||||
};
|
||||
|
||||
aliasMapType = mkOption {
|
||||
type = with types; enum [ "hash" "regexp" "pcre" ];
|
||||
default = "hash";
|
||||
example = "regexp";
|
||||
description = "The format the alias map should have. Use regexp if you want to use regular expressions.";
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = with types; attrsOf (oneOf [ bool str (listOf str) ]);
|
||||
description = ''
|
||||
The main.cf configuration file as key value set.
|
||||
'';
|
||||
example = {
|
||||
mail_owner = "postfix";
|
||||
smtp_tls_security_level = "may";
|
||||
};
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = "
|
||||
Extra lines to be added verbatim to the main.cf configuration file.
|
||||
";
|
||||
};
|
||||
|
||||
tlsTrustedAuthorities = mkOption {
|
||||
type = types.str;
|
||||
default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||
defaultText = literalExpression ''"''${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"'';
|
||||
description = ''
|
||||
File containing trusted certification authorities (CA) to verify certificates of mailservers contacted for mail delivery. This basically sets smtp_tls_CAfile and enables opportunistic tls. Defaults to NixOS trusted certification authorities.
|
||||
'';
|
||||
};
|
||||
|
||||
sslCert = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "SSL certificate to use.";
|
||||
};
|
||||
|
||||
sslKey = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "SSL key to use.";
|
||||
};
|
||||
|
||||
recipientDelimiter = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
example = "+";
|
||||
description = "
|
||||
Delimiter for address extension: so mail to user+test can be handled by ~user/.forward+test
|
||||
";
|
||||
};
|
||||
|
||||
canonical = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Entries for the <citerefentry><refentrytitle>canonical</refentrytitle>
|
||||
<manvolnum>5</manvolnum></citerefentry> table.
|
||||
'';
|
||||
};
|
||||
|
||||
virtual = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = "
|
||||
Entries for the virtual alias map, cf. man-page virtual(5).
|
||||
";
|
||||
};
|
||||
|
||||
virtualMapType = mkOption {
|
||||
type = types.enum ["hash" "regexp" "pcre"];
|
||||
default = "hash";
|
||||
description = ''
|
||||
What type of virtual alias map file to use. Use <literal>"regexp"</literal> for regular expressions.
|
||||
'';
|
||||
};
|
||||
|
||||
localRecipients = mkOption {
|
||||
type = with types; nullOr (listOf str);
|
||||
default = null;
|
||||
description = ''
|
||||
List of accepted local users. Specify a bare username, an
|
||||
<literal>"@domain.tld"</literal> wild-card, or a complete
|
||||
<literal>"user@domain.tld"</literal> address. If set, these names end
|
||||
up in the local recipient map -- see the local(8) man-page -- and
|
||||
effectively replace the system user database lookup that's otherwise
|
||||
used by default.
|
||||
'';
|
||||
};
|
||||
|
||||
transport = mkOption {
|
||||
default = "";
|
||||
type = types.lines;
|
||||
description = "
|
||||
Entries for the transport map, cf. man-page transport(8).
|
||||
";
|
||||
};
|
||||
|
||||
dnsBlacklists = mkOption {
|
||||
default = [];
|
||||
type = with types; listOf str;
|
||||
description = "dns blacklist servers to use with smtpd_client_restrictions";
|
||||
};
|
||||
|
||||
dnsBlacklistOverrides = mkOption {
|
||||
default = "";
|
||||
type = types.lines;
|
||||
description = "contents of check_client_access for overriding dnsBlacklists";
|
||||
};
|
||||
|
||||
masterConfig = mkOption {
|
||||
type = types.attrsOf (types.submodule masterCfOptions);
|
||||
default = {};
|
||||
example =
|
||||
{ submission = {
|
||||
type = "inet";
|
||||
args = [ "-o" "smtpd_tls_security_level=encrypt" ];
|
||||
};
|
||||
};
|
||||
description = ''
|
||||
An attribute set of service options, which correspond to the service
|
||||
definitions usually done within the Postfix
|
||||
<filename>master.cf</filename> file.
|
||||
'';
|
||||
};
|
||||
|
||||
extraMasterConf = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
example = "submission inet n - n - - smtpd";
|
||||
description = "Extra lines to append to the generated master.cf file.";
|
||||
};
|
||||
|
||||
enableHeaderChecks = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
example = true;
|
||||
description = "Whether to enable postfix header checks";
|
||||
};
|
||||
|
||||
headerChecks = mkOption {
|
||||
type = types.listOf (types.submodule headerCheckOptions);
|
||||
default = [];
|
||||
example = [ { pattern = "/^X-Spam-Flag:/"; action = "REDIRECT spam@example.com"; } ];
|
||||
description = "Postfix header checks.";
|
||||
};
|
||||
|
||||
extraHeaderChecks = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
example = "/^X-Spam-Flag:/ REDIRECT spam@example.com";
|
||||
description = "Extra lines to /etc/postfix/header_checks file.";
|
||||
};
|
||||
|
||||
aliasFiles = mkOption {
|
||||
type = types.attrsOf types.path;
|
||||
default = {};
|
||||
description = "Aliases' tables to be compiled and placed into /var/lib/postfix/conf.";
|
||||
};
|
||||
|
||||
mapFiles = mkOption {
|
||||
type = types.attrsOf types.path;
|
||||
default = {};
|
||||
description = "Maps to be compiled and placed into /var/lib/postfix/conf.";
|
||||
};
|
||||
|
||||
useSrs = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable sender rewriting scheme";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf config.services.postfix.enable (mkMerge [
|
||||
{
|
||||
|
||||
environment = {
|
||||
etc.postfix.source = "/var/lib/postfix/conf";
|
||||
|
||||
# This makes it comfortable to run 'postqueue/postdrop' for example.
|
||||
systemPackages = [ pkgs.postfix ];
|
||||
};
|
||||
|
||||
services.pfix-srsd.enable = config.services.postfix.useSrs;
|
||||
|
||||
services.mail.sendmailSetuidWrapper = mkIf config.services.postfix.setSendmail {
|
||||
program = "sendmail";
|
||||
source = "${pkgs.postfix}/bin/sendmail";
|
||||
owner = "root";
|
||||
group = setgidGroup;
|
||||
setuid = false;
|
||||
setgid = true;
|
||||
};
|
||||
|
||||
security.wrappers.mailq = {
|
||||
program = "mailq";
|
||||
source = "${pkgs.postfix}/bin/mailq";
|
||||
owner = "root";
|
||||
group = setgidGroup;
|
||||
setuid = false;
|
||||
setgid = true;
|
||||
};
|
||||
|
||||
security.wrappers.postqueue = {
|
||||
program = "postqueue";
|
||||
source = "${pkgs.postfix}/bin/postqueue";
|
||||
owner = "root";
|
||||
group = setgidGroup;
|
||||
setuid = false;
|
||||
setgid = true;
|
||||
};
|
||||
|
||||
security.wrappers.postdrop = {
|
||||
program = "postdrop";
|
||||
source = "${pkgs.postfix}/bin/postdrop";
|
||||
owner = "root";
|
||||
group = setgidGroup;
|
||||
setuid = false;
|
||||
setgid = true;
|
||||
};
|
||||
|
||||
users.users = optionalAttrs (user == "postfix")
|
||||
{ postfix = {
|
||||
description = "Postfix mail server user";
|
||||
uid = config.ids.uids.postfix;
|
||||
group = group;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups =
|
||||
optionalAttrs (group == "postfix")
|
||||
{ ${group}.gid = config.ids.gids.postfix;
|
||||
}
|
||||
// optionalAttrs (setgidGroup == "postdrop")
|
||||
{ ${setgidGroup}.gid = config.ids.gids.postdrop;
|
||||
};
|
||||
|
||||
systemd.services.postfix-setup =
|
||||
{ description = "Setup for Postfix mail server";
|
||||
serviceConfig.RemainAfterExit = true;
|
||||
serviceConfig.Type = "oneshot";
|
||||
script = ''
|
||||
# Backwards compatibility
|
||||
if [ ! -d /var/lib/postfix ] && [ -d /var/postfix ]; then
|
||||
mkdir -p /var/lib
|
||||
mv /var/postfix /var/lib/postfix
|
||||
fi
|
||||
|
||||
# All permissions set according ${pkgs.postfix}/etc/postfix/postfix-files script
|
||||
mkdir -p /var/lib/postfix /var/lib/postfix/queue/{pid,public,maildrop}
|
||||
chmod 0755 /var/lib/postfix
|
||||
chown root:root /var/lib/postfix
|
||||
|
||||
rm -rf /var/lib/postfix/conf
|
||||
mkdir -p /var/lib/postfix/conf
|
||||
chmod 0755 /var/lib/postfix/conf
|
||||
ln -sf ${pkgs.postfix}/etc/postfix/postfix-files /var/lib/postfix/conf/postfix-files
|
||||
ln -sf ${mainCfFile} /var/lib/postfix/conf/main.cf
|
||||
ln -sf ${masterCfFile} /var/lib/postfix/conf/master.cf
|
||||
|
||||
${concatStringsSep "\n" (mapAttrsToList (to: from: ''
|
||||
ln -sf ${from} /var/lib/postfix/conf/${to}
|
||||
${pkgs.postfix}/bin/postalias /var/lib/postfix/conf/${to}
|
||||
'') cfg.aliasFiles)}
|
||||
${concatStringsSep "\n" (mapAttrsToList (to: from: ''
|
||||
ln -sf ${from} /var/lib/postfix/conf/${to}
|
||||
${pkgs.postfix}/bin/postmap /var/lib/postfix/conf/${to}
|
||||
'') cfg.mapFiles)}
|
||||
|
||||
mkdir -p /var/spool/mail
|
||||
chown root:root /var/spool/mail
|
||||
chmod a+rwxt /var/spool/mail
|
||||
ln -sf /var/spool/mail /var/
|
||||
|
||||
#Finally delegate to postfix checking remain directories in /var/lib/postfix and set permissions on them
|
||||
${pkgs.postfix}/bin/postfix set-permissions config_directory=/var/lib/postfix/conf
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.services.postfix =
|
||||
{ description = "Postfix mail server";
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" "postfix-setup.service" ];
|
||||
requires = [ "postfix-setup.service" ];
|
||||
path = [ pkgs.postfix ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "forking";
|
||||
Restart = "always";
|
||||
PIDFile = "/var/lib/postfix/queue/pid/master.pid";
|
||||
ExecStart = "${pkgs.postfix}/bin/postfix start";
|
||||
ExecStop = "${pkgs.postfix}/bin/postfix stop";
|
||||
ExecReload = "${pkgs.postfix}/bin/postfix reload";
|
||||
};
|
||||
};
|
||||
|
||||
services.postfix.config = (mapAttrs (_: v: mkDefault v) {
|
||||
compatibility_level = pkgs.postfix.version;
|
||||
mail_owner = cfg.user;
|
||||
default_privs = "nobody";
|
||||
|
||||
# NixOS specific locations
|
||||
data_directory = "/var/lib/postfix/data";
|
||||
queue_directory = "/var/lib/postfix/queue";
|
||||
|
||||
# Default location of everything in package
|
||||
meta_directory = "${pkgs.postfix}/etc/postfix";
|
||||
command_directory = "${pkgs.postfix}/bin";
|
||||
sample_directory = "/etc/postfix";
|
||||
newaliases_path = "${pkgs.postfix}/bin/newaliases";
|
||||
mailq_path = "${pkgs.postfix}/bin/mailq";
|
||||
readme_directory = false;
|
||||
sendmail_path = "${pkgs.postfix}/bin/sendmail";
|
||||
daemon_directory = "${pkgs.postfix}/libexec/postfix";
|
||||
manpage_directory = "${pkgs.postfix}/share/man";
|
||||
html_directory = "${pkgs.postfix}/share/postfix/doc/html";
|
||||
shlib_directory = false;
|
||||
mail_spool_directory = "/var/spool/mail/";
|
||||
setgid_group = cfg.setgidGroup;
|
||||
})
|
||||
// optionalAttrs (cfg.relayHost != "") { relayhost = if cfg.lookupMX
|
||||
then "${cfg.relayHost}:${toString cfg.relayPort}"
|
||||
else "[${cfg.relayHost}]:${toString cfg.relayPort}"; }
|
||||
// optionalAttrs config.networking.enableIPv6 { inet_protocols = mkDefault "all"; }
|
||||
// optionalAttrs (cfg.networks != null) { mynetworks = cfg.networks; }
|
||||
// optionalAttrs (cfg.networksStyle != "") { mynetworks_style = cfg.networksStyle; }
|
||||
// optionalAttrs (cfg.hostname != "") { myhostname = cfg.hostname; }
|
||||
// optionalAttrs (cfg.domain != "") { mydomain = cfg.domain; }
|
||||
// optionalAttrs (cfg.origin != "") { myorigin = cfg.origin; }
|
||||
// optionalAttrs (cfg.destination != null) { mydestination = cfg.destination; }
|
||||
// optionalAttrs (cfg.relayDomains != null) { relay_domains = cfg.relayDomains; }
|
||||
// optionalAttrs (cfg.recipientDelimiter != "") { recipient_delimiter = cfg.recipientDelimiter; }
|
||||
// optionalAttrs haveAliases { alias_maps = [ "${cfg.aliasMapType}:/etc/postfix/aliases" ]; }
|
||||
// optionalAttrs haveTransport { transport_maps = [ "hash:/etc/postfix/transport" ]; }
|
||||
// optionalAttrs haveVirtual { virtual_alias_maps = [ "${cfg.virtualMapType}:/etc/postfix/virtual" ]; }
|
||||
// optionalAttrs haveLocalRecipients { local_recipient_maps = [ "hash:/etc/postfix/local_recipients" ] ++ optional haveAliases "$alias_maps"; }
|
||||
// optionalAttrs (cfg.dnsBlacklists != []) { smtpd_client_restrictions = clientRestrictions; }
|
||||
// optionalAttrs cfg.useSrs {
|
||||
sender_canonical_maps = [ "tcp:127.0.0.1:10001" ];
|
||||
sender_canonical_classes = [ "envelope_sender" ];
|
||||
recipient_canonical_maps = [ "tcp:127.0.0.1:10002" ];
|
||||
recipient_canonical_classes = [ "envelope_recipient" ];
|
||||
}
|
||||
// optionalAttrs cfg.enableHeaderChecks { header_checks = [ "regexp:/etc/postfix/header_checks" ]; }
|
||||
// optionalAttrs (cfg.tlsTrustedAuthorities != "") {
|
||||
smtp_tls_CAfile = cfg.tlsTrustedAuthorities;
|
||||
smtp_tls_security_level = mkDefault "may";
|
||||
}
|
||||
// optionalAttrs (cfg.sslCert != "") {
|
||||
smtp_tls_cert_file = cfg.sslCert;
|
||||
smtp_tls_key_file = cfg.sslKey;
|
||||
|
||||
smtp_tls_security_level = mkDefault "may";
|
||||
|
||||
smtpd_tls_cert_file = cfg.sslCert;
|
||||
smtpd_tls_key_file = cfg.sslKey;
|
||||
|
||||
smtpd_tls_security_level = "may";
|
||||
};
|
||||
|
||||
services.postfix.masterConfig = {
|
||||
pickup = {
|
||||
private = false;
|
||||
wakeup = 60;
|
||||
maxproc = 1;
|
||||
};
|
||||
cleanup = {
|
||||
private = false;
|
||||
maxproc = 0;
|
||||
};
|
||||
qmgr = {
|
||||
private = false;
|
||||
wakeup = 300;
|
||||
maxproc = 1;
|
||||
};
|
||||
tlsmgr = {
|
||||
wakeup = 1000;
|
||||
wakeupUnusedComponent = false;
|
||||
maxproc = 1;
|
||||
};
|
||||
rewrite = {
|
||||
command = "trivial-rewrite";
|
||||
};
|
||||
bounce = {
|
||||
maxproc = 0;
|
||||
};
|
||||
defer = {
|
||||
maxproc = 0;
|
||||
command = "bounce";
|
||||
};
|
||||
trace = {
|
||||
maxproc = 0;
|
||||
command = "bounce";
|
||||
};
|
||||
verify = {
|
||||
maxproc = 1;
|
||||
};
|
||||
flush = {
|
||||
private = false;
|
||||
wakeup = 1000;
|
||||
wakeupUnusedComponent = false;
|
||||
maxproc = 0;
|
||||
};
|
||||
proxymap = {
|
||||
command = "proxymap";
|
||||
};
|
||||
proxywrite = {
|
||||
maxproc = 1;
|
||||
command = "proxymap";
|
||||
};
|
||||
showq = {
|
||||
private = false;
|
||||
};
|
||||
error = {};
|
||||
retry = {
|
||||
command = "error";
|
||||
};
|
||||
discard = {};
|
||||
local = {
|
||||
privileged = true;
|
||||
};
|
||||
virtual = {
|
||||
privileged = true;
|
||||
};
|
||||
lmtp = {
|
||||
};
|
||||
anvil = {
|
||||
maxproc = 1;
|
||||
};
|
||||
scache = {
|
||||
maxproc = 1;
|
||||
};
|
||||
} // optionalAttrs cfg.enableSubmission {
|
||||
submission = {
|
||||
type = "inet";
|
||||
private = false;
|
||||
command = "smtpd";
|
||||
args = let
|
||||
mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
|
||||
in concatLists (mapAttrsToList mkKeyVal cfg.submissionOptions);
|
||||
};
|
||||
} // optionalAttrs cfg.enableSmtp {
|
||||
smtp_inet = {
|
||||
name = "smtp";
|
||||
type = "inet";
|
||||
private = false;
|
||||
command = "smtpd";
|
||||
};
|
||||
smtp = {};
|
||||
relay = {
|
||||
command = "smtp";
|
||||
args = [ "-o" "smtp_fallback_relay=" ];
|
||||
};
|
||||
} // optionalAttrs cfg.enableSubmissions {
|
||||
submissions = {
|
||||
type = "inet";
|
||||
private = false;
|
||||
command = "smtpd";
|
||||
args = let
|
||||
mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ];
|
||||
adjustSmtpTlsSecurityLevel = !(cfg.submissionsOptions ? smtpd_tls_security_level) ||
|
||||
cfg.submissionsOptions.smtpd_tls_security_level == "none" ||
|
||||
cfg.submissionsOptions.smtpd_tls_security_level == "may";
|
||||
submissionsOptions = cfg.submissionsOptions // {
|
||||
smtpd_tls_wrappermode = "yes";
|
||||
} // optionalAttrs adjustSmtpTlsSecurityLevel {
|
||||
smtpd_tls_security_level = "encrypt";
|
||||
};
|
||||
in concatLists (mapAttrsToList mkKeyVal submissionsOptions);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
(mkIf haveAliases {
|
||||
services.postfix.aliasFiles.aliases = aliasesFile;
|
||||
})
|
||||
(mkIf haveCanonical {
|
||||
services.postfix.mapFiles.canonical = canonicalFile;
|
||||
})
|
||||
(mkIf haveTransport {
|
||||
services.postfix.mapFiles.transport = transportFile;
|
||||
})
|
||||
(mkIf haveVirtual {
|
||||
services.postfix.mapFiles.virtual = virtualFile;
|
||||
})
|
||||
(mkIf haveLocalRecipients {
|
||||
services.postfix.mapFiles.local_recipients = localRecipientMapFile;
|
||||
})
|
||||
(mkIf cfg.enableHeaderChecks {
|
||||
services.postfix.mapFiles.header_checks = headerChecksFile;
|
||||
})
|
||||
(mkIf (cfg.dnsBlacklists != []) {
|
||||
services.postfix.mapFiles.client_access = checkClientAccessFile;
|
||||
})
|
||||
]);
|
||||
|
||||
imports = [
|
||||
(mkRemovedOptionModule [ "services" "postfix" "sslCACert" ]
|
||||
"services.postfix.sslCACert was replaced by services.postfix.tlsTrustedAuthorities. In case you intend that your server should validate requested client certificates use services.postfix.extraConfig.")
|
||||
|
||||
(mkChangedOptionModule [ "services" "postfix" "useDane" ]
|
||||
[ "services" "postfix" "config" "smtp_tls_security_level" ]
|
||||
(config: mkIf config.services.postfix.useDane "dane"))
|
||||
];
|
||||
}
|
||||
199
nixos/modules/services/mail/postfixadmin.nix
Normal file
199
nixos/modules/services/mail/postfixadmin.nix
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
{ lib, config, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.postfixadmin;
|
||||
fpm = config.services.phpfpm.pools.postfixadmin;
|
||||
localDB = cfg.database.host == "localhost";
|
||||
user = if localDB then cfg.database.username else "nginx";
|
||||
in
|
||||
{
|
||||
options.services.postfixadmin = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to enable postfixadmin.
|
||||
|
||||
Also enables nginx virtual host management.
|
||||
Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.<name></literal>.
|
||||
See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
|
||||
'';
|
||||
};
|
||||
|
||||
hostName = mkOption {
|
||||
type = types.str;
|
||||
example = "postfixadmin.example.com";
|
||||
description = "Hostname to use for the nginx vhost";
|
||||
};
|
||||
|
||||
adminEmail = mkOption {
|
||||
type = types.str;
|
||||
example = "postmaster@example.com";
|
||||
description = ''
|
||||
Defines the Site Admin's email address.
|
||||
This will be used to send emails from to create mailboxes and
|
||||
from Send Email / Broadcast message pages.
|
||||
'';
|
||||
};
|
||||
|
||||
setupPasswordFile = mkOption {
|
||||
type = types.path;
|
||||
description = ''
|
||||
Password file for the admin.
|
||||
Generate with <literal>php -r "echo password_hash('some password here', PASSWORD_DEFAULT);"</literal>
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
username = mkOption {
|
||||
type = types.str;
|
||||
default = "postfixadmin";
|
||||
description = ''
|
||||
Username for the postgresql connection.
|
||||
If <literal>database.host</literal> is set to <literal>localhost</literal>, a unix user and group of the same name will be created as well.
|
||||
'';
|
||||
};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = ''
|
||||
Host of the postgresql server. If this is not set to
|
||||
<literal>localhost</literal>, you have to create the
|
||||
postgresql user and database yourself, with appropriate
|
||||
permissions.
|
||||
'';
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
type = types.path;
|
||||
description = "Password file for the postgresql connection. Must be readable by user <literal>nginx</literal>.";
|
||||
};
|
||||
dbname = mkOption {
|
||||
type = types.str;
|
||||
default = "postfixadmin";
|
||||
description = "Name of the postgresql database";
|
||||
};
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = "Extra configuration for the postfixadmin instance, see postfixadmin's config.inc.php for available options.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
environment.etc."postfixadmin/config.local.php".text = ''
|
||||
<?php
|
||||
|
||||
$CONF['setup_password'] = file_get_contents('${cfg.setupPasswordFile}');
|
||||
|
||||
$CONF['database_type'] = 'pgsql';
|
||||
$CONF['database_host'] = ${if localDB then "null" else "'${cfg.database.host}'"};
|
||||
${optionalString localDB "$CONF['database_user'] = '${cfg.database.username}';"}
|
||||
$CONF['database_password'] = ${if localDB then "'dummy'" else "file_get_contents('${cfg.database.passwordFile}')"};
|
||||
$CONF['database_name'] = '${cfg.database.dbname}';
|
||||
$CONF['configured'] = true;
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
|
||||
systemd.tmpfiles.rules = [ "d /var/cache/postfixadmin/templates_c 700 ${user} ${user}" ];
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
${cfg.hostName} = {
|
||||
forceSSL = mkDefault true;
|
||||
enableACME = mkDefault true;
|
||||
locations."/" = {
|
||||
root = "${pkgs.postfixadmin}/public";
|
||||
index = "index.php";
|
||||
extraConfig = ''
|
||||
location ~* \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:${fpm.socket};
|
||||
include ${config.services.nginx.package}/conf/fastcgi_params;
|
||||
include ${pkgs.nginx}/conf/fastcgi.conf;
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.postgresql = mkIf localDB {
|
||||
enable = true;
|
||||
ensureUsers = [ {
|
||||
name = cfg.database.username;
|
||||
} ];
|
||||
};
|
||||
# The postgresql module doesn't currently support concepts like
|
||||
# objects owners and extensions; for now we tack on what's needed
|
||||
# here.
|
||||
systemd.services.postfixadmin-postgres = let pgsql = config.services.postgresql; in mkIf localDB {
|
||||
after = [ "postgresql.service" ];
|
||||
bindsTo = [ "postgresql.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
path = [
|
||||
pgsql.package
|
||||
pkgs.util-linux
|
||||
];
|
||||
script = ''
|
||||
set -eu
|
||||
|
||||
PSQL() {
|
||||
psql --port=${toString pgsql.port} "$@"
|
||||
}
|
||||
|
||||
PSQL -tAc "SELECT 1 FROM pg_database WHERE datname = '${cfg.database.dbname}'" | grep -q 1 || PSQL -tAc 'CREATE DATABASE "${cfg.database.dbname}" OWNER "${cfg.database.username}"'
|
||||
current_owner=$(PSQL -tAc "SELECT pg_catalog.pg_get_userbyid(datdba) FROM pg_catalog.pg_database WHERE datname = '${cfg.database.dbname}'")
|
||||
if [[ "$current_owner" != "${cfg.database.username}" ]]; then
|
||||
PSQL -tAc 'ALTER DATABASE "${cfg.database.dbname}" OWNER TO "${cfg.database.username}"'
|
||||
if [[ -e "${config.services.postgresql.dataDir}/.reassigning_${cfg.database.dbname}" ]]; then
|
||||
echo "Reassigning ownership of database ${cfg.database.dbname} to user ${cfg.database.username} failed on last boot. Failing..."
|
||||
exit 1
|
||||
fi
|
||||
touch "${config.services.postgresql.dataDir}/.reassigning_${cfg.database.dbname}"
|
||||
PSQL "${cfg.database.dbname}" -tAc "REASSIGN OWNED BY \"$current_owner\" TO \"${cfg.database.username}\""
|
||||
rm "${config.services.postgresql.dataDir}/.reassigning_${cfg.database.dbname}"
|
||||
fi
|
||||
'';
|
||||
|
||||
serviceConfig = {
|
||||
User = pgsql.superUser;
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
};
|
||||
};
|
||||
|
||||
users.users.${user} = mkIf localDB {
|
||||
group = user;
|
||||
isSystemUser = true;
|
||||
createHome = false;
|
||||
};
|
||||
users.groups.${user} = mkIf localDB {};
|
||||
|
||||
services.phpfpm.pools.postfixadmin = {
|
||||
user = user;
|
||||
phpPackage = pkgs.php74;
|
||||
phpOptions = ''
|
||||
error_log = 'stderr'
|
||||
log_errors = on
|
||||
'';
|
||||
settings = mapAttrs (name: mkDefault) {
|
||||
"listen.owner" = "nginx";
|
||||
"listen.group" = "nginx";
|
||||
"listen.mode" = "0660";
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 75;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 1;
|
||||
"pm.max_spare_servers" = 20;
|
||||
"pm.max_requests" = 500;
|
||||
"catch_workers_output" = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
205
nixos/modules/services/mail/postgrey.nix
Normal file
205
nixos/modules/services/mail/postgrey.nix
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib; let
|
||||
|
||||
cfg = config.services.postgrey;
|
||||
|
||||
natural = with types; addCheck int (x: x >= 0);
|
||||
natural' = with types; addCheck int (x: x > 0);
|
||||
|
||||
socket = with types; addCheck (either (submodule unixSocket) (submodule inetSocket)) (x: x ? path || x ? port);
|
||||
|
||||
inetSocket = with types; {
|
||||
options = {
|
||||
addr = mkOption {
|
||||
type = nullOr str;
|
||||
default = null;
|
||||
example = "127.0.0.1";
|
||||
description = "The address to bind to. Localhost if null";
|
||||
};
|
||||
port = mkOption {
|
||||
type = natural';
|
||||
default = 10030;
|
||||
description = "Tcp port to bind to";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
unixSocket = with types; {
|
||||
options = {
|
||||
path = mkOption {
|
||||
type = path;
|
||||
default = "/run/postgrey.sock";
|
||||
description = "Path of the unix socket";
|
||||
};
|
||||
|
||||
mode = mkOption {
|
||||
type = str;
|
||||
default = "0777";
|
||||
description = "Mode of the unix socket";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
in {
|
||||
imports = [
|
||||
(mkMergedOptionModule [ [ "services" "postgrey" "inetAddr" ] [ "services" "postgrey" "inetPort" ] ] [ "services" "postgrey" "socket" ] (config: let
|
||||
value = p: getAttrFromPath p config;
|
||||
inetAddr = [ "services" "postgrey" "inetAddr" ];
|
||||
inetPort = [ "services" "postgrey" "inetPort" ];
|
||||
in
|
||||
if value inetAddr == null
|
||||
then { path = "/run/postgrey.sock"; }
|
||||
else { addr = value inetAddr; port = value inetPort; }
|
||||
))
|
||||
];
|
||||
|
||||
options = {
|
||||
services.postgrey = with types; {
|
||||
enable = mkOption {
|
||||
type = bool;
|
||||
default = false;
|
||||
description = "Whether to run the Postgrey daemon";
|
||||
};
|
||||
socket = mkOption {
|
||||
type = socket;
|
||||
default = {
|
||||
path = "/run/postgrey.sock";
|
||||
mode = "0777";
|
||||
};
|
||||
example = {
|
||||
addr = "127.0.0.1";
|
||||
port = 10030;
|
||||
};
|
||||
description = "Socket to bind to";
|
||||
};
|
||||
greylistText = mkOption {
|
||||
type = str;
|
||||
default = "Greylisted for %%s seconds";
|
||||
description = "Response status text for greylisted messages; use %%s for seconds left until greylisting is over and %%r for mail domain of recipient";
|
||||
};
|
||||
greylistAction = mkOption {
|
||||
type = str;
|
||||
default = "DEFER_IF_PERMIT";
|
||||
description = "Response status for greylisted messages (see access(5))";
|
||||
};
|
||||
greylistHeader = mkOption {
|
||||
type = str;
|
||||
default = "X-Greylist: delayed %%t seconds by postgrey-%%v at %%h; %%d";
|
||||
description = "Prepend header to greylisted mails; use %%t for seconds delayed due to greylisting, %%v for the version of postgrey, %%d for the date, and %%h for the host";
|
||||
};
|
||||
delay = mkOption {
|
||||
type = natural;
|
||||
default = 300;
|
||||
description = "Greylist for N seconds";
|
||||
};
|
||||
maxAge = mkOption {
|
||||
type = natural;
|
||||
default = 35;
|
||||
description = "Delete entries from whitelist if they haven't been seen for N days";
|
||||
};
|
||||
retryWindow = mkOption {
|
||||
type = either str natural;
|
||||
default = 2;
|
||||
example = "12h";
|
||||
description = "Allow N days for the first retry. Use string with appended 'h' to specify time in hours";
|
||||
};
|
||||
lookupBySubnet = mkOption {
|
||||
type = bool;
|
||||
default = true;
|
||||
description = "Strip the last N bits from IP addresses, determined by IPv4CIDR and IPv6CIDR";
|
||||
};
|
||||
IPv4CIDR = mkOption {
|
||||
type = natural;
|
||||
default = 24;
|
||||
description = "Strip N bits from IPv4 addresses if lookupBySubnet is true";
|
||||
};
|
||||
IPv6CIDR = mkOption {
|
||||
type = natural;
|
||||
default = 64;
|
||||
description = "Strip N bits from IPv6 addresses if lookupBySubnet is true";
|
||||
};
|
||||
privacy = mkOption {
|
||||
type = bool;
|
||||
default = true;
|
||||
description = "Store data using one-way hash functions (SHA1)";
|
||||
};
|
||||
autoWhitelist = mkOption {
|
||||
type = nullOr natural';
|
||||
default = 5;
|
||||
description = "Whitelist clients after successful delivery of N messages";
|
||||
};
|
||||
whitelistClients = mkOption {
|
||||
type = listOf path;
|
||||
default = [];
|
||||
description = "Client address whitelist files (see postgrey(8))";
|
||||
};
|
||||
whitelistRecipients = mkOption {
|
||||
type = listOf path;
|
||||
default = [];
|
||||
description = "Recipient address whitelist files (see postgrey(8))";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
environment.systemPackages = [ pkgs.postgrey ];
|
||||
|
||||
users = {
|
||||
users = {
|
||||
postgrey = {
|
||||
description = "Postgrey Daemon";
|
||||
uid = config.ids.uids.postgrey;
|
||||
group = "postgrey";
|
||||
};
|
||||
};
|
||||
groups = {
|
||||
postgrey = {
|
||||
gid = config.ids.gids.postgrey;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.postgrey = let
|
||||
bind-flag = if cfg.socket ? path then
|
||||
"--unix=${cfg.socket.path} --socketmode=${cfg.socket.mode}"
|
||||
else
|
||||
''--inet=${optionalString (cfg.socket.addr != null) (cfg.socket.addr + ":")}${toString cfg.socket.port}'';
|
||||
in {
|
||||
description = "Postfix Greylisting Service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "postfix.service" ];
|
||||
preStart = ''
|
||||
mkdir -p /var/postgrey
|
||||
chown postgrey:postgrey /var/postgrey
|
||||
chmod 0770 /var/postgrey
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "simple";
|
||||
ExecStart = ''${pkgs.postgrey}/bin/postgrey \
|
||||
${bind-flag} \
|
||||
--group=postgrey --user=postgrey \
|
||||
--dbdir=/var/postgrey \
|
||||
--delay=${toString cfg.delay} \
|
||||
--max-age=${toString cfg.maxAge} \
|
||||
--retry-window=${toString cfg.retryWindow} \
|
||||
${if cfg.lookupBySubnet then "--lookup-by-subnet" else "--lookup-by-host"} \
|
||||
--ipv4cidr=${toString cfg.IPv4CIDR} --ipv6cidr=${toString cfg.IPv6CIDR} \
|
||||
${optionalString cfg.privacy "--privacy"} \
|
||||
--auto-whitelist-clients=${toString (if cfg.autoWhitelist == null then 0 else cfg.autoWhitelist)} \
|
||||
--greylist-action=${cfg.greylistAction} \
|
||||
--greylist-text="${cfg.greylistText}" \
|
||||
--x-greylist-header="${cfg.greylistHeader}" \
|
||||
${concatMapStringsSep " " (x: "--whitelist-clients=" + x) cfg.whitelistClients} \
|
||||
${concatMapStringsSep " " (x: "--whitelist-recipients=" + x) cfg.whitelistRecipients}
|
||||
'';
|
||||
Restart = "always";
|
||||
RestartSec = 5;
|
||||
TimeoutSec = 10;
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
135
nixos/modules/services/mail/postsrsd.nix
Normal file
135
nixos/modules/services/mail/postsrsd.nix
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.postsrsd;
|
||||
|
||||
in {
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.postsrsd = {
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable the postsrsd SRS server for Postfix.";
|
||||
};
|
||||
|
||||
secretsFile = mkOption {
|
||||
type = types.path;
|
||||
default = "/var/lib/postsrsd/postsrsd.secret";
|
||||
description = "Secret keys used for signing and verification";
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
type = types.str;
|
||||
description = "Domain name for rewrite";
|
||||
};
|
||||
|
||||
separator = mkOption {
|
||||
type = types.enum ["-" "=" "+"];
|
||||
default = "=";
|
||||
description = "First separator character in generated addresses";
|
||||
};
|
||||
|
||||
# bindAddress = mkOption { # uncomment once 1.5 is released
|
||||
# type = types.str;
|
||||
# default = "127.0.0.1";
|
||||
# description = "Socket listen address";
|
||||
# };
|
||||
|
||||
forwardPort = mkOption {
|
||||
type = types.int;
|
||||
default = 10001;
|
||||
description = "Port for the forward SRS lookup";
|
||||
};
|
||||
|
||||
reversePort = mkOption {
|
||||
type = types.int;
|
||||
default = 10002;
|
||||
description = "Port for the reverse SRS lookup";
|
||||
};
|
||||
|
||||
timeout = mkOption {
|
||||
type = types.int;
|
||||
default = 1800;
|
||||
description = "Timeout for idle client connections in seconds";
|
||||
};
|
||||
|
||||
excludeDomains = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Origin domains to exclude from rewriting in addition to primary domain";
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "postsrsd";
|
||||
description = "User for the daemon";
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "postsrsd";
|
||||
description = "Group for the daemon";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
services.postsrsd.domain = mkDefault config.networking.hostName;
|
||||
|
||||
users.users = optionalAttrs (cfg.user == "postsrsd") {
|
||||
postsrsd = {
|
||||
group = cfg.group;
|
||||
uid = config.ids.uids.postsrsd;
|
||||
};
|
||||
};
|
||||
|
||||
users.groups = optionalAttrs (cfg.group == "postsrsd") {
|
||||
postsrsd.gid = config.ids.gids.postsrsd;
|
||||
};
|
||||
|
||||
systemd.services.postsrsd = {
|
||||
description = "PostSRSd SRS rewriting server";
|
||||
after = [ "network.target" ];
|
||||
before = [ "postfix.service" ];
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
||||
path = [ pkgs.coreutils ];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = ''${pkgs.postsrsd}/sbin/postsrsd "-s${cfg.secretsFile}" "-d${cfg.domain}" -a${cfg.separator} -f${toString cfg.forwardPort} -r${toString cfg.reversePort} -t${toString cfg.timeout} "-X${concatStringsSep "," cfg.excludeDomains}"'';
|
||||
User = cfg.user;
|
||||
Group = cfg.group;
|
||||
PermissionsStartOnly = true;
|
||||
};
|
||||
|
||||
preStart = ''
|
||||
if [ ! -e "${cfg.secretsFile}" ]; then
|
||||
echo "WARNING: secrets file not found, autogenerating!"
|
||||
DIR="$(dirname "${cfg.secretsFile}")"
|
||||
if [ ! -d "$DIR" ]; then
|
||||
mkdir -p -m750 "$DIR"
|
||||
chown "${cfg.user}:${cfg.group}" "$DIR"
|
||||
fi
|
||||
dd if=/dev/random bs=18 count=1 | base64 > "${cfg.secretsFile}"
|
||||
chmod 600 "${cfg.secretsFile}"
|
||||
fi
|
||||
chown "${cfg.user}:${cfg.group}" "${cfg.secretsFile}"
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
}
|
||||
579
nixos/modules/services/mail/public-inbox.nix
Normal file
579
nixos/modules/services/mail/public-inbox.nix
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
{ lib, pkgs, config, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.public-inbox;
|
||||
stateDir = "/var/lib/public-inbox";
|
||||
|
||||
manref = name: vol: "<citerefentry><refentrytitle>${name}</refentrytitle><manvolnum>${toString vol}</manvolnum></citerefentry>";
|
||||
|
||||
gitIni = pkgs.formats.gitIni { listsAsDuplicateKeys = true; };
|
||||
iniAtom = elemAt gitIni.type/*attrsOf*/.functor.wrapped/*attrsOf*/.functor.wrapped/*either*/.functor.wrapped 0;
|
||||
|
||||
useSpamAssassin = cfg.settings.publicinboxmda.spamcheck == "spamc" ||
|
||||
cfg.settings.publicinboxwatch.spamcheck == "spamc";
|
||||
|
||||
publicInboxDaemonOptions = proto: defaultPort: {
|
||||
args = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = "Command-line arguments to pass to ${manref "public-inbox-${proto}d" 1}.";
|
||||
};
|
||||
port = mkOption {
|
||||
type = with types; nullOr (either str port);
|
||||
default = defaultPort;
|
||||
description = ''
|
||||
Listening port.
|
||||
Beware that public-inbox uses well-known ports number to decide whether to enable TLS or not.
|
||||
Set to null and use <code>systemd.sockets.public-inbox-${proto}d.listenStreams</code>
|
||||
if you need a more advanced listening.
|
||||
'';
|
||||
};
|
||||
cert = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
example = "/path/to/fullchain.pem";
|
||||
description = "Path to TLS certificate to use for connections to ${manref "public-inbox-${proto}d" 1}.";
|
||||
};
|
||||
key = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
example = "/path/to/key.pem";
|
||||
description = "Path to TLS key to use for connections to ${manref "public-inbox-${proto}d" 1}.";
|
||||
};
|
||||
};
|
||||
|
||||
serviceConfig = srv:
|
||||
let proto = removeSuffix "d" srv;
|
||||
needNetwork = builtins.hasAttr proto cfg && cfg.${proto}.port == null;
|
||||
in {
|
||||
serviceConfig = {
|
||||
# Enable JIT-compiled C (via Inline::C)
|
||||
Environment = [ "PERL_INLINE_DIRECTORY=/run/public-inbox-${srv}/perl-inline" ];
|
||||
# NonBlocking is REQUIRED to avoid a race condition
|
||||
# if running simultaneous services.
|
||||
NonBlocking = true;
|
||||
#LimitNOFILE = 30000;
|
||||
User = config.users.users."public-inbox".name;
|
||||
Group = config.users.groups."public-inbox".name;
|
||||
RuntimeDirectory = [
|
||||
"public-inbox-${srv}/perl-inline"
|
||||
];
|
||||
RuntimeDirectoryMode = "700";
|
||||
# This is for BindPaths= and BindReadOnlyPaths=
|
||||
# to allow traversal of directories they create inside RootDirectory=
|
||||
UMask = "0066";
|
||||
StateDirectory = ["public-inbox"];
|
||||
StateDirectoryMode = "0750";
|
||||
WorkingDirectory = stateDir;
|
||||
BindReadOnlyPaths = [
|
||||
"/etc"
|
||||
"/run/systemd"
|
||||
"${config.i18n.glibcLocales}"
|
||||
] ++
|
||||
mapAttrsToList (name: inbox: inbox.description) cfg.inboxes ++
|
||||
# Without confinement the whole Nix store
|
||||
# is made available to the service
|
||||
optionals (!config.systemd.services."public-inbox-${srv}".confinement.enable) [
|
||||
"${pkgs.dash}/bin/dash:/bin/sh"
|
||||
builtins.storeDir
|
||||
];
|
||||
# The following options are only for optimizing:
|
||||
# systemd-analyze security public-inbox-'*'
|
||||
AmbientCapabilities = "";
|
||||
CapabilityBoundingSet = "";
|
||||
# ProtectClock= adds DeviceAllow=char-rtc r
|
||||
DeviceAllow = "";
|
||||
LockPersonality = true;
|
||||
MemoryDenyWriteExecute = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateNetwork = mkDefault (!needNetwork);
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectHome = mkDefault true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectProc = "invisible";
|
||||
#ProtectSystem = "strict";
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_UNIX" ] ++
|
||||
optionals needNetwork [ "AF_INET" "AF_INET6" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@aio" "~@chown" "~@keyring" "~@memlock" "~@resources"
|
||||
# Not removing @setuid and @privileged because Inline::C needs them.
|
||||
# Not removing @timer because git upload-pack needs it.
|
||||
];
|
||||
SystemCallArchitectures = "native";
|
||||
|
||||
# The following options are redundant when confinement is enabled
|
||||
RootDirectory = "/var/empty";
|
||||
TemporaryFileSystem = "/";
|
||||
PrivateMounts = true;
|
||||
MountAPIVFS = true;
|
||||
PrivateDevices = true;
|
||||
PrivateTmp = true;
|
||||
PrivateUsers = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
};
|
||||
confinement = {
|
||||
# Until we agree upon doing it directly here in NixOS
|
||||
# https://github.com/NixOS/nixpkgs/pull/104457#issuecomment-1115768447
|
||||
# let the user choose to enable the confinement with:
|
||||
# systemd.services.public-inbox-httpd.confinement.enable = true;
|
||||
# systemd.services.public-inbox-imapd.confinement.enable = true;
|
||||
# systemd.services.public-inbox-init.confinement.enable = true;
|
||||
# systemd.services.public-inbox-nntpd.confinement.enable = true;
|
||||
#enable = true;
|
||||
mode = "full-apivfs";
|
||||
# Inline::C needs a /bin/sh, and dash is enough
|
||||
binSh = "${pkgs.dash}/bin/dash";
|
||||
packages = [
|
||||
pkgs.iana-etc
|
||||
(getLib pkgs.nss)
|
||||
pkgs.tzdata
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
options.services.public-inbox = {
|
||||
enable = mkEnableOption "the public-inbox mail archiver";
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.public-inbox;
|
||||
defaultText = literalExpression "pkgs.public-inbox";
|
||||
description = "public-inbox package to use.";
|
||||
};
|
||||
path = mkOption {
|
||||
type = with types; listOf package;
|
||||
default = [];
|
||||
example = literalExpression "with pkgs; [ spamassassin ]";
|
||||
description = ''
|
||||
Additional packages to place in the path of public-inbox-mda,
|
||||
public-inbox-watch, etc.
|
||||
'';
|
||||
};
|
||||
inboxes = mkOption {
|
||||
description = ''
|
||||
Inboxes to configure, where attribute names are inbox names.
|
||||
'';
|
||||
default = {};
|
||||
type = types.attrsOf (types.submodule ({name, ...}: {
|
||||
freeformType = types.attrsOf iniAtom;
|
||||
options.inboxdir = mkOption {
|
||||
type = types.str;
|
||||
default = "${stateDir}/inboxes/${name}";
|
||||
description = "The absolute path to the directory which hosts the public-inbox.";
|
||||
};
|
||||
options.address = mkOption {
|
||||
type = with types; listOf str;
|
||||
example = "example-discuss@example.org";
|
||||
description = "The email addresses of the public-inbox.";
|
||||
};
|
||||
options.url = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
example = "https://example.org/lists/example-discuss";
|
||||
description = "URL where this inbox can be accessed over HTTP.";
|
||||
};
|
||||
options.description = mkOption {
|
||||
type = types.str;
|
||||
example = "user/dev discussion of public-inbox itself";
|
||||
description = "User-visible description for the repository.";
|
||||
apply = pkgs.writeText "public-inbox-description-${name}";
|
||||
};
|
||||
options.newsgroup = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
description = "NNTP group name for the inbox.";
|
||||
};
|
||||
options.watch = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = "Paths for ${manref "public-inbox-watch" 1} to monitor for new mail.";
|
||||
example = [ "maildir:/path/to/test.example.com.git" ];
|
||||
};
|
||||
options.watchheader = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
example = "List-Id:<test@example.com>";
|
||||
description = ''
|
||||
If specified, ${manref "public-inbox-watch" 1} will only process
|
||||
mail containing a matching header.
|
||||
'';
|
||||
};
|
||||
options.coderepo = mkOption {
|
||||
type = (types.listOf (types.enum (attrNames cfg.settings.coderepo))) // {
|
||||
description = "list of coderepo names";
|
||||
};
|
||||
default = [];
|
||||
description = "Nicknames of a 'coderepo' section associated with the inbox.";
|
||||
};
|
||||
}));
|
||||
};
|
||||
imap = {
|
||||
enable = mkEnableOption "the public-inbox IMAP server";
|
||||
} // publicInboxDaemonOptions "imap" 993;
|
||||
http = {
|
||||
enable = mkEnableOption "the public-inbox HTTP server";
|
||||
mounts = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [ "/" ];
|
||||
example = [ "/lists/archives" ];
|
||||
description = ''
|
||||
Root paths or URLs that public-inbox will be served on.
|
||||
If domain parts are present, only requests to those
|
||||
domains will be accepted.
|
||||
'';
|
||||
};
|
||||
args = (publicInboxDaemonOptions "http" 80).args;
|
||||
port = mkOption {
|
||||
type = with types; nullOr (either str port);
|
||||
default = 80;
|
||||
example = "/run/public-inbox-httpd.sock";
|
||||
description = ''
|
||||
Listening port or systemd's ListenStream= entry
|
||||
to be used as a reverse proxy, eg. in nginx:
|
||||
<code>locations."/inbox".proxyPass = "http://unix:''${config.services.public-inbox.http.port}:/inbox";</code>
|
||||
Set to null and use <code>systemd.sockets.public-inbox-httpd.listenStreams</code>
|
||||
if you need a more advanced listening.
|
||||
'';
|
||||
};
|
||||
};
|
||||
mda = {
|
||||
enable = mkEnableOption "the public-inbox Mail Delivery Agent";
|
||||
args = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = "Command-line arguments to pass to ${manref "public-inbox-mda" 1}.";
|
||||
};
|
||||
};
|
||||
postfix.enable = mkEnableOption "the integration into Postfix";
|
||||
nntp = {
|
||||
enable = mkEnableOption "the public-inbox NNTP server";
|
||||
} // publicInboxDaemonOptions "nntp" 563;
|
||||
spamAssassinRules = mkOption {
|
||||
type = with types; nullOr path;
|
||||
default = "${cfg.package.sa_config}/user/.spamassassin/user_prefs";
|
||||
defaultText = literalExpression "\${cfg.package.sa_config}/user/.spamassassin/user_prefs";
|
||||
description = "SpamAssassin configuration specific to public-inbox.";
|
||||
};
|
||||
settings = mkOption {
|
||||
description = ''
|
||||
Settings for the <link xlink:href="https://public-inbox.org/public-inbox-config.html">public-inbox config file</link>.
|
||||
'';
|
||||
default = {};
|
||||
type = types.submodule {
|
||||
freeformType = gitIni.type;
|
||||
options.publicinbox = mkOption {
|
||||
default = {};
|
||||
description = "public inboxes";
|
||||
type = types.submodule {
|
||||
freeformType = with types; /*inbox name*/attrsOf (/*inbox option name*/attrsOf /*inbox option value*/iniAtom);
|
||||
options.css = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
description = "The local path name of a CSS file for the PSGI web interface.";
|
||||
};
|
||||
options.nntpserver = mkOption {
|
||||
type = with types; listOf str;
|
||||
default = [];
|
||||
example = [ "nntp://news.public-inbox.org" "nntps://news.public-inbox.org" ];
|
||||
description = "NNTP URLs to this public-inbox instance";
|
||||
};
|
||||
options.wwwlisting = mkOption {
|
||||
type = with types; enum [ "all" "404" "match=domain" ];
|
||||
default = "404";
|
||||
description = ''
|
||||
Controls which lists (if any) are listed for when the root
|
||||
public-inbox URL is accessed over HTTP.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
options.publicinboxmda.spamcheck = mkOption {
|
||||
type = with types; enum [ "spamc" "none" ];
|
||||
default = "none";
|
||||
description = ''
|
||||
If set to spamc, ${manref "public-inbox-watch" 1} will filter spam
|
||||
using SpamAssassin.
|
||||
'';
|
||||
};
|
||||
options.publicinboxwatch.spamcheck = mkOption {
|
||||
type = with types; enum [ "spamc" "none" ];
|
||||
default = "none";
|
||||
description = ''
|
||||
If set to spamc, ${manref "public-inbox-watch" 1} will filter spam
|
||||
using SpamAssassin.
|
||||
'';
|
||||
};
|
||||
options.publicinboxwatch.watchspam = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
example = "maildir:/path/to/spam";
|
||||
description = ''
|
||||
If set, mail in this maildir will be trained as spam and
|
||||
deleted from all watched inboxes
|
||||
'';
|
||||
};
|
||||
options.coderepo = mkOption {
|
||||
default = {};
|
||||
description = "code repositories";
|
||||
type = types.attrsOf (types.submodule {
|
||||
freeformType = types.attrsOf iniAtom;
|
||||
options.cgitUrl = mkOption {
|
||||
type = types.str;
|
||||
description = "URL of a cgit instance";
|
||||
};
|
||||
options.dir = mkOption {
|
||||
type = types.str;
|
||||
description = "Path to a git repository";
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
};
|
||||
openFirewall = mkEnableOption "opening the firewall when using a port option";
|
||||
};
|
||||
config = mkIf cfg.enable {
|
||||
assertions = [
|
||||
{ assertion = config.services.spamassassin.enable || !useSpamAssassin;
|
||||
message = ''
|
||||
public-inbox is configured to use SpamAssassin, but
|
||||
services.spamassassin.enable is false. If you don't need
|
||||
spam checking, set `services.public-inbox.settings.publicinboxmda.spamcheck' and
|
||||
`services.public-inbox.settings.publicinboxwatch.spamcheck' to null.
|
||||
'';
|
||||
}
|
||||
{ assertion = cfg.path != [] || !useSpamAssassin;
|
||||
message = ''
|
||||
public-inbox is configured to use SpamAssassin, but there is
|
||||
no spamc executable in services.public-inbox.path. If you
|
||||
don't need spam checking, set
|
||||
`services.public-inbox.settings.publicinboxmda.spamcheck' and
|
||||
`services.public-inbox.settings.publicinboxwatch.spamcheck' to null.
|
||||
'';
|
||||
}
|
||||
];
|
||||
services.public-inbox.settings =
|
||||
filterAttrsRecursive (n: v: v != null) {
|
||||
publicinbox = mapAttrs (n: filterAttrs (n: v: n != "description")) cfg.inboxes;
|
||||
};
|
||||
users = {
|
||||
users.public-inbox = {
|
||||
home = stateDir;
|
||||
group = "public-inbox";
|
||||
isSystemUser = true;
|
||||
};
|
||||
groups.public-inbox = {};
|
||||
};
|
||||
networking.firewall = mkIf cfg.openFirewall
|
||||
{ allowedTCPPorts = mkMerge
|
||||
(map (proto: (mkIf (cfg.${proto}.enable && types.port.check cfg.${proto}.port) [ cfg.${proto}.port ]))
|
||||
["imap" "http" "nntp"]);
|
||||
};
|
||||
services.postfix = mkIf (cfg.postfix.enable && cfg.mda.enable) {
|
||||
# Not sure limiting to 1 is necessary, but better safe than sorry.
|
||||
config.public-inbox_destination_recipient_limit = "1";
|
||||
|
||||
# Register the addresses as existing
|
||||
virtual =
|
||||
concatStringsSep "\n" (mapAttrsToList (_: inbox:
|
||||
concatMapStringsSep "\n" (address:
|
||||
"${address} ${address}"
|
||||
) inbox.address
|
||||
) cfg.inboxes);
|
||||
|
||||
# Deliver the addresses with the public-inbox transport
|
||||
transport =
|
||||
concatStringsSep "\n" (mapAttrsToList (_: inbox:
|
||||
concatMapStringsSep "\n" (address:
|
||||
"${address} public-inbox:${address}"
|
||||
) inbox.address
|
||||
) cfg.inboxes);
|
||||
|
||||
# The public-inbox transport
|
||||
masterConfig.public-inbox = {
|
||||
type = "unix";
|
||||
privileged = true; # Required for user=
|
||||
command = "pipe";
|
||||
args = [
|
||||
"flags=X" # Report as a final delivery
|
||||
"user=${with config.users; users."public-inbox".name + ":" + groups."public-inbox".name}"
|
||||
# Specifying a nexthop when using the transport
|
||||
# (eg. test public-inbox:test) allows to
|
||||
# receive mails with an extension (eg. test+foo).
|
||||
"argv=${pkgs.writeShellScript "public-inbox-transport" ''
|
||||
export HOME="${stateDir}"
|
||||
export ORIGINAL_RECIPIENT="''${2:-1}"
|
||||
export PATH="${makeBinPath cfg.path}:$PATH"
|
||||
exec ${cfg.package}/bin/public-inbox-mda ${escapeShellArgs cfg.mda.args}
|
||||
''} \${original_recipient} \${nexthop}"
|
||||
];
|
||||
};
|
||||
};
|
||||
systemd.sockets = mkMerge (map (proto:
|
||||
mkIf (cfg.${proto}.enable && cfg.${proto}.port != null)
|
||||
{ "public-inbox-${proto}d" = {
|
||||
listenStreams = [ (toString cfg.${proto}.port) ];
|
||||
wantedBy = [ "sockets.target" ];
|
||||
};
|
||||
}
|
||||
) [ "imap" "http" "nntp" ]);
|
||||
systemd.services = mkMerge [
|
||||
(mkIf cfg.imap.enable
|
||||
{ public-inbox-imapd = mkMerge [(serviceConfig "imapd") {
|
||||
after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
|
||||
requires = [ "public-inbox-init.service" ];
|
||||
serviceConfig = {
|
||||
ExecStart = escapeShellArgs (
|
||||
[ "${cfg.package}/bin/public-inbox-imapd" ] ++
|
||||
cfg.imap.args ++
|
||||
optionals (cfg.imap.cert != null) [ "--cert" cfg.imap.cert ] ++
|
||||
optionals (cfg.imap.key != null) [ "--key" cfg.imap.key ]
|
||||
);
|
||||
};
|
||||
}];
|
||||
})
|
||||
(mkIf cfg.http.enable
|
||||
{ public-inbox-httpd = mkMerge [(serviceConfig "httpd") {
|
||||
after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
|
||||
requires = [ "public-inbox-init.service" ];
|
||||
serviceConfig = {
|
||||
ExecStart = escapeShellArgs (
|
||||
[ "${cfg.package}/bin/public-inbox-httpd" ] ++
|
||||
cfg.http.args ++
|
||||
# See https://public-inbox.org/public-inbox.git/tree/examples/public-inbox.psgi
|
||||
# for upstream's example.
|
||||
[ (pkgs.writeText "public-inbox.psgi" ''
|
||||
#!${cfg.package.fullperl} -w
|
||||
use strict;
|
||||
use warnings;
|
||||
use Plack::Builder;
|
||||
use PublicInbox::WWW;
|
||||
|
||||
my $www = PublicInbox::WWW->new;
|
||||
$www->preload;
|
||||
|
||||
builder {
|
||||
# If reached through a reverse proxy,
|
||||
# make it transparent by resetting some HTTP headers
|
||||
# used by public-inbox to generate URIs.
|
||||
enable 'ReverseProxy';
|
||||
|
||||
# No need to send a response body if it's an HTTP HEAD requests.
|
||||
enable 'Head';
|
||||
|
||||
# Route according to configured domains and root paths.
|
||||
${concatMapStrings (path: ''
|
||||
mount q(${path}) => sub { $www->call(@_); };
|
||||
'') cfg.http.mounts}
|
||||
}
|
||||
'') ]
|
||||
);
|
||||
};
|
||||
}];
|
||||
})
|
||||
(mkIf cfg.nntp.enable
|
||||
{ public-inbox-nntpd = mkMerge [(serviceConfig "nntpd") {
|
||||
after = [ "public-inbox-init.service" "public-inbox-watch.service" ];
|
||||
requires = [ "public-inbox-init.service" ];
|
||||
serviceConfig = {
|
||||
ExecStart = escapeShellArgs (
|
||||
[ "${cfg.package}/bin/public-inbox-nntpd" ] ++
|
||||
cfg.nntp.args ++
|
||||
optionals (cfg.nntp.cert != null) [ "--cert" cfg.nntp.cert ] ++
|
||||
optionals (cfg.nntp.key != null) [ "--key" cfg.nntp.key ]
|
||||
);
|
||||
};
|
||||
}];
|
||||
})
|
||||
(mkIf (any (inbox: inbox.watch != []) (attrValues cfg.inboxes)
|
||||
|| cfg.settings.publicinboxwatch.watchspam != null)
|
||||
{ public-inbox-watch = mkMerge [(serviceConfig "watch") {
|
||||
inherit (cfg) path;
|
||||
wants = [ "public-inbox-init.service" ];
|
||||
requires = [ "public-inbox-init.service" ] ++
|
||||
optional (cfg.settings.publicinboxwatch.spamcheck == "spamc") "spamassassin.service";
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
serviceConfig = {
|
||||
ExecStart = "${cfg.package}/bin/public-inbox-watch";
|
||||
ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
||||
};
|
||||
}];
|
||||
})
|
||||
({ public-inbox-init = let
|
||||
PI_CONFIG = gitIni.generate "public-inbox.ini"
|
||||
(filterAttrsRecursive (n: v: v != null) cfg.settings);
|
||||
in mkMerge [(serviceConfig "init") {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
restartIfChanged = true;
|
||||
restartTriggers = [ PI_CONFIG ];
|
||||
script = ''
|
||||
set -ux
|
||||
install -D -p ${PI_CONFIG} ${stateDir}/.public-inbox/config
|
||||
'' + optionalString useSpamAssassin ''
|
||||
install -m 0700 -o spamd -d ${stateDir}/.spamassassin
|
||||
${optionalString (cfg.spamAssassinRules != null) ''
|
||||
ln -sf ${cfg.spamAssassinRules} ${stateDir}/.spamassassin/user_prefs
|
||||
''}
|
||||
'' + concatStrings (mapAttrsToList (name: inbox: ''
|
||||
if [ ! -e ${stateDir}/inboxes/${escapeShellArg name} ]; then
|
||||
# public-inbox-init creates an inbox and adds it to a config file.
|
||||
# It tries to atomically write the config file by creating
|
||||
# another file in the same directory, and renaming it.
|
||||
# This has the sad consequence that we can't use
|
||||
# /dev/null, or it would try to create a file in /dev.
|
||||
conf_dir="$(mktemp -d)"
|
||||
|
||||
PI_CONFIG=$conf_dir/conf \
|
||||
${cfg.package}/bin/public-inbox-init -V2 \
|
||||
${escapeShellArgs ([ name "${stateDir}/inboxes/${name}" inbox.url ] ++ inbox.address)}
|
||||
|
||||
rm -rf $conf_dir
|
||||
fi
|
||||
|
||||
ln -sf ${inbox.description} \
|
||||
${stateDir}/inboxes/${escapeShellArg name}/description
|
||||
|
||||
export GIT_DIR=${stateDir}/inboxes/${escapeShellArg name}/all.git
|
||||
if test -d "$GIT_DIR"; then
|
||||
# Config is inherited by each epoch repository,
|
||||
# so just needs to be set for all.git.
|
||||
${pkgs.git}/bin/git config core.sharedRepository 0640
|
||||
fi
|
||||
'') cfg.inboxes
|
||||
) + ''
|
||||
shopt -s nullglob
|
||||
for inbox in ${stateDir}/inboxes/*/; do
|
||||
# This should be idempotent, but only do it for new
|
||||
# inboxes anyway because it's only needed once, and could
|
||||
# be slow for large pre-existing inboxes.
|
||||
ls -1 "$inbox" | grep -q '^xap' ||
|
||||
${cfg.package}/bin/public-inbox-index "$inbox"
|
||||
done
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
RemainAfterExit = true;
|
||||
StateDirectory = [
|
||||
"public-inbox/.public-inbox"
|
||||
"public-inbox/.public-inbox/emergency"
|
||||
"public-inbox/inboxes"
|
||||
];
|
||||
};
|
||||
}];
|
||||
})
|
||||
];
|
||||
environment.systemPackages = with pkgs; [ cfg.package ];
|
||||
};
|
||||
meta.maintainers = with lib.maintainers; [ julm qyliss ];
|
||||
}
|
||||
249
nixos/modules/services/mail/roundcube.nix
Normal file
249
nixos/modules/services/mail/roundcube.nix
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
{ lib, config, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.roundcube;
|
||||
fpm = config.services.phpfpm.pools.roundcube;
|
||||
localDB = cfg.database.host == "localhost";
|
||||
user = cfg.database.username;
|
||||
phpWithPspell = pkgs.php80.withExtensions ({ enabled, all }: [ all.pspell ] ++ enabled);
|
||||
in
|
||||
{
|
||||
options.services.roundcube = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to enable roundcube.
|
||||
|
||||
Also enables nginx virtual host management.
|
||||
Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.<name></literal>.
|
||||
See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
|
||||
'';
|
||||
};
|
||||
|
||||
hostName = mkOption {
|
||||
type = types.str;
|
||||
example = "webmail.example.com";
|
||||
description = "Hostname to use for the nginx vhost";
|
||||
};
|
||||
|
||||
package = mkOption {
|
||||
type = types.package;
|
||||
default = pkgs.roundcube;
|
||||
defaultText = literalExpression "pkgs.roundcube";
|
||||
|
||||
example = literalExpression ''
|
||||
roundcube.withPlugins (plugins: [ plugins.persistent_login ])
|
||||
'';
|
||||
|
||||
description = ''
|
||||
The package which contains roundcube's sources. Can be overriden to create
|
||||
an environment which contains roundcube and third-party plugins.
|
||||
'';
|
||||
};
|
||||
|
||||
database = {
|
||||
username = mkOption {
|
||||
type = types.str;
|
||||
default = "roundcube";
|
||||
description = ''
|
||||
Username for the postgresql connection.
|
||||
If <literal>database.host</literal> is set to <literal>localhost</literal>, a unix user and group of the same name will be created as well.
|
||||
'';
|
||||
};
|
||||
host = mkOption {
|
||||
type = types.str;
|
||||
default = "localhost";
|
||||
description = ''
|
||||
Host of the postgresql server. If this is not set to
|
||||
<literal>localhost</literal>, you have to create the
|
||||
postgresql user and database yourself, with appropriate
|
||||
permissions.
|
||||
'';
|
||||
};
|
||||
password = mkOption {
|
||||
type = types.str;
|
||||
description = "Password for the postgresql connection. Do not use: the password will be stored world readable in the store; use <literal>passwordFile</literal> instead.";
|
||||
default = "";
|
||||
};
|
||||
passwordFile = mkOption {
|
||||
type = types.str;
|
||||
description = "Password file for the postgresql connection. Must be readable by user <literal>nginx</literal>. Ignored if <literal>database.host</literal> is set to <literal>localhost</literal>, as peer authentication will be used.";
|
||||
};
|
||||
dbname = mkOption {
|
||||
type = types.str;
|
||||
default = "roundcube";
|
||||
description = "Name of the postgresql database";
|
||||
};
|
||||
};
|
||||
|
||||
plugins = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = ''
|
||||
List of roundcube plugins to enable. Currently, only those directly shipped with Roundcube are supported.
|
||||
'';
|
||||
};
|
||||
|
||||
dicts = mkOption {
|
||||
type = types.listOf types.package;
|
||||
default = [];
|
||||
example = literalExpression "with pkgs.aspellDicts; [ en fr de ]";
|
||||
description = ''
|
||||
List of aspell dictionnaries for spell checking. If empty, spell checking is disabled.
|
||||
'';
|
||||
};
|
||||
|
||||
maxAttachmentSize = mkOption {
|
||||
type = types.int;
|
||||
default = 18;
|
||||
description = ''
|
||||
The maximum attachment size in MB.
|
||||
|
||||
Note: Since roundcube only uses 70% of max upload values configured in php
|
||||
30% is added automatically to <xref linkend="opt-services.roundcube.maxAttachmentSize"/>.
|
||||
'';
|
||||
apply = configuredMaxAttachmentSize: "${toString (configuredMaxAttachmentSize * 1.3)}M";
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = "Extra configuration for roundcube webmail instance";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
# backward compatibility: if password is set but not passwordFile, make one.
|
||||
services.roundcube.database.passwordFile = mkIf (!localDB && cfg.database.password != "") (mkDefault ("${pkgs.writeText "roundcube-password" cfg.database.password}"));
|
||||
warnings = lib.optional (!localDB && cfg.database.password != "") "services.roundcube.database.password is deprecated and insecure; use services.roundcube.database.passwordFile instead";
|
||||
|
||||
environment.etc."roundcube/config.inc.php".text = ''
|
||||
<?php
|
||||
|
||||
${lib.optionalString (!localDB) "$password = file_get_contents('${cfg.database.passwordFile}');"}
|
||||
|
||||
$config = array();
|
||||
$config['db_dsnw'] = 'pgsql://${cfg.database.username}${lib.optionalString (!localDB) ":' . $password . '"}@${if localDB then "unix(/run/postgresql)" else cfg.database.host}/${cfg.database.dbname}';
|
||||
$config['log_driver'] = 'syslog';
|
||||
$config['max_message_size'] = '${cfg.maxAttachmentSize}';
|
||||
$config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
|
||||
$config['des_key'] = file_get_contents('/var/lib/roundcube/des_key');
|
||||
$config['mime_types'] = '${pkgs.nginx}/conf/mime.types';
|
||||
$config['enable_spellcheck'] = ${if cfg.dicts == [] then "false" else "true"};
|
||||
# by default, spellchecking uses a third-party cloud services
|
||||
$config['spellcheck_engine'] = 'pspell';
|
||||
$config['spellcheck_languages'] = array(${lib.concatMapStringsSep ", " (dict: let p = builtins.parseDrvName dict.shortName; in "'${p.name}' => '${dict.fullName}'") cfg.dicts});
|
||||
|
||||
${cfg.extraConfig}
|
||||
'';
|
||||
|
||||
services.nginx = {
|
||||
enable = true;
|
||||
virtualHosts = {
|
||||
${cfg.hostName} = {
|
||||
forceSSL = mkDefault true;
|
||||
enableACME = mkDefault true;
|
||||
locations."/" = {
|
||||
root = cfg.package;
|
||||
index = "index.php";
|
||||
extraConfig = ''
|
||||
location ~* \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:${fpm.socket};
|
||||
include ${config.services.nginx.package}/conf/fastcgi_params;
|
||||
include ${pkgs.nginx}/conf/fastcgi.conf;
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.postgresql = mkIf localDB {
|
||||
enable = true;
|
||||
ensureDatabases = [ cfg.database.dbname ];
|
||||
ensureUsers = [ {
|
||||
name = cfg.database.username;
|
||||
ensurePermissions = {
|
||||
"DATABASE ${cfg.database.username}" = "ALL PRIVILEGES";
|
||||
};
|
||||
} ];
|
||||
};
|
||||
|
||||
users.users.${user} = mkIf localDB {
|
||||
group = user;
|
||||
isSystemUser = true;
|
||||
createHome = false;
|
||||
};
|
||||
users.groups.${user} = mkIf localDB {};
|
||||
|
||||
services.phpfpm.pools.roundcube = {
|
||||
user = if localDB then user else "nginx";
|
||||
phpOptions = ''
|
||||
error_log = 'stderr'
|
||||
log_errors = on
|
||||
post_max_size = ${cfg.maxAttachmentSize}
|
||||
upload_max_filesize = ${cfg.maxAttachmentSize}
|
||||
'';
|
||||
settings = mapAttrs (name: mkDefault) {
|
||||
"listen.owner" = "nginx";
|
||||
"listen.group" = "nginx";
|
||||
"listen.mode" = "0660";
|
||||
"pm" = "dynamic";
|
||||
"pm.max_children" = 75;
|
||||
"pm.start_servers" = 2;
|
||||
"pm.min_spare_servers" = 1;
|
||||
"pm.max_spare_servers" = 20;
|
||||
"pm.max_requests" = 500;
|
||||
"catch_workers_output" = true;
|
||||
};
|
||||
phpPackage = phpWithPspell;
|
||||
phpEnv.ASPELL_CONF = "dict-dir ${pkgs.aspellWithDicts (_: cfg.dicts)}/lib/aspell";
|
||||
};
|
||||
systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
|
||||
|
||||
# Restart on config changes.
|
||||
systemd.services.phpfpm-roundcube.restartTriggers = [
|
||||
config.environment.etc."roundcube/config.inc.php".source
|
||||
];
|
||||
|
||||
systemd.services.roundcube-setup = mkMerge [
|
||||
(mkIf (cfg.database.host == "localhost") {
|
||||
requires = [ "postgresql.service" ];
|
||||
after = [ "postgresql.service" ];
|
||||
path = [ config.services.postgresql.package ];
|
||||
})
|
||||
{
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
script = let
|
||||
psql = "${lib.optionalString (!localDB) "PGPASSFILE=${cfg.database.passwordFile}"} ${pkgs.postgresql}/bin/psql ${lib.optionalString (!localDB) "-h ${cfg.database.host} -U ${cfg.database.username} "} ${cfg.database.dbname}";
|
||||
in
|
||||
''
|
||||
version="$(${psql} -t <<< "select value from system where name = 'roundcube-version';" || true)"
|
||||
if ! (grep -E '[a-zA-Z0-9]' <<< "$version"); then
|
||||
${psql} -f ${cfg.package}/SQL/postgres.initial.sql
|
||||
fi
|
||||
|
||||
if [ ! -f /var/lib/roundcube/des_key ]; then
|
||||
base64 /dev/urandom | head -c 24 > /var/lib/roundcube/des_key;
|
||||
# we need to log out everyone in case change the des_key
|
||||
# from the default when upgrading from nixos 19.09
|
||||
${psql} <<< 'TRUNCATE TABLE session;'
|
||||
fi
|
||||
|
||||
${phpWithPspell}/bin/php ${cfg.package}/bin/update.sh
|
||||
'';
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
StateDirectory = "roundcube";
|
||||
User = if localDB then user else "nginx";
|
||||
# so that the des_key is not world readable
|
||||
StateDirectoryMode = "0700";
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
446
nixos/modules/services/mail/rspamd.nix
Normal file
446
nixos/modules/services/mail/rspamd.nix
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
{ config, options, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
|
||||
cfg = config.services.rspamd;
|
||||
opt = options.services.rspamd;
|
||||
postfixCfg = config.services.postfix;
|
||||
|
||||
bindSocketOpts = {options, config, ... }: {
|
||||
options = {
|
||||
socket = mkOption {
|
||||
type = types.str;
|
||||
example = "localhost:11333";
|
||||
description = ''
|
||||
Socket for this worker to listen on in a format acceptable by rspamd.
|
||||
'';
|
||||
};
|
||||
mode = mkOption {
|
||||
type = types.str;
|
||||
default = "0644";
|
||||
description = "Mode to set on unix socket";
|
||||
};
|
||||
owner = mkOption {
|
||||
type = types.str;
|
||||
default = "${cfg.user}";
|
||||
description = "Owner to set on unix socket";
|
||||
};
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "${cfg.group}";
|
||||
description = "Group to set on unix socket";
|
||||
};
|
||||
rawEntry = mkOption {
|
||||
type = types.str;
|
||||
internal = true;
|
||||
};
|
||||
};
|
||||
config.rawEntry = let
|
||||
maybeOption = option:
|
||||
optionalString options.${option}.isDefined " ${option}=${config.${option}}";
|
||||
in
|
||||
if (!(hasPrefix "/" config.socket)) then "${config.socket}"
|
||||
else "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}";
|
||||
};
|
||||
|
||||
traceWarning = w: x: builtins.trace "[1;31mwarning: ${w}[0m" x;
|
||||
|
||||
workerOpts = { name, options, ... }: {
|
||||
options = {
|
||||
enable = mkOption {
|
||||
type = types.nullOr types.bool;
|
||||
default = null;
|
||||
description = "Whether to run the rspamd worker.";
|
||||
};
|
||||
name = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = name;
|
||||
description = "Name of the worker";
|
||||
};
|
||||
type = mkOption {
|
||||
type = types.nullOr (types.enum [
|
||||
"normal" "controller" "fuzzy" "rspamd_proxy" "lua" "proxy"
|
||||
]);
|
||||
description = ''
|
||||
The type of this worker. The type <literal>proxy</literal> is
|
||||
deprecated and only kept for backwards compatibility and should be
|
||||
replaced with <literal>rspamd_proxy</literal>.
|
||||
'';
|
||||
apply = let
|
||||
from = "services.rspamd.workers.\"${name}\".type";
|
||||
files = options.type.files;
|
||||
warning = "The option `${from}` defined in ${showFiles files} has enum value `proxy` which has been renamed to `rspamd_proxy`";
|
||||
in x: if x == "proxy" then traceWarning warning "rspamd_proxy" else x;
|
||||
};
|
||||
bindSockets = mkOption {
|
||||
type = types.listOf (types.either types.str (types.submodule bindSocketOpts));
|
||||
default = [];
|
||||
description = ''
|
||||
List of sockets to listen, in format acceptable by rspamd
|
||||
'';
|
||||
example = [{
|
||||
socket = "/run/rspamd.sock";
|
||||
mode = "0666";
|
||||
owner = "rspamd";
|
||||
} "*:11333"];
|
||||
apply = value: map (each: if (isString each)
|
||||
then if (isUnixSocket each)
|
||||
then {socket = each; owner = cfg.user; group = cfg.group; mode = "0644"; rawEntry = "${each}";}
|
||||
else {socket = each; rawEntry = "${each}";}
|
||||
else each) value;
|
||||
};
|
||||
count = mkOption {
|
||||
type = types.nullOr types.int;
|
||||
default = null;
|
||||
description = ''
|
||||
Number of worker instances to run
|
||||
'';
|
||||
};
|
||||
includes = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = ''
|
||||
List of files to include in configuration
|
||||
'';
|
||||
};
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = "Additional entries to put verbatim into worker section of rspamd config file.";
|
||||
};
|
||||
};
|
||||
config = mkIf (name == "normal" || name == "controller" || name == "fuzzy" || name == "rspamd_proxy") {
|
||||
type = mkDefault name;
|
||||
includes = mkDefault [ "$CONFDIR/worker-${if name == "rspamd_proxy" then "proxy" else name}.inc" ];
|
||||
bindSockets =
|
||||
let
|
||||
unixSocket = name: {
|
||||
mode = "0660";
|
||||
socket = "/run/rspamd/${name}.sock";
|
||||
owner = cfg.user;
|
||||
group = cfg.group;
|
||||
};
|
||||
in mkDefault (if name == "normal" then [(unixSocket "rspamd")]
|
||||
else if name == "controller" then [ "localhost:11334" ]
|
||||
else if name == "rspamd_proxy" then [ (unixSocket "proxy") ]
|
||||
else [] );
|
||||
};
|
||||
};
|
||||
|
||||
isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket);
|
||||
|
||||
mkBindSockets = enabled: socks: concatStringsSep "\n "
|
||||
(flatten (map (each: "bind_socket = \"${each.rawEntry}\";") socks));
|
||||
|
||||
rspamdConfFile = pkgs.writeText "rspamd.conf"
|
||||
''
|
||||
.include "$CONFDIR/common.conf"
|
||||
|
||||
options {
|
||||
pidfile = "$RUNDIR/rspamd.pid";
|
||||
.include "$CONFDIR/options.inc"
|
||||
.include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/options.inc"
|
||||
.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/options.inc"
|
||||
}
|
||||
|
||||
logging {
|
||||
type = "syslog";
|
||||
.include "$CONFDIR/logging.inc"
|
||||
.include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/logging.inc"
|
||||
.include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/logging.inc"
|
||||
}
|
||||
|
||||
${concatStringsSep "\n" (mapAttrsToList (name: value: let
|
||||
includeName = if name == "rspamd_proxy" then "proxy" else name;
|
||||
tryOverride = boolToString (value.extraConfig == "");
|
||||
in ''
|
||||
worker "${value.type}" {
|
||||
type = "${value.type}";
|
||||
${optionalString (value.enable != null)
|
||||
"enabled = ${if value.enable != false then "yes" else "no"};"}
|
||||
${mkBindSockets value.enable value.bindSockets}
|
||||
${optionalString (value.count != null) "count = ${toString value.count};"}
|
||||
${concatStringsSep "\n " (map (each: ".include \"${each}\"") value.includes)}
|
||||
.include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/worker-${includeName}.inc"
|
||||
.include(try=${tryOverride}; priority=10) "$LOCAL_CONFDIR/override.d/worker-${includeName}.inc"
|
||||
}
|
||||
'') cfg.workers)}
|
||||
|
||||
${optionalString (cfg.extraConfig != "") ''
|
||||
.include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc"
|
||||
''}
|
||||
'';
|
||||
|
||||
filterFiles = files: filterAttrs (n: v: v.enable) files;
|
||||
rspamdDir = pkgs.linkFarm "etc-rspamd-dir" (
|
||||
(mapAttrsToList (name: file: { name = "local.d/${name}"; path = file.source; }) (filterFiles cfg.locals)) ++
|
||||
(mapAttrsToList (name: file: { name = "override.d/${name}"; path = file.source; }) (filterFiles cfg.overrides)) ++
|
||||
(optional (cfg.localLuaRules != null) { name = "rspamd.local.lua"; path = cfg.localLuaRules; }) ++
|
||||
[ { name = "rspamd.conf"; path = rspamdConfFile; } ]
|
||||
);
|
||||
|
||||
configFileModule = prefix: { name, config, ... }: {
|
||||
options = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether this file ${prefix} should be generated. This
|
||||
option allows specific ${prefix} files to be disabled.
|
||||
'';
|
||||
};
|
||||
|
||||
text = mkOption {
|
||||
default = null;
|
||||
type = types.nullOr types.lines;
|
||||
description = "Text of the file.";
|
||||
};
|
||||
|
||||
source = mkOption {
|
||||
type = types.path;
|
||||
description = "Path of the source file.";
|
||||
};
|
||||
};
|
||||
config = {
|
||||
source = mkIf (config.text != null) (
|
||||
let name' = "rspamd-${prefix}-" + baseNameOf name;
|
||||
in mkDefault (pkgs.writeText name' config.text));
|
||||
};
|
||||
};
|
||||
|
||||
configOverrides =
|
||||
(mapAttrs' (n: v: nameValuePair "worker-${if n == "rspamd_proxy" then "proxy" else n}.inc" {
|
||||
text = v.extraConfig;
|
||||
})
|
||||
(filterAttrs (n: v: v.extraConfig != "") cfg.workers))
|
||||
// (if cfg.extraConfig == "" then {} else {
|
||||
"extra-config.inc".text = cfg.extraConfig;
|
||||
});
|
||||
in
|
||||
|
||||
{
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.rspamd = {
|
||||
|
||||
enable = mkEnableOption "rspamd, the Rapid spam filtering system";
|
||||
|
||||
debug = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to run the rspamd daemon in debug mode.";
|
||||
};
|
||||
|
||||
locals = mkOption {
|
||||
type = with types; attrsOf (submodule (configFileModule "locals"));
|
||||
default = {};
|
||||
description = ''
|
||||
Local configuration files, written into <filename>/etc/rspamd/local.d/{name}</filename>.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{ "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
|
||||
"arc.conf".text = "allow_envfrom_empty = true;";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
overrides = mkOption {
|
||||
type = with types; attrsOf (submodule (configFileModule "overrides"));
|
||||
default = {};
|
||||
description = ''
|
||||
Overridden configuration files, written into <filename>/etc/rspamd/override.d/{name}</filename>.
|
||||
'';
|
||||
example = literalExpression ''
|
||||
{ "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
|
||||
"arc.conf".text = "allow_envfrom_empty = true;";
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
localLuaRules = mkOption {
|
||||
default = null;
|
||||
type = types.nullOr types.path;
|
||||
description = ''
|
||||
Path of file to link to <filename>/etc/rspamd/rspamd.local.lua</filename> for local
|
||||
rules written in Lua
|
||||
'';
|
||||
};
|
||||
|
||||
workers = mkOption {
|
||||
type = with types; attrsOf (submodule workerOpts);
|
||||
description = ''
|
||||
Attribute set of workers to start.
|
||||
'';
|
||||
default = {
|
||||
normal = {};
|
||||
controller = {};
|
||||
};
|
||||
example = literalExpression ''
|
||||
{
|
||||
normal = {
|
||||
includes = [ "$CONFDIR/worker-normal.inc" ];
|
||||
bindSockets = [{
|
||||
socket = "/run/rspamd/rspamd.sock";
|
||||
mode = "0660";
|
||||
owner = "''${config.${opt.user}}";
|
||||
group = "''${config.${opt.group}}";
|
||||
}];
|
||||
};
|
||||
controller = {
|
||||
includes = [ "$CONFDIR/worker-controller.inc" ];
|
||||
bindSockets = [ "[::1]:11334" ];
|
||||
};
|
||||
}
|
||||
'';
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = ''
|
||||
Extra configuration to add at the end of the rspamd configuration
|
||||
file.
|
||||
'';
|
||||
};
|
||||
|
||||
user = mkOption {
|
||||
type = types.str;
|
||||
default = "rspamd";
|
||||
description = ''
|
||||
User to use when no root privileges are required.
|
||||
'';
|
||||
};
|
||||
|
||||
group = mkOption {
|
||||
type = types.str;
|
||||
default = "rspamd";
|
||||
description = ''
|
||||
Group to use when no root privileges are required.
|
||||
'';
|
||||
};
|
||||
|
||||
postfix = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Add rspamd milter to postfix main.conf";
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = with types; attrsOf (oneOf [ bool str (listOf str) ]);
|
||||
description = ''
|
||||
Addon to postfix configuration
|
||||
'';
|
||||
default = {
|
||||
smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
|
||||
non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
services.rspamd.overrides = configOverrides;
|
||||
services.rspamd.workers = mkIf cfg.postfix.enable {
|
||||
controller = {};
|
||||
rspamd_proxy = {
|
||||
bindSockets = [ {
|
||||
mode = "0660";
|
||||
socket = "/run/rspamd/rspamd-milter.sock";
|
||||
owner = cfg.user;
|
||||
group = postfixCfg.group;
|
||||
} ];
|
||||
extraConfig = ''
|
||||
upstream "local" {
|
||||
default = yes; # Self-scan upstreams are always default
|
||||
self_scan = yes; # Enable self-scan
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config;
|
||||
|
||||
systemd.services.postfix = mkIf cfg.postfix.enable {
|
||||
serviceConfig.SupplementaryGroups = [ postfixCfg.group ];
|
||||
};
|
||||
|
||||
# Allow users to run 'rspamc' and 'rspamadm'.
|
||||
environment.systemPackages = [ pkgs.rspamd ];
|
||||
|
||||
users.users.${cfg.user} = {
|
||||
description = "rspamd daemon";
|
||||
uid = config.ids.uids.rspamd;
|
||||
group = cfg.group;
|
||||
};
|
||||
|
||||
users.groups.${cfg.group} = {
|
||||
gid = config.ids.gids.rspamd;
|
||||
};
|
||||
|
||||
environment.etc.rspamd.source = rspamdDir;
|
||||
|
||||
systemd.services.rspamd = {
|
||||
description = "Rspamd Service";
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
after = [ "network.target" ];
|
||||
restartTriggers = [ rspamdDir ];
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} -c /etc/rspamd/rspamd.conf -f";
|
||||
Restart = "always";
|
||||
|
||||
User = "${cfg.user}";
|
||||
Group = "${cfg.group}";
|
||||
SupplementaryGroups = mkIf cfg.postfix.enable [ postfixCfg.group ];
|
||||
|
||||
RuntimeDirectory = "rspamd";
|
||||
RuntimeDirectoryMode = "0755";
|
||||
StateDirectory = "rspamd";
|
||||
StateDirectoryMode = "0700";
|
||||
|
||||
AmbientCapabilities = [];
|
||||
CapabilityBoundingSet = "";
|
||||
DevicePolicy = "closed";
|
||||
LockPersonality = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = true;
|
||||
# we need to chown socket to rspamd-milter
|
||||
PrivateUsers = !cfg.postfix.enable;
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = true;
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectSystem = "strict";
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" "AF_UNIX" ];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = "@system-service";
|
||||
UMask = "0077";
|
||||
};
|
||||
};
|
||||
};
|
||||
imports = [
|
||||
(mkRemovedOptionModule [ "services" "rspamd" "socketActivation" ]
|
||||
"Socket activation never worked correctly and could at this time not be fixed and so was removed")
|
||||
(mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ])
|
||||
(mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ])
|
||||
(mkRemovedOptionModule [ "services" "rmilter" ] "Use services.rspamd.* instead to set up milter service")
|
||||
];
|
||||
}
|
||||
135
nixos/modules/services/mail/rss2email.nix
Normal file
135
nixos/modules/services/mail/rss2email.nix
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.rss2email;
|
||||
in {
|
||||
|
||||
###### interface
|
||||
|
||||
options = {
|
||||
|
||||
services.rss2email = {
|
||||
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to enable rss2email.";
|
||||
};
|
||||
|
||||
to = mkOption {
|
||||
type = types.str;
|
||||
description = "Mail address to which to send emails";
|
||||
};
|
||||
|
||||
interval = mkOption {
|
||||
type = types.str;
|
||||
default = "12h";
|
||||
description = "How often to check the feeds, in systemd interval format";
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = with types; attrsOf (oneOf [ str int bool ]);
|
||||
default = {};
|
||||
description = ''
|
||||
The configuration to give rss2email.
|
||||
|
||||
Default will use system-wide <literal>sendmail</literal> to send the
|
||||
email. This is rss2email's default when running
|
||||
<literal>r2e new</literal>.
|
||||
|
||||
This set contains key-value associations that will be set in the
|
||||
<literal>[DEFAULT]</literal> block along with the
|
||||
<literal>to</literal> parameter.
|
||||
|
||||
See <literal>man r2e</literal> for more information on which
|
||||
parameters are accepted.
|
||||
'';
|
||||
};
|
||||
|
||||
feeds = mkOption {
|
||||
description = "The feeds to watch.";
|
||||
type = types.attrsOf (types.submodule {
|
||||
options = {
|
||||
url = mkOption {
|
||||
type = types.str;
|
||||
description = "The URL at which to fetch the feed.";
|
||||
};
|
||||
|
||||
to = mkOption {
|
||||
type = with types; nullOr str;
|
||||
default = null;
|
||||
description = ''
|
||||
Email address to which to send feed items.
|
||||
|
||||
If <literal>null</literal>, this will not be set in the
|
||||
configuration file, and rss2email will make it default to
|
||||
<literal>rss2email.to</literal>.
|
||||
'';
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
|
||||
###### implementation
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
users.groups = {
|
||||
rss2email.gid = config.ids.gids.rss2email;
|
||||
};
|
||||
|
||||
users.users = {
|
||||
rss2email = {
|
||||
description = "rss2email user";
|
||||
uid = config.ids.uids.rss2email;
|
||||
group = "rss2email";
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [ rss2email ];
|
||||
|
||||
services.rss2email.config.to = cfg.to;
|
||||
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/rss2email 0700 rss2email rss2email - -"
|
||||
];
|
||||
|
||||
systemd.services.rss2email = let
|
||||
conf = pkgs.writeText "rss2email.cfg" (lib.generators.toINI {} ({
|
||||
DEFAULT = cfg.config;
|
||||
} // lib.mapAttrs' (name: feed: nameValuePair "feed.${name}" (
|
||||
{ inherit (feed) url; } //
|
||||
lib.optionalAttrs (feed.to != null) { inherit (feed) to; }
|
||||
)) cfg.feeds
|
||||
));
|
||||
in
|
||||
{
|
||||
preStart = ''
|
||||
cp ${conf} /var/rss2email/conf.cfg
|
||||
if [ ! -f /var/rss2email/db.json ]; then
|
||||
echo '{"version":2,"feeds":[]}' > /var/rss2email/db.json
|
||||
fi
|
||||
'';
|
||||
path = [ pkgs.system-sendmail ];
|
||||
serviceConfig = {
|
||||
ExecStart =
|
||||
"${pkgs.rss2email}/bin/r2e -c /var/rss2email/conf.cfg -d /var/rss2email/db.json run";
|
||||
User = "rss2email";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.timers.rss2email = {
|
||||
partOf = [ "rss2email.service" ];
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig.OnBootSec = "0";
|
||||
timerConfig.OnUnitActiveSec = cfg.interval;
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ ekleog ];
|
||||
}
|
||||
191
nixos/modules/services/mail/spamassassin.nix
Normal file
191
nixos/modules/services/mail/spamassassin.nix
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
|
||||
with lib;
|
||||
|
||||
let
|
||||
cfg = config.services.spamassassin;
|
||||
spamassassin-local-cf = pkgs.writeText "local.cf" cfg.config;
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options = {
|
||||
|
||||
services.spamassassin = {
|
||||
enable = mkEnableOption "the SpamAssassin daemon";
|
||||
|
||||
debug = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to run the SpamAssassin daemon in debug mode";
|
||||
};
|
||||
|
||||
config = mkOption {
|
||||
type = types.lines;
|
||||
description = ''
|
||||
The SpamAssassin local.cf config
|
||||
|
||||
If you are using this configuration:
|
||||
add_header all Status _YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_
|
||||
|
||||
Then you can Use this sieve filter:
|
||||
require ["fileinto", "reject", "envelope"];
|
||||
|
||||
if header :contains "X-Spam-Flag" "YES" {
|
||||
fileinto "spam";
|
||||
}
|
||||
|
||||
Or this procmail filter:
|
||||
:0:
|
||||
* ^X-Spam-Flag: YES
|
||||
/var/vpopmail/domains/lastlog.de/js/.maildir/.spam/new
|
||||
|
||||
To filter your messages based on the additional mail headers added by spamassassin.
|
||||
'';
|
||||
example = ''
|
||||
#rewrite_header Subject [***** SPAM _SCORE_ *****]
|
||||
required_score 5.0
|
||||
use_bayes 1
|
||||
bayes_auto_learn 1
|
||||
add_header all Status _YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_
|
||||
'';
|
||||
default = "";
|
||||
};
|
||||
|
||||
initPreConf = mkOption {
|
||||
type = with types; either str path;
|
||||
description = "The SpamAssassin init.pre config.";
|
||||
apply = val: if builtins.isPath val then val else pkgs.writeText "init.pre" val;
|
||||
default =
|
||||
''
|
||||
#
|
||||
# to update this list, run this command in the rules directory:
|
||||
# grep 'loadplugin.*Mail::SpamAssassin::Plugin::.*' -o -h * | sort | uniq
|
||||
#
|
||||
|
||||
#loadplugin Mail::SpamAssassin::Plugin::AccessDB
|
||||
#loadplugin Mail::SpamAssassin::Plugin::AntiVirus
|
||||
loadplugin Mail::SpamAssassin::Plugin::AskDNS
|
||||
# loadplugin Mail::SpamAssassin::Plugin::ASN
|
||||
loadplugin Mail::SpamAssassin::Plugin::AutoLearnThreshold
|
||||
#loadplugin Mail::SpamAssassin::Plugin::AWL
|
||||
loadplugin Mail::SpamAssassin::Plugin::Bayes
|
||||
loadplugin Mail::SpamAssassin::Plugin::BodyEval
|
||||
loadplugin Mail::SpamAssassin::Plugin::Check
|
||||
#loadplugin Mail::SpamAssassin::Plugin::DCC
|
||||
loadplugin Mail::SpamAssassin::Plugin::DKIM
|
||||
loadplugin Mail::SpamAssassin::Plugin::DNSEval
|
||||
loadplugin Mail::SpamAssassin::Plugin::FreeMail
|
||||
loadplugin Mail::SpamAssassin::Plugin::Hashcash
|
||||
loadplugin Mail::SpamAssassin::Plugin::HeaderEval
|
||||
loadplugin Mail::SpamAssassin::Plugin::HTMLEval
|
||||
loadplugin Mail::SpamAssassin::Plugin::HTTPSMismatch
|
||||
loadplugin Mail::SpamAssassin::Plugin::ImageInfo
|
||||
loadplugin Mail::SpamAssassin::Plugin::MIMEEval
|
||||
loadplugin Mail::SpamAssassin::Plugin::MIMEHeader
|
||||
# loadplugin Mail::SpamAssassin::Plugin::PDFInfo
|
||||
#loadplugin Mail::SpamAssassin::Plugin::PhishTag
|
||||
loadplugin Mail::SpamAssassin::Plugin::Pyzor
|
||||
loadplugin Mail::SpamAssassin::Plugin::Razor2
|
||||
# loadplugin Mail::SpamAssassin::Plugin::RelayCountry
|
||||
loadplugin Mail::SpamAssassin::Plugin::RelayEval
|
||||
loadplugin Mail::SpamAssassin::Plugin::ReplaceTags
|
||||
# loadplugin Mail::SpamAssassin::Plugin::Rule2XSBody
|
||||
# loadplugin Mail::SpamAssassin::Plugin::Shortcircuit
|
||||
loadplugin Mail::SpamAssassin::Plugin::SpamCop
|
||||
loadplugin Mail::SpamAssassin::Plugin::SPF
|
||||
#loadplugin Mail::SpamAssassin::Plugin::TextCat
|
||||
# loadplugin Mail::SpamAssassin::Plugin::TxRep
|
||||
loadplugin Mail::SpamAssassin::Plugin::URIDetail
|
||||
loadplugin Mail::SpamAssassin::Plugin::URIDNSBL
|
||||
loadplugin Mail::SpamAssassin::Plugin::URIEval
|
||||
# loadplugin Mail::SpamAssassin::Plugin::URILocalBL
|
||||
loadplugin Mail::SpamAssassin::Plugin::VBounce
|
||||
loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject
|
||||
loadplugin Mail::SpamAssassin::Plugin::WLBLEval
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
environment.etc."mail/spamassassin/init.pre".source = cfg.initPreConf;
|
||||
environment.etc."mail/spamassassin/local.cf".source = spamassassin-local-cf;
|
||||
|
||||
# Allow users to run 'spamc'.
|
||||
environment.systemPackages = [ pkgs.spamassassin ];
|
||||
|
||||
users.users.spamd = {
|
||||
description = "Spam Assassin Daemon";
|
||||
uid = config.ids.uids.spamd;
|
||||
group = "spamd";
|
||||
};
|
||||
|
||||
users.groups.spamd = {
|
||||
gid = config.ids.gids.spamd;
|
||||
};
|
||||
|
||||
systemd.services.sa-update = {
|
||||
# Needs to be able to contact the update server.
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "spamd";
|
||||
Group = "spamd";
|
||||
StateDirectory = "spamassassin";
|
||||
ExecStartPost = "+${config.systemd.package}/bin/systemctl -q --no-block try-reload-or-restart spamd.service";
|
||||
};
|
||||
|
||||
script = ''
|
||||
set +e
|
||||
${pkgs.spamassassin}/bin/sa-update --verbose --gpghomedir=/var/lib/spamassassin/sa-update-keys/
|
||||
rc=$?
|
||||
set -e
|
||||
|
||||
if [[ $rc -gt 1 ]]; then
|
||||
# sa-update failed.
|
||||
exit $rc
|
||||
fi
|
||||
|
||||
if [[ $rc -eq 1 ]]; then
|
||||
# No update was available, exit successfully.
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# An update was available and installed. Compile the rules.
|
||||
${pkgs.spamassassin}/bin/sa-compile
|
||||
'';
|
||||
};
|
||||
|
||||
systemd.timers.sa-update = {
|
||||
description = "sa-update-service";
|
||||
partOf = [ "sa-update.service" ];
|
||||
wantedBy = [ "timers.target" ];
|
||||
timerConfig = {
|
||||
OnCalendar = "1:*";
|
||||
Persistent = true;
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services.spamd = {
|
||||
description = "SpamAssassin Server";
|
||||
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
wants = [ "sa-update.service" ];
|
||||
after = [
|
||||
"network.target"
|
||||
"sa-update.service"
|
||||
];
|
||||
|
||||
serviceConfig = {
|
||||
User = "spamd";
|
||||
Group = "spamd";
|
||||
ExecStart = "+${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --virtual-config-dir=%S/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid";
|
||||
ExecReload = "+${pkgs.coreutils}/bin/kill -HUP $MAINPID";
|
||||
StateDirectory = "spamassassin";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
590
nixos/modules/services/mail/sympa.nix
Normal file
590
nixos/modules/services/mail/sympa.nix
Normal file
|
|
@ -0,0 +1,590 @@
|
|||
{ 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 = literalExpression ''
|
||||
{
|
||||
"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 = literalExpression ''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 = literalExpression ''
|
||||
{
|
||||
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 = literalExpression ''
|
||||
{
|
||||
"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;
|
||||
'';
|
||||
}) // {
|
||||
"/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 ];
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue