summaryrefslogtreecommitdiffstats
path: root/nixos
diff options
context:
space:
mode:
authorJoachim Schiele <js@lastlog.de>2017-07-14 16:55:53 +0200
committerGitHub <noreply@github.com>2017-07-14 16:55:53 +0200
commitaf7c7b42c137b65a6d12de63544cad400de636f5 (patch)
treee1d8a4417879c95e6153f173a273917310f14dfc /nixos
parent8a35f751d1f20ba73cd4c478c6dfbc46db1ced34 (diff)
postfix: complete remake of postfix service (#27276)
Diffstat (limited to 'nixos')
-rw-r--r--nixos/modules/services/mail/postfix.nix592
1 files changed, 443 insertions, 149 deletions
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index caaa87b94d61..845c6acc7feb 100644
--- a/nixos/modules/services/mail/postfix.nix
+++ b/nixos/modules/services/mail/postfix.nix
@@ -9,7 +9,8 @@ let
group = cfg.group;
setgidGroup = cfg.setgidGroup;
- haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != "" || cfg.extraAliases != "";
+ haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != ""
+ || cfg.extraAliases != "";
haveTransport = cfg.transport != "";
haveVirtual = cfg.virtual != "";
@@ -25,149 +26,275 @@ let
clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl);
- mainCf =
- ''
- compatibility_level = 9999
-
- mail_owner = ${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 = no
- 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 = no
+ mainCf = let
+ escape = replaceStrings ["$"] ["$$"];
+ 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;
+
+ defaultConf = {
+ compatibility_level = "9999";
+ mail_owner = 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;
+ 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;
+
+ smtpd_tls_CAfile = cfg.sslCACert;
+ smtpd_tls_cert_file = cfg.sslCert;
+ smtpd_tls_key_file = cfg.sslKey;
+
+ smtpd_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 + "]"}
+ 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.
+ '';
+ };
- mail_spool_directory = /var/spool/mail/
+ type = mkOption {
+ type = types.enum [ "inet" "unix" "fifo" "pass" ];
+ default = "unix";
+ example = "inet";
+ description = "The type of the service";
+ };
- setgid_group = ${setgidGroup}
- ''
- + optionalString (cfg.sslCert != "") ''
+ 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>.
+ '';
+ };
- smtp_tls_CAfile = ${cfg.sslCACert}
- smtp_tls_cert_file = ${cfg.sslCert}
- smtp_tls_key_file = ${cfg.sslKey}
+ privileged = mkOption {
+ type = types.bool;
+ example = true;
+ description = "";
+ };
- smtp_use_tls = yes
+ 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.
+ '';
+ };
- smtpd_tls_CAfile = ${cfg.sslCACert}
- smtpd_tls_cert_file = ${cfg.sslCert}
- smtpd_tls_key_file = ${cfg.sslKey}
+ 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.
+ '';
+ };
- smtpd_use_tls = yes
- ''
- + optionalString (cfg.recipientDelimiter != "") ''
- recipient_delimiter = ${cfg.recipientDelimiter}
- ''
- + 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 = ''
- # ==========================================================================
- # service type private unpriv chroot wakeup maxproc command + args
- # (yes) (yes) (no) (never) (100)
- # ==========================================================================
- smtp inet n - n - - smtpd
- '' + optionalString cfg.enableSubmission ''
- 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 =
+ 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 != "") ''
- postmaster: ${cfg.postmasterAlias}
+ postmaster${seperator} ${cfg.postmasterAlias}
''
+ optionalString (cfg.rootAlias != "") ''
- root: ${cfg.rootAlias}
+ root${seperator} ${cfg.rootAlias}
''
+ cfg.extraAliases
;
@@ -176,8 +303,9 @@ let
virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual;
checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides;
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;
+ headerChecksFile = pkgs.writeText "postfix-header-checks" headerChecks;
in
@@ -199,27 +327,29 @@ in
default = true;
description = "Whether to enable smtp in master.cf.";
};
-
+
enableSubmission = mkOption {
type = types.bool;
default = false;
- description = "Whether to enable smtp submission";
+ description = "Whether to enable smtp submission.";
};
submissionOptions = mkOption {
type = types.attrs;
- default = { "smtpd_tls_security_level" = "encrypt";
- "smtpd_sasl_auth_enable" = "yes";
- "smtpd_client_restrictions" = "permit_sasl_authenticated,reject";
- "milter_macro_daemon_name" = "ORIGINATING";
- };
+ 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";
- 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 {
@@ -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 {
type = types.lines;
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 {
default = "";
description = "
@@ -413,6 +570,22 @@ in
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 = "";
@@ -420,6 +593,27 @@ in
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 = {};
@@ -530,6 +724,101 @@ in
${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 {
@@ -541,9 +830,14 @@ in
(mkIf haveVirtual {
services.postfix.mapFiles."virtual" = virtualFile;
})
+ (mkIf cfg.enableHeaderChecks {
+ services.postfix.mapFiles."header_checks" = headerChecksFile;
+ })
(mkIf (cfg.dnsBlacklists != []) {
services.postfix.mapFiles."client_access" = checkClientAccessFile;
})
+ (mkIf (cfg.extraConfig != "") {
+ warnings = [ "The services.postfix.extraConfig option was deprecated. Please use services.postfix.config instead." ];
+ })
]);
-
}