about summary refs log tree commit diff
path: root/nixos/modules/services/mail/postfix.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixos/modules/services/mail/postfix.nix')
-rw-r--r--nixos/modules/services/mail/postfix.nix595
1 files changed, 446 insertions, 149 deletions
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
index caaa87b94d61..01ae49d49090 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/postfix/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 (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 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" + 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: ${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,17 @@ 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." ];
+    })
+    (mkIf (cfg.extraMasterConf != "") {
+      warnings = [ "The services.postfix.extraMasterConf option was deprecated. Please use services.postfix.masterConfig instead." ];
+    })
   ]);
-
 }