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.nix988
1 files changed, 988 insertions, 0 deletions
diff --git a/nixos/modules/services/mail/postfix.nix b/nixos/modules/services/mail/postfix.nix
new file mode 100644
index 00000000000..23d3574ae27
--- /dev/null
+++ b/nixos/modules/services/mail/postfix.nix
@@ -0,0 +1,988 @@
+{ 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 =
+        { description = "Postfix mail server";
+
+          wantedBy = [ "multi-user.target" ];
+          after = [ "network.target" ];
+          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";
+          };
+
+          preStart = ''
+            # 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
+          '';
+        };
+
+      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"))
+  ];
+}