postfix: complete remake of postfix service (#27276)

This commit is contained in:
Joachim Schiele 2017-07-14 16:55:53 +02:00 committed by GitHub
parent 8a35f751d1
commit af7c7b42c1

View File

@ -9,7 +9,8 @@ let
group = cfg.group; group = cfg.group;
setgidGroup = cfg.setgidGroup; setgidGroup = cfg.setgidGroup;
haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != "" || cfg.extraAliases != ""; haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != ""
|| cfg.extraAliases != "";
haveTransport = cfg.transport != ""; haveTransport = cfg.transport != "";
haveVirtual = cfg.virtual != ""; haveVirtual = cfg.virtual != "";
@ -25,149 +26,275 @@ let
clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl); clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl);
mainCf = mainCf = let
'' escape = replaceStrings ["$"] ["$$"];
compatibility_level = 9999 mkList = items: "\n " + concatMapStringsSep "\n " escape 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 (recursiveUpdate defaultConf cfg.config))
+ "\n" + cfg.extraConfig;
mail_owner = ${user} defaultConf = {
default_privs = nobody compatibility_level = "9999";
mail_owner = user;
default_privs = "nobody";
# NixOS specific locations # NixOS specific locations
data_directory = /var/lib/postfix/data data_directory = "/var/lib/postfix/data";
queue_directory = /var/lib/postfix/queue queue_directory = "/var/lib/postfix/queue";
# Default location of everything in package # Default location of everything in package
meta_directory = ${pkgs.postfix}/etc/postfix meta_directory = "${pkgs.postfix}/etc/postfix";
command_directory = ${pkgs.postfix}/bin command_directory = "${pkgs.postfix}/bin";
sample_directory = /etc/postfix sample_directory = "/etc/postfix";
newaliases_path = ${pkgs.postfix}/bin/newaliases newaliases_path = "${pkgs.postfix}/bin/newaliases";
mailq_path = ${pkgs.postfix}/bin/mailq mailq_path = "${pkgs.postfix}/bin/mailq";
readme_directory = no readme_directory = false;
sendmail_path = ${pkgs.postfix}/bin/sendmail sendmail_path = "${pkgs.postfix}/bin/sendmail";
daemon_directory = ${pkgs.postfix}/libexec/postfix daemon_directory = "${pkgs.postfix}/libexec/postfix";
manpage_directory = ${pkgs.postfix}/share/man manpage_directory = "${pkgs.postfix}/share/man";
html_directory = ${pkgs.postfix}/share/postfix/doc/html html_directory = "${pkgs.postfix}/share/postfix/doc/html";
shlib_directory = no shlib_directory = false;
relayhost = if cfg.lookupMX || cfg.relayHost == ""
then cfg.relayHost
else "[${cfg.relayHost}]";
mail_spool_directory = "/var/spool/mail/";
setgid_group = setgidGroup;
}
// optionalAttrs config.networking.enableIPv6 { inet_protocols = "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/postfx/transport"; }
// optionalAttrs haveVirtual { virtual_alias_maps = "${cfg.virtualMapType}:/etc/postfix/virtual"; }
// optionalAttrs (cfg.dnsBlacklists != []) { smtpd_client_restrictions = clientRestrictions; }
// optionalAttrs cfg.enableHeaderChecks { header_checks = "regexp:/etc/postfix/header_checks"; }
// optionalAttrs (cfg.sslCert != "") {
smtp_tls_CAfile = cfg.sslCACert;
smtp_tls_cert_file = cfg.sslCert;
smtp_tls_key_file = cfg.sslKey;
'' smtp_use_tls = true;
+ optionalString config.networking.enableIPv6 ''
inet_protocols = all
''
+ (if cfg.networks != null then
''
mynetworks = ${concatStringsSep ", " cfg.networks}
''
else if cfg.networksStyle != "" then
''
mynetworks_style = ${cfg.networksStyle}
''
else
"")
+ optionalString (cfg.hostname != "") ''
myhostname = ${cfg.hostname}
''
+ optionalString (cfg.domain != "") ''
mydomain = ${cfg.domain}
''
+ optionalString (cfg.origin != "") ''
myorigin = ${cfg.origin}
''
+ optionalString (cfg.destination != null) ''
mydestination = ${concatStringsSep ", " cfg.destination}
''
+ optionalString (cfg.relayDomains != null) ''
relay_domains = ${concatStringsSep ", " cfg.relayDomains}
''
+ ''
relayhost = ${if cfg.lookupMX || cfg.relayHost == "" then
cfg.relayHost
else
"[" + cfg.relayHost + "]"}
mail_spool_directory = /var/spool/mail/ smtpd_tls_CAfile = cfg.sslCACert;
smtpd_tls_cert_file = cfg.sslCert;
smtpd_tls_key_file = cfg.sslKey;
setgid_group = ${setgidGroup} smtpd_use_tls = true;
'' };
+ optionalString (cfg.sslCert != "") ''
smtp_tls_CAfile = ${cfg.sslCACert} masterCfOptions = { options, config, name, ... }: {
smtp_tls_cert_file = ${cfg.sslCert} options = {
smtp_tls_key_file = ${cfg.sslKey} name = mkOption {
type = types.str;
default = name;
example = "smtp";
description = ''
The name of the service to run. Defaults to the attribute set key.
'';
};
smtp_use_tls = yes type = mkOption {
type = types.enum [ "inet" "unix" "fifo" "pass" ];
default = "unix";
example = "inet";
description = "The type of the service";
};
smtpd_tls_CAfile = ${cfg.sslCACert} private = mkOption {
smtpd_tls_cert_file = ${cfg.sslCert} type = types.bool;
smtpd_tls_key_file = ${cfg.sslKey} 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>.
'';
};
smtpd_use_tls = yes privileged = mkOption {
'' type = types.bool;
+ optionalString (cfg.recipientDelimiter != "") '' example = true;
recipient_delimiter = ${cfg.recipientDelimiter} description = "";
'' };
+ optionalString haveAliases ''
alias_maps = hash:/etc/postfix/aliases
''
+ optionalString haveTransport ''
transport_maps = hash:/etc/postfix/transport
''
+ optionalString haveVirtual ''
virtual_alias_maps = hash:/etc/postfix/virtual
''
+ optionalString (cfg.dnsBlacklists != []) ''
smtpd_client_restrictions = ${clientRestrictions}
''
+ cfg.extraConfig;
masterCf = '' chroot = mkOption {
# ========================================================================== type = types.bool;
# service type private unpriv chroot wakeup maxproc command + args example = true;
# (yes) (yes) (no) (never) (100) description = ''
# ========================================================================== Whether the service is chrooted to have only access to the
smtp inet n - n - - smtpd <option>services.postfix.queueDir</option> and the closure of
'' + optionalString cfg.enableSubmission '' store paths specified by the <option>program</option> option.
submission inet n - n - - smtpd '';
${concatStringsSep "\n " (mapAttrsToList (x: y: "-o " + x + "=" + y) cfg.submissionOptions)} };
''
+ ''
pickup unix n - n 60 1 pickup
cleanup unix n - n - 0 cleanup
qmgr unix n - n 300 1 qmgr
tlsmgr unix - - n 1000? 1 tlsmgr
rewrite unix - - n - - trivial-rewrite
bounce unix - - n - 0 bounce
defer unix - - n - 0 bounce
trace unix - - n - 0 bounce
verify unix - - n - 1 verify
flush unix n - n 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
''
+ optionalString cfg.enableSmtp ''
smtp unix - - n - - smtp
relay unix - - n - - smtp
-o smtp_fallback_relay=
# -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
''
+ ''
showq unix n - n - - showq
error unix - - n - - error
retry unix - - n - - error
discard unix - - n - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - n - - lmtp
anvil unix - - n - 1 anvil
scache unix - - n - 1 scache
${cfg.extraMasterConf}
'';
aliases = 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 (!config.wakeupUnusedComponent) "?";
in if wakeupDefined && wakeupUCDefined 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 fold 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 = fold (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";
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 != "") '' optionalString (cfg.postmasterAlias != "") ''
postmaster: ${cfg.postmasterAlias} postmaster${seperator} ${cfg.postmasterAlias}
'' ''
+ optionalString (cfg.rootAlias != "") '' + optionalString (cfg.rootAlias != "") ''
root: ${cfg.rootAlias} root${seperator} ${cfg.rootAlias}
'' ''
+ cfg.extraAliases + cfg.extraAliases
; ;
@ -176,8 +303,9 @@ let
virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual; virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual;
checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides; checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides;
mainCfFile = pkgs.writeText "postfix-main.cf" mainCf; mainCfFile = pkgs.writeText "postfix-main.cf" mainCf;
masterCfFile = pkgs.writeText "postfix-master.cf" masterCf; masterCfFile = pkgs.writeText "postfix-master.cf" masterCfContent;
transportFile = pkgs.writeText "postfix-transport" cfg.transport; transportFile = pkgs.writeText "postfix-transport" cfg.transport;
headerChecksFile = pkgs.writeText "postfix-header-checks" headerChecks;
in in
@ -203,23 +331,25 @@ in
enableSubmission = mkOption { enableSubmission = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
description = "Whether to enable smtp submission"; description = "Whether to enable smtp submission.";
}; };
submissionOptions = mkOption { submissionOptions = mkOption {
type = types.attrs; type = types.attrs;
default = { "smtpd_tls_security_level" = "encrypt"; default = {
"smtpd_sasl_auth_enable" = "yes"; smtpd_tls_security_level = "encrypt";
"smtpd_client_restrictions" = "permit_sasl_authenticated,reject"; smtpd_sasl_auth_enable = "yes";
"milter_macro_daemon_name" = "ORIGINATING"; 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"; description = "Options for the submission config in master.cf";
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";
};
}; };
setSendmail = mkOption { setSendmail = mkOption {
@ -352,6 +482,25 @@ in
"; ";
}; };
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 (either bool (either str (listOf str)));
default = defaultConf;
description = ''
The main.cf configuration file as key value set.
'';
example = {
mail_owner = "postfix";
smtp_use_tls = true;
};
};
extraConfig = mkOption { extraConfig = mkOption {
type = types.lines; type = types.lines;
default = ""; default = "";
@ -395,6 +544,14 @@ in
"; ";
}; };
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.
'';
};
transport = mkOption { transport = mkOption {
default = ""; default = "";
description = " description = "
@ -413,6 +570,22 @@ in
description = "contents of check_client_access for overriding dnsBlacklists"; 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 { extraMasterConf = mkOption {
type = types.lines; type = types.lines;
default = ""; default = "";
@ -420,6 +593,27 @@ in
description = "Extra lines to append to the generated master.cf file."; 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 { aliasFiles = mkOption {
type = types.attrsOf types.path; type = types.attrsOf types.path;
default = {}; default = {};
@ -530,6 +724,101 @@ in
${pkgs.postfix}/bin/postfix set-permissions config_directory=/var/lib/postfix/conf ${pkgs.postfix}/bin/postfix set-permissions config_directory=/var/lib/postfix/conf
''; '';
}; };
services.postfix.masterConfig = {
smtp_inet = {
name = "smtp";
type = "inet";
private = false;
command = "smtpd";
};
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 = {};
relay = {
command = "smtp";
args = [ "-o" "smtp_fallback_relay=" ];
};
};
} }
(mkIf haveAliases { (mkIf haveAliases {
@ -541,9 +830,14 @@ in
(mkIf haveVirtual { (mkIf haveVirtual {
services.postfix.mapFiles."virtual" = virtualFile; services.postfix.mapFiles."virtual" = virtualFile;
}) })
(mkIf cfg.enableHeaderChecks {
services.postfix.mapFiles."header_checks" = headerChecksFile;
})
(mkIf (cfg.dnsBlacklists != []) { (mkIf (cfg.dnsBlacklists != []) {
services.postfix.mapFiles."client_access" = checkClientAccessFile; services.postfix.mapFiles."client_access" = checkClientAccessFile;
}) })
(mkIf (cfg.extraConfig != "") {
warnings = [ "The services.postfix.extraConfig option was deprecated. Please use services.postfix.config instead." ];
})
]); ]);
} }