about summary refs log tree commit diff
path: root/nixpkgs/nixos/modules/services/mail
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/nixos/modules/services/mail')
-rw-r--r--nixpkgs/nixos/modules/services/mail/clamsmtp.nix181
-rw-r--r--nixpkgs/nixos/modules/services/mail/davmail.nix99
-rw-r--r--nixpkgs/nixos/modules/services/mail/dkimproxy-out.nix120
-rw-r--r--nixpkgs/nixos/modules/services/mail/dovecot.nix401
-rw-r--r--nixpkgs/nixos/modules/services/mail/dspam.nix150
-rw-r--r--nixpkgs/nixos/modules/services/mail/exim.nix122
-rw-r--r--nixpkgs/nixos/modules/services/mail/freepops.nix89
-rw-r--r--nixpkgs/nixos/modules/services/mail/mail.nix33
-rw-r--r--nixpkgs/nixos/modules/services/mail/mailcatcher.nix60
-rw-r--r--nixpkgs/nixos/modules/services/mail/mailhog.nix43
-rw-r--r--nixpkgs/nixos/modules/services/mail/mlmmj.nix156
-rw-r--r--nixpkgs/nixos/modules/services/mail/nullmailer.nix246
-rw-r--r--nixpkgs/nixos/modules/services/mail/offlineimap.nix72
-rw-r--r--nixpkgs/nixos/modules/services/mail/opendkim.nix133
-rw-r--r--nixpkgs/nixos/modules/services/mail/opensmtpd.nix131
-rw-r--r--nixpkgs/nixos/modules/services/mail/pfix-srsd.nix56
-rw-r--r--nixpkgs/nixos/modules/services/mail/postfix.nix898
-rw-r--r--nixpkgs/nixos/modules/services/mail/postgrey.nix194
-rw-r--r--nixpkgs/nixos/modules/services/mail/postsrsd.nix135
-rw-r--r--nixpkgs/nixos/modules/services/mail/rmilter.nix252
-rw-r--r--nixpkgs/nixos/modules/services/mail/roundcube.nix171
-rw-r--r--nixpkgs/nixos/modules/services/mail/rspamd.nix418
-rw-r--r--nixpkgs/nixos/modules/services/mail/rss2email.nix134
-rw-r--r--nixpkgs/nixos/modules/services/mail/spamassassin.nix199
24 files changed, 4493 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/mail/clamsmtp.nix b/nixpkgs/nixos/modules/services/mail/clamsmtp.nix
new file mode 100644
index 000000000000..fc1267c5d280
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/clamsmtp.nix
@@ -0,0 +1,181 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.clamsmtp;
+  clamdSocket = "/run/clamav/clamd.ctl"; # See services/security/clamav.nix
+in
+{
+  ##### interface
+  options = {
+    services.clamsmtp = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable clamsmtp.";
+      };
+
+      instances = mkOption {
+        description = "Instances of clamsmtp to run.";
+        type = types.listOf (types.submodule { options = {
+          action = mkOption {
+            type = types.enum [ "bounce" "drop" "pass" ];
+            default = "drop";
+            description =
+              ''
+                Action to take when a virus is detected.
+
+                Note that viruses often spoof sender addresses, so bouncing is
+                in most cases not a good idea.
+              '';
+          };
+
+          header = mkOption {
+            type = types.str;
+            default = "";
+            example = "X-Virus-Scanned: ClamAV using ClamSMTP";
+            description =
+              ''
+                A header to add to scanned messages. See clamsmtpd.conf(5) for
+                more details. Empty means no header.
+              '';
+          };
+
+          keepAlives = mkOption {
+            type = types.int;
+            default = 0;
+            description =
+              ''
+                Number of seconds to wait between each NOOP sent to the sending
+                server. 0 to disable.
+
+                This is meant for slow servers where the sending MTA times out
+                waiting for clamd to scan the file.
+              '';
+          };
+
+          listen = mkOption {
+            type = types.str;
+            example = "127.0.0.1:10025";
+            description =
+              ''
+                Address to wait for incoming SMTP connections on. See
+                clamsmtpd.conf(5) for more details.
+              '';
+          };
+
+          quarantine = mkOption {
+            type = types.bool;
+            default = false;
+            description =
+              ''
+                Whether to quarantine files that contain viruses by leaving them
+                in the temporary directory.
+              '';
+          };
+
+          maxConnections = mkOption {
+            type = types.int;
+            default = 64;
+            description = "Maximum number of connections to accept at once.";
+          };
+
+          outAddress = mkOption {
+            type = types.str;
+            description =
+              ''
+                Address of the SMTP server to send email to once it has been
+                scanned.
+              '';
+          };
+
+          tempDirectory = mkOption {
+            type = types.str;
+            default = "/tmp";
+            description =
+              ''
+                Temporary directory that needs to be accessible to both clamd
+                and clamsmtpd.
+              '';
+          };
+
+          timeout = mkOption {
+            type = types.int;
+            default = 180;
+            description = "Time-out for network connections.";
+          };
+
+          transparentProxy = mkOption {
+            type = types.bool;
+            default = false;
+            description = "Enable clamsmtp's transparent proxy support.";
+          };
+
+          virusAction = mkOption {
+            type = with types; nullOr path;
+            default = null;
+            description =
+              ''
+                Command to run when a virus is found. Please see VIRUS ACTION in
+                clamsmtpd(8) for a discussion of this option and its safe use.
+              '';
+          };
+
+          xClient = mkOption {
+            type = types.bool;
+            default = false;
+            description =
+              ''
+                Send the XCLIENT command to the receiving server, for forwarding
+                client addresses and connection information if the receiving
+                server supports this feature.
+              '';
+          };
+        };});
+      };
+    };
+  };
+
+  ##### implementation
+  config = let
+    configfile = conf: pkgs.writeText "clamsmtpd.conf"
+      ''
+        Action: ${conf.action}
+        ClamAddress: ${clamdSocket}
+        Header: ${conf.header}
+        KeepAlives: ${toString conf.keepAlives}
+        Listen: ${conf.listen}
+        Quarantine: ${if conf.quarantine then "on" else "off"}
+        MaxConnections: ${toString conf.maxConnections}
+        OutAddress: ${conf.outAddress}
+        TempDirectory: ${conf.tempDirectory}
+        TimeOut: ${toString conf.timeout}
+        TransparentProxy: ${if conf.transparentProxy then "on" else "off"}
+        User: clamav
+        ${optionalString (conf.virusAction != null) "VirusAction: ${conf.virusAction}"}
+        XClient: ${if conf.xClient then "on" else "off"}
+      '';
+  in
+    mkIf cfg.enable {
+      assertions = [
+        { assertion = config.services.clamav.daemon.enable;
+          message = "clamsmtp requires clamav to be enabled";
+        }
+      ];
+
+      systemd.services = listToAttrs (imap1 (i: conf:
+        nameValuePair "clamsmtp-${toString i}" {
+          description = "ClamSMTP instance ${toString i}";
+          wantedBy = [ "multi-user.target" ];
+          script = "exec ${pkgs.clamsmtp}/bin/clamsmtpd -f ${configfile conf}";
+          after = [ "clamav-daemon.service" ];
+          requires = [ "clamav-daemon.service" ];
+          serviceConfig.Type = "forking";
+          serviceConfig.PrivateTmp = "yes";
+          unitConfig.JoinsNamespaceOf = "clamav-daemon.service";
+        }
+      ) cfg.instances);
+    };
+
+  meta.maintainers = with lib.maintainers; [ ekleog ];
+}
diff --git a/nixpkgs/nixos/modules/services/mail/davmail.nix b/nixpkgs/nixos/modules/services/mail/davmail.nix
new file mode 100644
index 000000000000..374a3dd75c1c
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/davmail.nix
@@ -0,0 +1,99 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.davmail;
+
+  configType = with types;
+    oneOf [ (attrsOf configType) str int bool ] // {
+      description = "davmail config type (str, int, bool or attribute set thereof)";
+    };
+
+  toStr = val: if isBool val then boolToString val else toString val;
+
+  linesForAttrs = attrs: concatMap (name: let value = attrs.${name}; in
+    if isAttrs value
+      then map (line: name + "." + line) (linesForAttrs value)
+      else [ "${name}=${toStr value}" ]
+  ) (attrNames attrs);
+
+  configFile = pkgs.writeText "davmail.properties" (concatStringsSep "\n" (linesForAttrs cfg.config));
+
+in
+
+  {
+    options.services.davmail = {
+      enable = mkEnableOption "davmail, an MS Exchange gateway";
+
+      url = mkOption {
+        type = types.str;
+        description = "Outlook Web Access URL to access the exchange server, i.e. the base webmail URL.";
+        example = "https://outlook.office365.com/EWS/Exchange.asmx";
+      };
+
+      config = mkOption {
+        type = configType;
+        default = {};
+        description = ''
+          Davmail configuration. Refer to
+          <link xlink:href="http://davmail.sourceforge.net/serversetup.html"/>
+          and <link xlink:href="http://davmail.sourceforge.net/advanced.html"/>
+          for details on supported values.
+        '';
+        example = literalExample ''
+          {
+            davmail.allowRemote = true;
+            davmail.imapPort = 55555;
+            davmail.bindAddress = "10.0.1.2";
+            davmail.smtpSaveInSent = true;
+            davmail.folderSizeLimit = 10;
+            davmail.caldavAutoSchedule = false;
+            log4j.logger.rootLogger = "DEBUG";
+          }
+        '';
+      };
+    };
+
+    config = mkIf cfg.enable {
+
+      services.davmail.config = {
+        davmail = mapAttrs (name: mkDefault) {
+          server = true;
+          disableUpdateCheck = true;
+          logFilePath = "/var/log/davmail/davmail.log";
+          logFileSize = "1MB";
+          mode = "auto";
+          url = cfg.url;
+          caldavPort = 1080;
+          imapPort = 1143;
+          ldapPort = 1389;
+          popPort = 1110;
+          smtpPort = 1025;
+        };
+        log4j = {
+          logger.davmail = mkDefault "WARN";
+          logger.httpclient.wire = mkDefault "WARN";
+          logger.org.apache.commons.httpclient = mkDefault "WARN";
+          rootLogger = mkDefault "WARN";
+        };
+      };
+
+      systemd.services.davmail = {
+        description = "DavMail POP/IMAP/SMTP Exchange Gateway";
+        after = [ "network.target" ];
+        wantedBy = [ "multi-user.target" ];
+
+        serviceConfig = {
+          Type = "simple";
+          ExecStart = "${pkgs.davmail}/bin/davmail ${configFile}";
+          Restart = "on-failure";
+          DynamicUser = "yes";
+          LogsDirectory = "davmail";
+        };
+      };
+
+      environment.systemPackages = [ pkgs.davmail ];
+    };
+  }
diff --git a/nixpkgs/nixos/modules/services/mail/dkimproxy-out.nix b/nixpkgs/nixos/modules/services/mail/dkimproxy-out.nix
new file mode 100644
index 000000000000..f4ac9e47007a
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/dkimproxy-out.nix
@@ -0,0 +1,120 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+let
+  cfg = config.services.dkimproxy-out;
+  keydir = "/var/lib/dkimproxy-out";
+  privkey = "${keydir}/private.key";
+  pubkey = "${keydir}/public.key";
+in
+{
+  ##### interface
+  options = {
+    services.dkimproxy-out = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description =
+          ''
+            Whether to enable dkimproxy_out.
+
+            Note that a key will be auto-generated, and can be found in
+            ${keydir}.
+          '';
+      };
+
+      listen = mkOption {
+        type = types.str;
+        example = "127.0.0.1:10027";
+        description = "Address:port DKIMproxy should listen on.";
+      };
+
+      relay = mkOption {
+        type = types.str;
+        example = "127.0.0.1:10028";
+        description = "Address:port DKIMproxy should forward mail to.";
+      };
+
+      domains = mkOption {
+        type = with types; listOf str;
+        example = [ "example.org" "example.com" ];
+        description = "List of domains DKIMproxy can sign for.";
+      };
+
+      selector = mkOption {
+        type = types.str;
+        example = "selector1";
+        description =
+          ''
+            The selector to use for DKIM key identification.
+
+            For example, if 'selector1' is used here, then for each domain
+            'example.org' given in `domain`, 'selector1._domainkey.example.org'
+            should contain the TXT record indicating the public key is the one
+            in ${pubkey}: "v=DKIM1; t=s; p=[THE PUBLIC KEY]".
+          '';
+      };
+
+      keySize = mkOption {
+        type = types.int;
+        default = 2048;
+        description =
+          ''
+            Size of the RSA key to use to sign outgoing emails. Note that the
+            maximum mandatorily verified as per RFC6376 is 2048.
+          '';
+      };
+
+      # TODO: allow signature for other schemes than dkim(c=relaxed/relaxed)?
+      # This being the scheme used by gmail, maybe nothing more is needed for
+      # reasonable use.
+    };
+  };
+
+  ##### implementation
+  config = let
+    configfile = pkgs.writeText "dkimproxy_out.conf"
+      ''
+        listen ${cfg.listen}
+        relay ${cfg.relay}
+
+        domain ${concatStringsSep "," cfg.domains}
+        selector ${cfg.selector}
+
+        signature dkim(c=relaxed/relaxed)
+
+        keyfile ${privkey}
+      '';
+  in
+    mkIf cfg.enable {
+      users.groups.dkimproxy-out = {};
+      users.users.dkimproxy-out = {
+        description = "DKIMproxy_out daemon";
+        group = "dkimproxy-out";
+        isSystemUser = true;
+      };
+
+      systemd.services.dkimproxy-out = {
+        description = "DKIMproxy_out";
+        wantedBy = [ "multi-user.target" ];
+        preStart = ''
+          if [ ! -d "${keydir}" ]; then
+            mkdir -p "${keydir}"
+            chmod 0700 "${keydir}"
+            ${pkgs.openssl}/bin/openssl genrsa -out "${privkey}" ${toString cfg.keySize}
+            ${pkgs.openssl}/bin/openssl rsa -in "${privkey}" -pubout -out "${pubkey}"
+            chown -R dkimproxy-out:dkimproxy-out "${keydir}"
+          fi
+        '';
+        script = ''
+          exec ${pkgs.dkimproxy}/bin/dkimproxy.out --conf_file=${configfile}
+        '';
+        serviceConfig = {
+          User = "dkimproxy-out";
+          PermissionsStartOnly = true;
+        };
+      };
+    };
+
+  meta.maintainers = with lib.maintainers; [ ekleog ];
+}
diff --git a/nixpkgs/nixos/modules/services/mail/dovecot.nix b/nixpkgs/nixos/modules/services/mail/dovecot.nix
new file mode 100644
index 000000000000..139011dca23a
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/dovecot.nix
@@ -0,0 +1,401 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.dovecot2;
+  dovecotPkg = pkgs.dovecot;
+
+  baseDir = "/run/dovecot2";
+  stateDir = "/var/lib/dovecot";
+
+  dovecotConf = concatStrings [
+    ''
+      base_dir = ${baseDir}
+      protocols = ${concatStringsSep " " cfg.protocols}
+      sendmail_path = /run/wrappers/bin/sendmail
+    ''
+
+    (if cfg.sslServerCert == null then ''
+      ssl = no
+      disable_plaintext_auth = no
+    '' else ''
+      ssl_cert = <${cfg.sslServerCert}
+      ssl_key = <${cfg.sslServerKey}
+      ${optionalString (cfg.sslCACert != null) ("ssl_ca = <" + cfg.sslCACert)}
+      ssl_dh = <${config.security.dhparams.params.dovecot2.path}
+      disable_plaintext_auth = yes
+    '')
+
+    ''
+      default_internal_user = ${cfg.user}
+      default_internal_group = ${cfg.group}
+      ${optionalString (cfg.mailUser != null) "mail_uid = ${cfg.mailUser}"}
+      ${optionalString (cfg.mailGroup != null) "mail_gid = ${cfg.mailGroup}"}
+
+      mail_location = ${cfg.mailLocation}
+
+      maildir_copy_with_hardlinks = yes
+      pop3_uidl_format = %08Xv%08Xu
+
+      auth_mechanisms = plain login
+
+      service auth {
+        user = root
+      }
+    ''
+
+    (optionalString cfg.enablePAM ''
+      userdb {
+        driver = passwd
+      }
+
+      passdb {
+        driver = pam
+        args = ${optionalString cfg.showPAMFailure "failure_show_msg=yes"} dovecot2
+      }
+    '')
+
+    (optionalString (cfg.sieveScripts != {}) ''
+      plugin {
+        ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieveScripts)}
+      }
+    '')
+
+    (optionalString (cfg.mailboxes != []) ''
+      protocol imap {
+        namespace inbox {
+          inbox=yes
+          ${concatStringsSep "\n" (map mailboxConfig cfg.mailboxes)}
+        }
+      }
+    '')
+
+    (optionalString cfg.enableQuota ''
+      mail_plugins = $mail_plugins quota
+      service quota-status {
+        executable = ${dovecotPkg}/libexec/dovecot/quota-status -p postfix
+        inet_listener {
+          port = ${cfg.quotaPort}
+        }
+        client_limit = 1
+      }
+
+      protocol imap {
+        mail_plugins = $mail_plugins imap_quota
+      }
+
+      plugin {
+        quota_rule = *:storage=${cfg.quotaGlobalPerUser} 
+        quota = maildir:User quota # per virtual mail user quota # BUG/FIXME broken, we couldn't get this working
+        quota_status_success = DUNNO
+        quota_status_nouser = DUNNO
+        quota_status_overquota = "552 5.2.2 Mailbox is full"
+        quota_grace = 10%%
+      }
+    '')
+
+    cfg.extraConfig
+  ];
+
+  modulesDir = pkgs.symlinkJoin {
+    name = "dovecot-modules";
+    paths = map (pkg: "${pkg}/lib/dovecot") ([ dovecotPkg ] ++ map (module: module.override { dovecot = dovecotPkg; }) cfg.modules);
+  };
+
+  mailboxConfig = mailbox: ''
+    mailbox "${mailbox.name}" {
+      auto = ${toString mailbox.auto}
+  '' + optionalString (mailbox.specialUse != null) ''
+      special_use = \${toString mailbox.specialUse}
+  '' + "}";
+
+  mailboxes = { ... }: {
+    options = {
+      name = mkOption {
+        type = types.strMatching ''[^"]+'';
+        example = "Spam";
+        description = "The name of the mailbox.";
+      };
+      auto = mkOption {
+        type = types.enum [ "no" "create" "subscribe" ];
+        default = "no";
+        example = "subscribe";
+        description = "Whether to automatically create or create and subscribe to the mailbox or not.";
+      };
+      specialUse = mkOption {
+        type = types.nullOr (types.enum [ "All" "Archive" "Drafts" "Flagged" "Junk" "Sent" "Trash" ]);
+        default = null;
+        example = "Junk";
+        description = "Null if no special use flag is set. Other than that every use flag mentioned in the RFC is valid.";
+      };
+    };
+  };
+in
+{
+
+  options.services.dovecot2 = {
+    enable = mkEnableOption "Dovecot 2.x POP3/IMAP server";
+
+    enablePop3 = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Start the POP3 listener (when Dovecot is enabled).";
+    };
+
+    enableImap = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Start the IMAP listener (when Dovecot is enabled).";
+    };
+
+    enableLmtp = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Start the LMTP listener (when Dovecot is enabled).";
+    };
+
+    protocols = mkOption {
+      type = types.listOf types.str;
+      default = [ ];
+      description = "Additional listeners to start when Dovecot is enabled.";
+    };
+
+    user = mkOption {
+      type = types.str;
+      default = "dovecot2";
+      description = "Dovecot user name.";
+    };
+
+    group = mkOption {
+      type = types.str;
+      default = "dovecot2";
+      description = "Dovecot group name.";
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      example = "mail_debug = yes";
+      description = "Additional entries to put verbatim into Dovecot's config file.";
+    };
+
+    configFile = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Config file used for the whole dovecot configuration.";
+      apply = v: if v != null then v else pkgs.writeText "dovecot.conf" dovecotConf;
+    };
+
+    mailLocation = mkOption {
+      type = types.str;
+      default = "maildir:/var/spool/mail/%u"; /* Same as inbox, as postfix */
+      example = "maildir:~/mail:INBOX=/var/spool/mail/%u";
+      description = ''
+        Location that dovecot will use for mail folders. Dovecot mail_location option.
+      '';
+    };
+
+    mailUser = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Default user to store mail for virtual users.";
+    };
+
+    mailGroup = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Default group to store mail for virtual users.";
+    };
+
+    createMailUser = mkOption {
+      type = types.bool;
+      default = true;
+      description = ''Whether to automatically create the user
+        given in <option>services.dovecot.user</option> and the group
+        given in <option>services.dovecot.group</option>.'';
+    };
+
+    modules = mkOption {
+      type = types.listOf types.package;
+      default = [];
+      example = literalExample "[ pkgs.dovecot_pigeonhole ]";
+      description = ''
+        Symlinks the contents of lib/dovecot of every given package into
+        /etc/dovecot/modules. This will make the given modules available
+        if a dovecot package with the module_dir patch applied is being used.
+      '';
+    };
+
+    sslCACert = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Path to the server's CA certificate key.";
+    };
+
+    sslServerCert = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Path to the server's public key.";
+    };
+
+    sslServerKey = mkOption {
+      type = types.nullOr types.str;
+      default = null;
+      description = "Path to the server's private key.";
+    };
+
+    enablePAM = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Whether to create a own Dovecot PAM service and configure PAM user logins.";
+    };
+
+    sieveScripts = mkOption {
+      type = types.attrsOf types.path;
+      default = {};
+      description = "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
+    };
+
+    showPAMFailure = mkOption {
+      type = types.bool;
+      default = false;
+      description = "Show the PAM failure message on authentication error (useful for OTPW).";
+    };
+
+    mailboxes = mkOption {
+      type = types.listOf (types.submodule mailboxes);
+      default = [];
+      example = [ { name = "Spam"; specialUse = "Junk"; auto = "create"; } ];
+      description = "Configure mailboxes and auto create or subscribe them.";
+    };
+
+    enableQuota = mkOption {
+      type = types.bool;
+      default = false;
+      example = true;
+      description = "Whether to enable the dovecot quota service.";
+    };
+
+    quotaPort = mkOption {
+      type = types.str;
+      default = "12340";
+      description = ''
+        The Port the dovecot quota service binds to.
+        If using postfix, add check_policy_service inet:localhost:12340 to your smtpd_recipient_restrictions in your postfix config.
+      '';
+    };
+    quotaGlobalPerUser = mkOption {
+      type = types.str;
+      default = "100G";
+      example = "10G";
+      description = "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
+    };
+
+  };
+
+
+  config = mkIf cfg.enable {
+    security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
+
+    security.dhparams = mkIf (cfg.sslServerCert != null) {
+      enable = true;
+      params.dovecot2 = {};
+    };
+   services.dovecot2.protocols =
+     optional cfg.enableImap "imap"
+     ++ optional cfg.enablePop3 "pop3"
+     ++ optional cfg.enableLmtp "lmtp";
+
+    users.users = [
+      { name = "dovenull";
+        uid = config.ids.uids.dovenull2;
+        description = "Dovecot user for untrusted logins";
+        group = "dovenull";
+      }
+    ] ++ optional (cfg.user == "dovecot2")
+         { name = "dovecot2";
+           uid = config.ids.uids.dovecot2;
+           description = "Dovecot user";
+           group = cfg.group;
+         }
+      ++ optional (cfg.createMailUser && cfg.mailUser != null)
+         ({ name = cfg.mailUser;
+            description = "Virtual Mail User";
+         } // optionalAttrs (cfg.mailGroup != null) {
+           group = cfg.mailGroup;
+         });
+
+    users.groups = optional (cfg.group == "dovecot2")
+      { name = "dovecot2";
+        gid = config.ids.gids.dovecot2;
+      }
+    ++ optional (cfg.createMailUser && cfg.mailGroup != null)
+      { name = cfg.mailGroup;
+      }
+    ++ singleton
+      { name = "dovenull";
+        gid = config.ids.gids.dovenull2;
+      };
+
+    environment.etc."dovecot/modules".source = modulesDir;
+    environment.etc."dovecot/dovecot.conf".source = cfg.configFile;
+
+    systemd.services.dovecot2 = {
+      description = "Dovecot IMAP/POP3 server";
+
+      after = [ "keys.target" "network.target" ];
+      wants = [ "keys.target" ];
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ cfg.configFile ];
+
+      serviceConfig = {
+        ExecStart = "${dovecotPkg}/sbin/dovecot -F";
+        ExecReload = "${dovecotPkg}/sbin/doveadm reload";
+        Restart = "on-failure";
+        RestartSec = "1s";
+        StartLimitInterval = "1min";
+        RuntimeDirectory = [ "dovecot2" ];
+      };
+
+      # When copying sieve scripts preserve the original time stamp
+      # (should be 0) so that the compiled sieve script is newer than
+      # the source file and Dovecot won't try to compile it.
+      preStart = ''
+        rm -rf ${stateDir}/sieve
+      '' + optionalString (cfg.sieveScripts != {}) ''
+        mkdir -p ${stateDir}/sieve
+        ${concatStringsSep "\n" (mapAttrsToList (to: from: ''
+          if [ -d '${from}' ]; then
+            mkdir '${stateDir}/sieve/${to}'
+            cp -p "${from}/"*.sieve '${stateDir}/sieve/${to}'
+          else
+            cp -p '${from}' '${stateDir}/sieve/${to}'
+          fi
+          ${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
+        '') cfg.sieveScripts)}
+        chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
+      '';
+    };
+
+    environment.systemPackages = [ dovecotPkg ];
+
+    assertions = [
+      { assertion = intersectLists cfg.protocols [ "pop3" "imap" ] != [];
+        message = "dovecot needs at least one of the IMAP or POP3 listeners enabled";
+      }
+      { assertion = (cfg.sslServerCert == null) == (cfg.sslServerKey == null)
+          && (cfg.sslCACert != null -> !(cfg.sslServerCert == null || cfg.sslServerKey == null));
+        message = "dovecot needs both sslServerCert and sslServerKey defined for working crypto";
+      }
+      { assertion = cfg.showPAMFailure -> cfg.enablePAM;
+        message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
+      }
+      { assertion = cfg.sieveScripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
+        message = "dovecot requires mailUser and mailGroup to be set when sieveScripts is set";
+      }
+    ];
+
+  };
+
+}
diff --git a/nixpkgs/nixos/modules/services/mail/dspam.nix b/nixpkgs/nixos/modules/services/mail/dspam.nix
new file mode 100644
index 000000000000..72b8c4c08b92
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/dspam.nix
@@ -0,0 +1,150 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.dspam;
+
+  dspam = pkgs.dspam;
+
+  defaultSock = "/run/dspam/dspam.sock";
+
+  cfgfile = pkgs.writeText "dspam.conf" ''
+    Home /var/lib/dspam
+    StorageDriver ${dspam}/lib/dspam/lib${cfg.storageDriver}_drv.so
+
+    Trust root
+    Trust ${cfg.user}
+    SystemLog on
+    UserLog on
+
+    ${optionalString (cfg.domainSocket != null) ''
+      ServerDomainSocketPath "${cfg.domainSocket}"
+      ClientHost "${cfg.domainSocket}"
+    ''}
+
+    ${cfg.extraConfig}
+  '';
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.dspam = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the dspam spam filter.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "dspam";
+        description = "User for the dspam daemon.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "dspam";
+        description = "Group for the dspam daemon.";
+      };
+
+      storageDriver = mkOption {
+        type = types.str;
+        default = "hash";
+        description =  "Storage driver backend to use for dspam.";
+      };
+
+      domainSocket = mkOption {
+        type = types.nullOr types.path;
+        default = defaultSock;
+        description = "Path to local domain socket which is used for communication with the daemon. Set to null to disable UNIX socket.";
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional dspam configuration.";
+      };
+
+      maintenanceInterval = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = "If set, maintenance script will be run at specified (in systemd.timer format) interval";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable (mkMerge [
+    {
+      users.users = optionalAttrs (cfg.user == "dspam") (singleton
+        { name = "dspam";
+          group = cfg.group;
+          uid = config.ids.uids.dspam;
+        });
+
+      users.groups = optionalAttrs (cfg.group == "dspam") (singleton
+        { name = "dspam";
+          gid = config.ids.gids.dspam;
+        });
+
+      environment.systemPackages = [ dspam ];
+
+      environment.etc."dspam/dspam.conf".source = cfgfile;
+
+      systemd.services.dspam = {
+        description = "dspam spam filtering daemon";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "postgresql.service" ];
+        restartTriggers = [ cfgfile ];
+
+        serviceConfig = {
+          ExecStart = "${dspam}/bin/dspam --daemon --nofork";
+          User = cfg.user;
+          Group = cfg.group;
+          RuntimeDirectory = optional (cfg.domainSocket == defaultSock) "dspam";
+          RuntimeDirectoryMode = optional (cfg.domainSocket == defaultSock) "0750";
+          StateDirectory = "dspam";
+          StateDirectoryMode = "0750";
+          LogsDirectory = "dspam";
+          LogsDirectoryMode = "0750";
+          # DSPAM segfaults on just about every error
+          Restart = "on-abort";
+          RestartSec = "1s";
+        };
+      };
+    }
+
+    (mkIf (cfg.maintenanceInterval != null) {
+      systemd.timers.dspam-maintenance = {
+        description = "Timer for dspam maintenance script";
+        wantedBy = [ "timers.target" ];
+        timerConfig = {
+          OnCalendar = cfg.maintenanceInterval;
+          Unit = "dspam-maintenance.service";
+        };
+      };
+
+      systemd.services.dspam-maintenance = {
+        description = "dspam maintenance script";
+        restartTriggers = [ cfgfile ];
+
+        serviceConfig = {
+          ExecStart = "${dspam}/bin/dspam_maintenance --verbose";
+          Type = "oneshot";
+          User = cfg.user;
+          Group = cfg.group;
+        };
+      };
+    })
+  ]);
+}
diff --git a/nixpkgs/nixos/modules/services/mail/exim.nix b/nixpkgs/nixos/modules/services/mail/exim.nix
new file mode 100644
index 000000000000..c05811291359
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/exim.nix
@@ -0,0 +1,122 @@
+{ config, lib, pkgs, ... }:
+
+let
+  inherit (lib) mkIf mkOption singleton types;
+  inherit (pkgs) coreutils;
+  cfg = config.services.exim;
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.exim = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the Exim mail transfer agent.";
+      };
+
+      config = mkOption {
+        type = types.string;
+        default = "";
+        description = ''
+          Verbatim Exim configuration.  This should not contain exim_user,
+          exim_group, exim_path, or spool_directory.
+        '';
+      };
+
+      user = mkOption {
+        type = types.string;
+        default = "exim";
+        description = ''
+          User to use when no root privileges are required.
+          In particular, this applies when receiving messages and when doing
+          remote deliveries.  (Local deliveries run as various non-root users,
+          typically as the owner of a local mailbox.) Specifying this value
+          as root is not supported.
+        '';
+      };
+
+      group = mkOption {
+        type = types.string;
+        default = "exim";
+        description = ''
+          Group to use when no root privileges are required.
+        '';
+      };
+
+      spoolDir = mkOption {
+        type = types.string;
+        default = "/var/spool/exim";
+        description = ''
+          Location of the spool directory of exim.
+        '';
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.exim;
+        defaultText = "pkgs.exim";
+        description = ''
+          The Exim derivation to use.
+          This can be used to enable features such as LDAP or PAM support.
+        '';
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    environment = {
+      etc."exim.conf".text = ''
+        exim_user = ${cfg.user}
+        exim_group = ${cfg.group}
+        exim_path = /run/wrappers/bin/exim
+        spool_directory = ${cfg.spoolDir}
+        ${cfg.config}
+      '';
+      systemPackages = [ cfg.package ];
+    };
+
+    users.users = singleton {
+      name = cfg.user;
+      description = "Exim mail transfer agent user";
+      uid = config.ids.uids.exim;
+      group = cfg.group;
+    };
+
+    users.groups = singleton {
+      name = cfg.group;
+      gid = config.ids.gids.exim;
+    };
+
+    security.wrappers.exim.source = "${cfg.package}/bin/exim";
+
+    systemd.services.exim = {
+      description = "Exim Mail Daemon";
+      wantedBy = [ "multi-user.target" ];
+      restartTriggers = [ config.environment.etc."exim.conf".source ];
+      serviceConfig = {
+        ExecStart   = "${cfg.package}/bin/exim -bdf -q30m";
+        ExecReload  = "${coreutils}/bin/kill -HUP $MAINPID";
+      };
+      preStart = ''
+        if ! test -d ${cfg.spoolDir}; then
+          ${coreutils}/bin/mkdir -p ${cfg.spoolDir}
+          ${coreutils}/bin/chown ${cfg.user}:${cfg.group} ${cfg.spoolDir}
+        fi
+      '';
+    };
+
+  };
+
+}
diff --git a/nixpkgs/nixos/modules/services/mail/freepops.nix b/nixpkgs/nixos/modules/services/mail/freepops.nix
new file mode 100644
index 000000000000..5b729ca50a5e
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/freepops.nix
@@ -0,0 +1,89 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mail.freepopsd;
+in
+
+{
+  options = {
+    services.mail.freepopsd = {
+      enable = mkOption {
+        default = false;
+        type = with types; bool;
+        description = ''
+          Enables Freepops, a POP3 webmail wrapper.
+        '';
+      };
+
+      port = mkOption {
+        default = 2000;
+        type = with types; uniq int;
+        description = ''
+          Port on which the pop server will listen.
+        '';
+      };
+
+      threads = mkOption {
+        default = 5;
+        type = with types; uniq int;
+        description = ''
+          Max simultaneous connections.
+        '';
+      };
+
+      bind = mkOption {
+        default = "0.0.0.0";
+        type = types.str;
+        description = ''
+          Bind over an IPv4 address instead of any.
+        '';
+      };
+
+      logFile = mkOption {
+        default = "/var/log/freepopsd";
+        example = "syslog";
+        type = types.str;
+        description = ''
+          Filename of the log file or syslog to rely on the logging daemon.
+        '';
+      };
+
+      suid = {
+        user = mkOption {
+          default = "nobody";
+          type = types.str;
+          description = ''
+            User name under which freepopsd will be after binding the port.
+          '';
+        };
+
+        group = mkOption {
+          default = "nogroup";
+          type = types.str;
+          description = ''
+            Group under which freepopsd will be after binding the port.
+          '';
+        };
+      };
+
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.freepopsd = {
+      description = "Freepopsd (webmail over POP3)";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      script = ''
+        ${pkgs.freepops}/bin/freepopsd \
+          -p ${toString cfg.port} \
+          -t ${toString cfg.threads} \
+          -b ${cfg.bind} \
+          -vv -l ${cfg.logFile} \
+          -s ${cfg.suid.user}.${cfg.suid.group}
+      '';
+    };
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/mail.nix b/nixpkgs/nixos/modules/services/mail/mail.nix
new file mode 100644
index 000000000000..fed313e4738e
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/mail.nix
@@ -0,0 +1,33 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.mail = {
+
+      sendmailSetuidWrapper = mkOption {
+        default = null;
+        internal = true;
+        description = ''
+          Configuration for the sendmail setuid wapper.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf (config.services.mail.sendmailSetuidWrapper != null) {
+
+    security.wrappers.sendmail = config.services.mail.sendmailSetuidWrapper;
+
+  };
+
+}
diff --git a/nixpkgs/nixos/modules/services/mail/mailcatcher.nix b/nixpkgs/nixos/modules/services/mail/mailcatcher.nix
new file mode 100644
index 000000000000..fa8d41e918d3
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/mailcatcher.nix
@@ -0,0 +1,60 @@
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.mailcatcher;
+
+  inherit (lib) mkEnableOption mkIf mkOption types;
+in
+{
+  # interface
+
+  options = {
+
+    services.mailcatcher = {
+      enable = mkEnableOption "MailCatcher";
+
+      http.ip = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "The ip address of the http server.";
+      };
+
+      http.port = mkOption {
+        type = types.port;
+        default = 1080;
+        description = "The port address of the http server.";
+      };
+
+      smtp.ip = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "The ip address of the smtp server.";
+      };
+
+      smtp.port = mkOption {
+        type = types.port;
+        default = 1025;
+        description = "The port address of the smtp server.";
+      };
+    };
+
+  };
+
+  # implementation
+
+  config = mkIf cfg.enable {
+    environment.systemPackages = [ pkgs.mailcatcher ];
+
+    systemd.services.mailcatcher = {
+      description = "MailCatcher Service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      serviceConfig = {
+        DynamicUser = true;
+        Restart = "always";
+        ExecStart = "${pkgs.mailcatcher}/bin/mailcatcher --foreground --no-quit --http-ip ${cfg.http.ip} --http-port ${toString cfg.http.port} --smtp-ip ${cfg.smtp.ip} --smtp-port ${toString cfg.smtp.port}";
+      };
+    };
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/mailhog.nix b/nixpkgs/nixos/modules/services/mail/mailhog.nix
new file mode 100644
index 000000000000..b78f4c8e0e66
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/mailhog.nix
@@ -0,0 +1,43 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.mailhog;
+in {
+  ###### interface
+
+  options = {
+
+    services.mailhog = {
+      enable = mkEnableOption "MailHog";
+      user = mkOption {
+        type = types.str;
+        default = "mailhog";
+        description = "User account under which mailhog runs.";
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users.mailhog = {
+      name = cfg.user;
+      description = "MailHog service user";
+    };
+
+    systemd.services.mailhog = {
+      description = "MailHog service";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = "${pkgs.mailhog}/bin/MailHog";
+        User = cfg.user;
+      };
+    };
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/mlmmj.nix b/nixpkgs/nixos/modules/services/mail/mlmmj.nix
new file mode 100644
index 000000000000..11565bc02f89
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/mlmmj.nix
@@ -0,0 +1,156 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  concatMapLines = f: l: lib.concatStringsSep "\n" (map f l);
+
+  cfg = config.services.mlmmj;
+  stateDir = "/var/lib/mlmmj";
+  spoolDir = "/var/spool/mlmmj";
+  listDir = domain: list: "${spoolDir}/${domain}/${list}";
+  listCtl = domain: list: "${listDir domain list}/control";
+  transport = domain: list: "${domain}--${list}@local.list.mlmmj mlmmj:${domain}/${list}";
+  virtual = domain: list: "${list}@${domain} ${domain}--${list}@local.list.mlmmj";
+  alias = domain: list: "${list}: \"|${pkgs.mlmmj}/bin/mlmmj-receive -L ${listDir domain list}/\"";
+  subjectPrefix = list: "[${list}]";
+  listAddress = domain: list: "${list}@${domain}";
+  customHeaders = domain: list: [ "List-Id: ${list}" "Reply-To: ${list}@${domain}" ];
+  footer = domain: list: "To unsubscribe send a mail to ${list}+unsubscribe@${domain}";
+  createList = d: l:
+    let ctlDir = listCtl d l; in
+    ''
+      for DIR in incoming queue queue/discarded archive text subconf unsubconf \
+                 bounce control moderation subscribers.d digesters.d requeue \
+                 nomailsubs.d
+      do
+             mkdir -p '${listDir d l}'/"$DIR"
+      done
+      ${pkgs.coreutils}/bin/mkdir -p ${ctlDir}
+      echo ${listAddress d l} > '${ctlDir}/listaddress'
+      [ ! -e ${ctlDir}/customheaders ] && \
+          echo "${lib.concatStringsSep "\n" (customHeaders d l)}" > '${ctlDir}/customheaders'
+      [ ! -e ${ctlDir}/footer ] && \
+          echo ${footer d l} > '${ctlDir}/footer'
+      [ ! -e ${ctlDir}/prefix ] && \
+          echo ${subjectPrefix l} > '${ctlDir}/prefix'
+    '';
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.mlmmj = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Enable mlmmj";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "mlmmj";
+        description = "mailinglist local user";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "mlmmj";
+        description = "mailinglist local group";
+      };
+
+      listDomain = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = "Set the mailing list domain";
+      };
+
+      mailLists = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "The collection of hosted maillists";
+      };
+
+      maintInterval = mkOption {
+        type = types.str;
+        default = "20min";
+        description = ''
+          Time interval between mlmmj-maintd runs, see
+          <citerefentry><refentrytitle>systemd.time</refentrytitle>
+          <manvolnum>7</manvolnum></citerefentry> for format information.
+        '';
+      };
+
+    };
+
+  };
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = singleton {
+      name = cfg.user;
+      description = "mlmmj user";
+      home = stateDir;
+      createHome = true;
+      uid = config.ids.uids.mlmmj;
+      group = cfg.group;
+      useDefaultShell = true;
+    };
+
+    users.groups = singleton {
+      name = cfg.group;
+      gid = config.ids.gids.mlmmj;
+    };
+
+    services.postfix = {
+      enable = true;
+      recipientDelimiter= "+";
+      extraMasterConf = ''
+        mlmmj unix - n n - - pipe flags=ORhu user=mlmmj argv=${pkgs.mlmmj}/bin/mlmmj-receive -F -L ${spoolDir}/$nexthop
+      '';
+
+      extraAliases = concatMapLines (alias cfg.listDomain) cfg.mailLists;
+
+      extraConfig = ''
+        transport_maps = hash:${stateDir}/transports
+        virtual_alias_maps = hash:${stateDir}/virtuals
+        propagate_unmatched_extensions = virtual
+      '';
+    };
+
+    environment.systemPackages = [ pkgs.mlmmj ];
+
+    system.activationScripts.mlmmj = ''
+          ${pkgs.coreutils}/bin/mkdir -p ${stateDir} ${spoolDir}/${cfg.listDomain}
+          ${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${spoolDir}
+          ${concatMapLines (createList cfg.listDomain) cfg.mailLists}
+          echo "${concatMapLines (virtual cfg.listDomain) cfg.mailLists}" > ${stateDir}/virtuals
+          echo "${concatMapLines (transport cfg.listDomain) cfg.mailLists}" > ${stateDir}/transports
+          ${pkgs.postfix}/bin/postmap ${stateDir}/virtuals
+          ${pkgs.postfix}/bin/postmap ${stateDir}/transports
+      '';
+
+    systemd.services."mlmmj-maintd" = {
+      description = "mlmmj maintenance daemon";
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.mlmmj}/bin/mlmmj-maintd -F -d ${spoolDir}/${cfg.listDomain}";
+      };
+    };
+
+    systemd.timers."mlmmj-maintd" = {
+      description = "mlmmj maintenance timer";
+      timerConfig.OnUnitActiveSec = cfg.maintInterval;
+      wantedBy = [ "timers.target" ];
+    };
+  };
+
+}
diff --git a/nixpkgs/nixos/modules/services/mail/nullmailer.nix b/nixpkgs/nixos/modules/services/mail/nullmailer.nix
new file mode 100644
index 000000000000..9997d287013e
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/nullmailer.nix
@@ -0,0 +1,246 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  options = {
+
+    services.nullmailer = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable nullmailer daemon.";
+      };
+
+      user = mkOption {
+        type = types.string;
+        default = "nullmailer";
+        description = ''
+          User to use to run nullmailer-send.
+        '';
+      };
+
+      group = mkOption {
+        type = types.string;
+        default = "nullmailer";
+        description = ''
+          Group to use to run nullmailer-send.
+        '';
+      };
+
+      setSendmail = mkOption {
+        type = types.bool;
+        default = true;
+        description = "Whether to set the system sendmail to nullmailer's.";
+      };
+
+      remotesFile = mkOption {
+        type = types.nullOr types.str;
+        default = null;
+        description = ''
+          Path to the <code>remotes</code> control file. This file contains a
+          list of remote servers to which to send each message.
+
+          See <code>man 8 nullmailer-send</code> for syntax and available
+          options.
+        '';
+      };
+
+      config = {
+        adminaddr = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            If set, all recipients to users at either "localhost" (the literal string)
+            or the canonical host name (from the me control attribute) are remapped to this address.
+            This is provided to allow local daemons to be able to send email to
+            "somebody@localhost" and have it go somewhere sensible instead of being  bounced
+            by your relay host. To send to multiple addresses,
+            put them all on one line separated by a comma.
+          '';
+        };
+
+        allmailfrom = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            If set, content will override the envelope sender on all messages.
+          '';
+        };
+
+        defaultdomain = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+             The content of this attribute is appended to any host name that
+             does not contain a period (except localhost), including defaulthost
+             and idhost. Defaults to the value of the me attribute, if it exists,
+             otherwise the literal name defauldomain.
+          '';
+        };
+
+        defaulthost = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+             The content of this attribute is appended to any address that
+             is missing a host name. Defaults to the value of the me control
+             attribute, if it exists, otherwise the literal name defaulthost.
+          '';
+        };
+
+        doublebounceto = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            If the original sender was empty (the original message was a
+            delivery status or disposition notification), the double bounce
+            is sent to the address in this attribute.
+          '';
+        };
+
+        helohost = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            Sets  the  environment variable $HELOHOST which is used by the
+            SMTP protocol module to set the parameter given to the HELO command.
+            Defaults to the value of the me configuration attribute.
+          '';
+        };
+
+        idhost = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The content of this attribute is used when building the message-id
+            string for the message. Defaults to the canonicalized value of defaulthost.
+          '';
+        };
+
+        maxpause = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+             The maximum time to pause between successive queue runs, in seconds.
+             Defaults to 24 hours (86400).
+          '';
+        };
+
+        me = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+             The fully-qualifiled host name of the computer running nullmailer.
+             Defaults to the literal name me.
+          '';
+        };
+
+        pausetime = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The minimum time to pause between successive queue runs when there
+            are messages in the queue, in seconds. Defaults to 1 minute (60).
+            Each time this timeout is reached, the timeout is doubled to a
+            maximum of maxpause. After new messages are injected, the timeout
+            is reset.  If this is set to 0, nullmailer-send will exit
+            immediately after going through the queue once (one-shot mode).
+          '';
+        };
+
+        remotes = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            A list of remote servers to which to send each message. Each line
+            contains a remote host name or address followed by an optional
+            protocol string, separated by white space.
+
+            See <code>man 8 nullmailer-send</code> for syntax and available
+            options.
+
+            WARNING: This is stored world-readable in the nix store. If you need
+            to specify any secret credentials here, consider using the
+            <code>remotesFile</code> option instead.
+          '';
+        };
+
+        sendtimeout = mkOption {
+          type = types.nullOr types.str;
+          default = null;
+          description = ''
+            The  time to wait for a remote module listed above to complete sending
+            a message before killing it and trying again, in seconds.
+            Defaults to 1 hour (3600).  If this is set to 0, nullmailer-send
+            will wait forever for messages to complete sending.
+          '';
+        };
+      };
+    };
+  };
+
+  config = let
+    cfg = config.services.nullmailer;
+  in mkIf cfg.enable {
+
+    assertions = [
+      { assertion = cfg.config.remotes == null || cfg.remotesFile == null;
+        message = "Only one of `remotesFile` or `config.remotes` may be used at a time.";
+      }
+    ];
+
+    environment = {
+      systemPackages = [ pkgs.nullmailer ];
+      etc = let
+        validAttrs = filterAttrs (name: value: value != null) cfg.config;
+      in
+        (foldl' (as: name: as // { "nullmailer/${name}".text = validAttrs.${name}; }) {} (attrNames validAttrs))
+          // optionalAttrs (cfg.remotesFile != null) { "nullmailer/remotes".source = cfg.remotesFile; };
+    };
+
+    users = {
+      users = singleton {
+        name = cfg.user;
+        description = "Nullmailer relay-only mta user";
+        group = cfg.group;
+      };
+
+      groups = singleton {
+        name = cfg.group;
+      };
+    };
+
+    systemd.tmpfiles.rules = [
+      "d /var/spool/nullmailer - ${cfg.user} - - -"
+    ];
+
+    systemd.services.nullmailer = {
+      description = "nullmailer";
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      preStart = ''
+        mkdir -p /var/spool/nullmailer/{queue,tmp}
+        rm -f /var/spool/nullmailer/trigger && mkfifo -m 660 /var/spool/nullmailer/trigger
+      '';
+
+      serviceConfig = {
+        User = cfg.user;
+        Group = cfg.group;
+        ExecStart = "${pkgs.nullmailer}/bin/nullmailer-send";
+        Restart = "always";
+      };
+    };
+
+    services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail {
+      program = "sendmail";
+      source = "${pkgs.nullmailer}/bin/sendmail";
+      owner = cfg.user;
+      group = cfg.group;
+      setuid = true;
+      setgid = true;
+    };
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/offlineimap.nix b/nixpkgs/nixos/modules/services/mail/offlineimap.nix
new file mode 100644
index 000000000000..294e3806f94a
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/offlineimap.nix
@@ -0,0 +1,72 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.offlineimap;
+in {
+
+  options.services.offlineimap = {
+    enable = mkEnableOption "OfflineIMAP, a software to dispose your mailbox(es) as a local Maildir(s)";
+
+    install = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to install a user service for Offlineimap. Once
+        the service is started, emails will be fetched automatically.
+
+        The service must be manually started for each user with
+        "systemctl --user start offlineimap" or globally through
+        <varname>services.offlineimap.enable</varname>.
+      '';
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.offlineimap;
+      defaultText = "pkgs.offlineimap";
+      description = "Offlineimap derivation to use.";
+    };
+
+    path = mkOption {
+      type = types.listOf types.path;
+      default = [];
+      example = literalExample "[ pkgs.pass pkgs.bash pkgs.notmuch ]";
+      description = "List of derivations to put in Offlineimap's path.";
+    };
+
+    onCalendar = mkOption {
+      type = types.str;
+      default = "*:0/3"; # every 3 minutes
+      description = "How often is offlineimap started. Default is '*:0/3' meaning every 3 minutes. See systemd.time(7) for more information about the format.";
+    };
+
+    timeoutStartSec = mkOption {
+      type = types.str;
+      default = "120sec"; # Kill if still alive after 2 minutes
+      description = "How long waiting for offlineimap before killing it. Default is '120sec' meaning every 2 minutes. See systemd.time(7) for more information about the format.";
+    };
+  };
+  config = mkIf (cfg.enable || cfg.install) {
+    systemd.user.services.offlineimap = {
+      description = "Offlineimap: a software to dispose your mailbox(es) as a local Maildir(s)";
+      serviceConfig = {
+        Type      = "oneshot";
+        ExecStart = "${cfg.package}/bin/offlineimap -u syslog -o -1";
+        TimeoutStartSec = cfg.timeoutStartSec;
+      };
+      path = cfg.path;
+    };
+    environment.systemPackages = [ cfg.package ];
+    systemd.user.timers.offlineimap = {
+      description = "offlineimap timer";
+      timerConfig               = {
+        Unit = "offlineimap.service";
+        OnCalendar = cfg.onCalendar;
+        # start immediately after computer is started:
+        Persistent = "true";
+      };
+    } // optionalAttrs cfg.enable { wantedBy = [ "default.target" ]; };
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/opendkim.nix b/nixpkgs/nixos/modules/services/mail/opendkim.nix
new file mode 100644
index 000000000000..253823cbaf9c
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/opendkim.nix
@@ -0,0 +1,133 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.opendkim;
+
+  defaultSock = "local:/run/opendkim/opendkim.sock";
+
+  keyFile = "${cfg.keyPath}/${cfg.selector}.private";
+
+  args = [ "-f" "-l"
+           "-p" cfg.socket
+           "-d" cfg.domains
+           "-k" keyFile
+           "-s" cfg.selector
+         ] ++ optionals (cfg.configFile != null) [ "-x" cfg.configFile ];
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.opendkim = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the OpenDKIM sender authentication system.";
+      };
+
+      socket = mkOption {
+        type = types.str;
+        default = defaultSock;
+        description = "Socket which is used for communication with OpenDKIM.";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "opendkim";
+        description = "User for the daemon.";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "opendkim";
+        description = "Group for the daemon.";
+      };
+
+      domains = mkOption {
+        type = types.str;
+        default = "csl:${config.networking.hostName}";
+        example = "csl:example.com,mydomain.net";
+        description = ''
+          Local domains set (see <literal>opendkim(8)</literal> for more information on datasets).
+          Messages from them are signed, not verified.
+        '';
+      };
+
+      keyPath = mkOption {
+        type = types.path;
+        description = ''
+          The path that opendkim should put its generated private keys into.
+          The DNS settings will be found in this directory with the name selector.txt.
+        '';
+        default = "/var/lib/opendkim/keys";
+      };
+
+      selector = mkOption {
+        type = types.str;
+        description = "Selector to use when signing.";
+      };
+
+      configFile = mkOption {
+        type = types.nullOr types.path;
+        default = null;
+        description = "Additional opendkim configuration.";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    users.users = optionalAttrs (cfg.user == "opendkim") (singleton
+      { name = "opendkim";
+        group = cfg.group;
+        uid = config.ids.uids.opendkim;
+      });
+
+    users.groups = optionalAttrs (cfg.group == "opendkim") (singleton
+      { name = "opendkim";
+        gid = config.ids.gids.opendkim;
+      });
+
+    environment.systemPackages = [ pkgs.opendkim ];
+
+    systemd.tmpfiles.rules = [
+      "d '${cfg.keyPath}' - ${cfg.user} ${cfg.group} - -"
+    ];
+
+    systemd.services.opendkim = {
+      description = "OpenDKIM signing and verification daemon";
+      after = [ "network.target" ];
+      wantedBy = [ "multi-user.target" ];
+
+      preStart = ''
+        cd "${cfg.keyPath}"
+        if ! test -f ${cfg.selector}.private; then
+          ${pkgs.opendkim}/bin/opendkim-genkey -s ${cfg.selector} -d all-domains-generic-key
+          echo "Generated OpenDKIM key! Please update your DNS settings:\n"
+          echo "-------------------------------------------------------------"
+          cat ${cfg.selector}.txt
+          echo "-------------------------------------------------------------"
+        fi
+      '';
+
+      serviceConfig = {
+        ExecStart = "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
+        User = cfg.user;
+        Group = cfg.group;
+        RuntimeDirectory = optional (cfg.socket == defaultSock) "opendkim";
+      };
+    };
+
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/opensmtpd.nix b/nixpkgs/nixos/modules/services/mail/opensmtpd.nix
new file mode 100644
index 000000000000..a870550ba50b
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/opensmtpd.nix
@@ -0,0 +1,131 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.opensmtpd;
+  conf = pkgs.writeText "smtpd.conf" cfg.serverConfiguration;
+  args = concatStringsSep " " cfg.extraServerArgs;
+
+  sendmail = pkgs.runCommand "opensmtpd-sendmail" { preferLocalBuild = true; } ''
+    mkdir -p $out/bin
+    ln -s ${cfg.package}/sbin/smtpctl $out/bin/sendmail
+  '';
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.opensmtpd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the OpenSMTPD server.";
+      };
+
+      package = mkOption {
+        type = types.package;
+        default = pkgs.opensmtpd;
+        defaultText = "pkgs.opensmtpd";
+        description = "The OpenSMTPD package to use.";
+      };
+
+      addSendmailToSystemPath = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether to add OpenSMTPD's sendmail binary to the
+          system path or not.
+        '';
+      };
+
+      extraServerArgs = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        example = [ "-v" "-P mta" ];
+        description = ''
+          Extra command line arguments provided when the smtpd process
+          is started.
+        '';
+      };
+
+      serverConfiguration = mkOption {
+        type = types.lines;
+        example = ''
+          listen on lo
+          accept for any deliver to lmtp localhost:24
+        '';
+        description = ''
+          The contents of the smtpd.conf configuration file. See the
+          OpenSMTPD documentation for syntax information.
+        '';
+      };
+
+      procPackages = mkOption {
+        type = types.listOf types.package;
+        default = [];
+        description = ''
+          Packages to search for filters, tables, queues, and schedulers.
+
+          Add OpenSMTPD-extras here if you want to use the filters, etc. from
+          that package.
+        '';
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    users.groups = {
+      smtpd.gid = config.ids.gids.smtpd;
+      smtpq.gid = config.ids.gids.smtpq;
+    };
+
+    users.users = {
+      smtpd = {
+        description = "OpenSMTPD process user";
+        uid = config.ids.uids.smtpd;
+        group = "smtpd";
+      };
+      smtpq = {
+        description = "OpenSMTPD queue user";
+        uid = config.ids.uids.smtpq;
+        group = "smtpq";
+      };
+    };
+
+    systemd.services.opensmtpd = let
+      procEnv = pkgs.buildEnv {
+        name = "opensmtpd-procs";
+        paths = [ cfg.package ] ++ cfg.procPackages;
+        pathsToLink = [ "/libexec/opensmtpd" ];
+      };
+    in {
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      preStart = ''
+        mkdir -p /var/spool/smtpd
+        chmod 711 /var/spool/smtpd
+
+        mkdir -p /var/spool/smtpd/offline
+        chown root.smtpq /var/spool/smtpd/offline
+        chmod 770 /var/spool/smtpd/offline
+
+        mkdir -p /var/spool/smtpd/purge
+        chown smtpq.root /var/spool/smtpd/purge
+        chmod 700 /var/spool/smtpd/purge
+      '';
+      serviceConfig.ExecStart = "${cfg.package}/sbin/smtpd -d -f ${conf} ${args}";
+      environment.OPENSMTPD_PROC_PATH = "${procEnv}/libexec/opensmtpd";
+    };
+
+    environment.systemPackages = mkIf cfg.addSendmailToSystemPath [ sendmail ];
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/pfix-srsd.nix b/nixpkgs/nixos/modules/services/mail/pfix-srsd.nix
new file mode 100644
index 000000000000..9599854352c9
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/pfix-srsd.nix
@@ -0,0 +1,56 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.pfix-srsd = {
+      enable = mkOption {
+        default = false;
+        type = types.bool;
+        description = "Whether to run the postfix sender rewriting scheme daemon.";
+      };
+
+      domain = mkOption {
+        description = "The domain for which to enable srs";
+        type = types.str;
+        example = "example.com";
+      };
+
+      secretsFile = mkOption {
+        description = ''
+          The secret data used to encode the SRS address.
+          to generate, use a command like:
+          <literal>for n in $(seq 5); do dd if=/dev/urandom count=1 bs=1024 status=none | sha256sum | sed 's/  -$//' | sed 's/^/          /'; done</literal>
+        '';
+        type = types.path;
+        default = "/var/lib/pfix-srsd/secrets";
+      };
+    };
+  };
+
+  ###### implementation
+
+  config = mkIf config.services.pfix-srsd.enable {
+    environment = {
+      systemPackages = [ pkgs.pfixtools ];
+    };
+
+    systemd.services."pfix-srsd" = {
+      description = "Postfix sender rewriting scheme daemon";
+      before = [ "postfix.service" ];
+      #note that we use requires rather than wants because postfix
+      #is unable to process (almost) all mail without srsd
+      requiredBy = [ "postfix.service" ];
+      serviceConfig = {
+        Type = "forking";
+        PIDFile = "/run/pfix-srsd.pid";
+        ExecStart = "${pkgs.pfixtools}/bin/pfix-srsd -p /run/pfix-srsd.pid -I ${config.services.pfix-srsd.domain} ${config.services.pfix-srsd.secretsFile}";
+      };
+    };
+  };
+}
\ No newline at end of file
diff --git a/nixpkgs/nixos/modules/services/mail/postfix.nix b/nixpkgs/nixos/modules/services/mail/postfix.nix
new file mode 100644
index 000000000000..2b08ab1e6aa6
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/postfix.nix
@@ -0,0 +1,898 @@
+{ 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 != "";
+  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" "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 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${seperator} ${cfg.postmasterAlias}
+    ''
+    + optionalString (cfg.rootAlias != "") ''
+      root${seperator} ${cfg.rootAlias}
+    ''
+    + cfg.extraAliases
+  ;
+
+  aliasesFile = pkgs.writeText "postfix-aliases" aliases;
+  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 {
+        default = true;
+        description = "Whether to enable smtp in master.cf.";
+      };
+
+      enableSubmission = mkOption {
+        type = types.bool;
+        default = false;
+        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";
+        };
+        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";
+      };
+
+      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_use_tls = true;
+        };
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Extra lines to be added verbatim to the main.cf configuration file.
+        ";
+      };
+
+      sslCert = mkOption {
+        type = types.str;
+        default = "";
+        description = "SSL certificate to use.";
+      };
+
+      sslCACert = mkOption {
+        type = types.str;
+        default = "";
+        description = "SSL certificate of CA.";
+      };
+
+      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
+        ";
+      };
+
+      virtual = mkOption {
+        type = types.lines;
+        default = "";
+        description = "
+          Entries for the virtual alias map, cf. man-page virtual(8).
+        ";
+      };
+
+      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 string);
+        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 = "";
+        description = "
+          Entries for the transport map, cf. man-page transport(8).
+        ";
+      };
+
+      dnsBlacklists = mkOption {
+        default = [];
+        type = with types; listOf string;
+        description = "dns blacklist servers to use with smtpd_client_restrictions";
+      };
+
+      dnsBlacklistOverrides = mkOption {
+        default = "";
+        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 = singleton
+          { source = "/var/lib/postfix/conf";
+            target = "postfix";
+          };
+
+        # 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";
+        group = setgidGroup;
+        setuid = false;
+        setgid = true;
+      };
+
+      security.wrappers.postqueue = {
+        program = "postqueue";
+        source = "${pkgs.postfix}/bin/postqueue";
+        group = setgidGroup;
+        setuid = false;
+        setgid = true;
+      };
+
+      security.wrappers.postdrop = {
+        program = "postdrop";
+        source = "${pkgs.postfix}/bin/postdrop";
+        group = setgidGroup;
+        setuid = false;
+        setgid = true;
+      };
+
+      users.users = optional (user == "postfix")
+        { name = "postfix";
+          description = "Postfix mail server user";
+          uid = config.ids.uids.postfix;
+          group = group;
+        };
+
+      users.groups =
+        optional (group == "postfix")
+        { name = group;
+          gid = config.ids.gids.postfix;
+        }
+        ++ optional (setgidGroup == "postdrop")
+        { name = 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  = "9999";
+        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.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;
+      };
+
+      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 {
+      services.postfix.aliasFiles."aliases" = aliasesFile;
+    })
+    (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;
+    })
+  ]);
+}
diff --git a/nixpkgs/nixos/modules/services/mail/postgrey.nix b/nixpkgs/nixos/modules/services/mail/postgrey.nix
new file mode 100644
index 000000000000..8e2b9c5dbc56
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/postgrey.nix
@@ -0,0 +1,194 @@
+{ config, lib, pkgs, ... }:
+
+with lib; let
+
+  cfg = config.services.postgrey;
+
+  natural = with types; addCheck int (x: x >= 0);
+  natural' = with types; addCheck int (x: x > 0);
+
+  socket = with types; addCheck (either (submodule unixSocket) (submodule inetSocket)) (x: x ? "path" || x ? "port");
+
+  inetSocket = with types; {
+    options = {
+      addr = mkOption {
+        type = nullOr string;
+        default = null;
+        example = "127.0.0.1";
+        description = "The address to bind to. Localhost if null";
+      };
+      port = mkOption {
+        type = natural';
+        default = 10030;
+        description = "Tcp port to bind to";
+      };
+    };
+  };
+
+  unixSocket = with types; {
+    options = {
+      path = mkOption {
+        type = path;
+        default = "/run/postgrey.sock";
+        description = "Path of the unix socket";
+      };
+
+      mode = mkOption {
+        type = string;
+        default = "0777";
+        description = "Mode of the unix socket";
+      };
+    };
+  };
+
+in {
+
+  options = {
+    services.postgrey = with types; {
+      enable = mkOption {
+        type = bool;
+        default = false;
+        description = "Whether to run the Postgrey daemon";
+      };
+      socket = mkOption {
+        type = socket;
+        default = {
+          path = "/run/postgrey.sock";
+          mode = "0777";
+        };
+        example = {
+          addr = "127.0.0.1";
+          port = 10030;
+        };
+        description = "Socket to bind to";
+      };
+      greylistText = mkOption {
+        type = string;
+        default = "Greylisted for %%s seconds";
+        description = "Response status text for greylisted messages; use %%s for seconds left until greylisting is over and %%r for mail domain of recipient";
+      };
+      greylistAction = mkOption {
+        type = string;
+        default = "DEFER_IF_PERMIT";
+        description = "Response status for greylisted messages (see access(5))";
+      };
+      greylistHeader = mkOption {
+        type = string;
+        default = "X-Greylist: delayed %%t seconds by postgrey-%%v at %%h; %%d";
+        description = "Prepend header to greylisted mails; use %%t for seconds delayed due to greylisting, %%v for the version of postgrey, %%d for the date, and %%h for the host";
+      };
+      delay = mkOption {
+        type = natural;
+        default = 300;
+        description = "Greylist for N seconds";
+      };
+      maxAge = mkOption {
+        type = natural;
+        default = 35;
+        description = "Delete entries from whitelist if they haven't been seen for N days";
+      };
+      retryWindow = mkOption {
+        type = either string natural;
+        default = 2;
+        example = "12h";
+        description = "Allow N days for the first retry. Use string with appended 'h' to specify time in hours";
+      };
+      lookupBySubnet = mkOption {
+        type = bool;
+        default = true;
+        description = "Strip the last N bits from IP addresses, determined by IPv4CIDR and IPv6CIDR";
+      };
+      IPv4CIDR = mkOption {
+        type = natural;
+        default = 24;
+        description = "Strip N bits from IPv4 addresses if lookupBySubnet is true";
+      };
+      IPv6CIDR = mkOption {
+        type = natural;
+        default = 64;
+        description = "Strip N bits from IPv6 addresses if lookupBySubnet is true";
+      };
+      privacy = mkOption {
+        type = bool;
+        default = true;
+        description = "Store data using one-way hash functions (SHA1)";
+      };
+      autoWhitelist = mkOption {
+        type = nullOr natural';
+        default = 5;
+        description = "Whitelist clients after successful delivery of N messages";
+      };
+      whitelistClients = mkOption {
+        type = listOf path;
+        default = [];
+        description = "Client address whitelist files (see postgrey(8))";
+      };
+      whitelistRecipients = mkOption {
+        type = listOf path;
+        default = [];
+        description = "Recipient address whitelist files (see postgrey(8))";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ pkgs.postgrey ];
+
+    users = {
+      users = {
+        postgrey = {
+          description = "Postgrey Daemon";
+          uid = config.ids.uids.postgrey;
+          group = "postgrey";
+        };
+      };
+      groups = {
+        postgrey = {
+          gid = config.ids.gids.postgrey;
+        };
+      };
+    };
+
+    systemd.services.postgrey = let
+      bind-flag = if cfg.socket ? "path" then
+        ''--unix=${cfg.socket.path} --socketmode=${cfg.socket.mode}''
+      else
+        ''--inet=${optionalString (cfg.socket.addr != null) (cfg.socket.addr + ":")}${toString cfg.socket.port}'';
+    in {
+      description = "Postfix Greylisting Service";
+      wantedBy = [ "multi-user.target" ];
+      before = [ "postfix.service" ];
+      preStart = ''
+        mkdir -p /var/postgrey
+        chown postgrey:postgrey /var/postgrey
+        chmod 0770 /var/postgrey
+      '';
+      serviceConfig = {
+        Type = "simple";
+        ExecStart = ''${pkgs.postgrey}/bin/postgrey \
+          ${bind-flag} \
+          --group=postgrey --user=postgrey \
+          --dbdir=/var/postgrey \
+          --delay=${toString cfg.delay} \
+          --max-age=${toString cfg.maxAge} \
+          --retry-window=${toString cfg.retryWindow} \
+          ${if cfg.lookupBySubnet then "--lookup-by-subnet" else "--lookup-by-host"} \
+          --ipv4cidr=${toString cfg.IPv4CIDR} --ipv6cidr=${toString cfg.IPv6CIDR} \
+          ${optionalString cfg.privacy "--privacy"} \
+          --auto-whitelist-clients=${toString (if cfg.autoWhitelist == null then 0 else cfg.autoWhitelist)} \
+          --greylist-action=${cfg.greylistAction} \
+          --greylist-text="${cfg.greylistText}" \
+          --x-greylist-header="${cfg.greylistHeader}" \
+          ${concatMapStringsSep " " (x: "--whitelist-clients=" + x) cfg.whitelistClients} \
+          ${concatMapStringsSep " " (x: "--whitelist-recipients=" + x) cfg.whitelistRecipients}
+        '';
+        Restart = "always";
+        RestartSec = 5;
+        TimeoutSec = 10;
+      };
+    };
+
+  };
+
+}
diff --git a/nixpkgs/nixos/modules/services/mail/postsrsd.nix b/nixpkgs/nixos/modules/services/mail/postsrsd.nix
new file mode 100644
index 000000000000..8f12a16906c5
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/postsrsd.nix
@@ -0,0 +1,135 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.postsrsd;
+
+in {
+
+  ###### interface
+
+  options = {
+
+    services.postsrsd = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable the postsrsd SRS server for Postfix.";
+      };
+
+      secretsFile = mkOption {
+        type = types.path;
+        default = "/var/lib/postsrsd/postsrsd.secret";
+        description = "Secret keys used for signing and verification";
+      };
+
+      domain = mkOption {
+        type = types.str;
+        description = "Domain name for rewrite";
+      };
+
+      separator = mkOption {
+        type = types.enum ["-" "=" "+"];
+        default = "=";
+        description = "First separator character in generated addresses";
+      };
+
+      # bindAddress = mkOption { # uncomment once 1.5 is released
+      #   type = types.str;
+      #   default = "127.0.0.1";
+      #   description = "Socket listen address";
+      # };
+
+      forwardPort = mkOption {
+        type = types.int;
+        default = 10001;
+        description = "Port for the forward SRS lookup";
+      };
+
+      reversePort = mkOption {
+        type = types.int;
+        default = 10002;
+        description = "Port for the reverse SRS lookup";
+      };
+
+      timeout = mkOption {
+        type = types.int;
+        default = 1800;
+        description = "Timeout for idle client connections in seconds";
+      };
+
+      excludeDomains = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = "Origin domains to exclude from rewriting in addition to primary domain";
+      };
+
+      user = mkOption {
+        type = types.str;
+        default = "postsrsd";
+        description = "User for the daemon";
+      };
+
+      group = mkOption {
+        type = types.str;
+        default = "postsrsd";
+        description = "Group for the daemon";
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+
+    services.postsrsd.domain = mkDefault config.networking.hostName;
+
+    users.users = optionalAttrs (cfg.user == "postsrsd") (singleton
+      { name = "postsrsd";
+        group = cfg.group;
+        uid = config.ids.uids.postsrsd;
+      });
+
+    users.groups = optionalAttrs (cfg.group == "postsrsd") (singleton
+      { name = "postsrsd";
+        gid = config.ids.gids.postsrsd;
+      });
+
+    systemd.services.postsrsd = {
+      description = "PostSRSd SRS rewriting server";
+      after = [ "network.target" ];
+      before = [ "postfix.service" ];
+      wantedBy = [ "multi-user.target" ];
+
+      path = [ pkgs.coreutils ];
+
+      serviceConfig = {
+        ExecStart = ''${pkgs.postsrsd}/sbin/postsrsd "-s${cfg.secretsFile}" "-d${cfg.domain}" -a${cfg.separator} -f${toString cfg.forwardPort} -r${toString cfg.reversePort} -t${toString cfg.timeout} "-X${concatStringsSep "," cfg.excludeDomains}"'';
+        User = cfg.user;
+        Group = cfg.group;
+        PermissionsStartOnly = true;
+      };
+
+      preStart = ''
+        if [ ! -e "${cfg.secretsFile}" ]; then
+          echo "WARNING: secrets file not found, autogenerating!"
+          DIR="$(dirname "${cfg.secretsFile}")"
+          if [ ! -d "$DIR" ]; then
+            mkdir -p -m750 "$DIR"
+            chown "${cfg.user}:${cfg.group}" "$DIR"
+          fi
+          dd if=/dev/random bs=18 count=1 | base64 > "${cfg.secretsFile}"
+          chmod 600 "${cfg.secretsFile}"
+        fi
+        chown "${cfg.user}:${cfg.group}" "${cfg.secretsFile}"
+      '';
+    };
+
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/rmilter.nix b/nixpkgs/nixos/modules/services/mail/rmilter.nix
new file mode 100644
index 000000000000..466365b6b305
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/rmilter.nix
@@ -0,0 +1,252 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+  rspamdCfg = config.services.rspamd;
+  postfixCfg = config.services.postfix;
+  cfg = config.services.rmilter;
+
+  inetSocket = addr: port: "inet:${addr}:${toString port}";
+  unixSocket = sock: "unix:${sock}";
+
+  systemdSocket = if cfg.bindSocket.type == "unix" then cfg.bindSocket.path
+    else "${cfg.bindSocket.address}:${toString cfg.bindSocket.port}";
+  rmilterSocket = if cfg.bindSocket.type == "unix" then unixSocket cfg.bindSocket.path
+    else inetSocket cfg.bindSocket.address cfg.bindSocket.port;
+
+  rmilterConf = ''
+    pidfile = /run/rmilter/rmilter.pid;
+    bind_socket = ${if cfg.socketActivation then "fd:3" else rmilterSocket};
+    tempdir = /tmp;
+  '' + (with cfg.rspamd; if enable then ''
+    spamd {
+      servers = ${concatStringsSep ", " servers};
+      connect_timeout = 1s;
+      results_timeout = 20s;
+      error_time = 10;
+      dead_time = 300;
+      maxerrors = 10;
+      reject_message = "${rejectMessage}";
+      ${optionalString (length whitelist != 0)  "whitelist = ${concatStringsSep ", " whitelist};"}
+
+      # rspamd_metric - metric for using with rspamd
+      # Default: "default"
+      rspamd_metric = "default";
+      ${extraConfig}
+    };
+  '' else "") + cfg.extraConfig;
+
+  rmilterConfigFile = pkgs.writeText "rmilter.conf" rmilterConf;
+
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.rmilter = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run the rmilter daemon.";
+      };
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run the rmilter daemon in debug mode.";
+      };
+
+      user = mkOption {
+        type = types.string;
+        default = "rmilter";
+        description = ''
+          User to use when no root privileges are required.
+        '';
+       };
+
+      group = mkOption {
+        type = types.string;
+        default = "rmilter";
+        description = ''
+          Group to use when no root privileges are required.
+        '';
+       };
+
+      bindSocket.type = mkOption {
+        type = types.enum [ "unix" "inet" ];
+        default = "unix";
+        description = ''
+          What kind of socket rmilter should listen on. Either "unix"
+          for an Unix domain socket or "inet" for a TCP socket.
+        '';
+      };
+
+      bindSocket.path = mkOption {
+       type = types.str;
+       default = "/run/rmilter.sock";
+       description = ''
+          Path to Unix domain socket to listen on.
+        '';
+      };
+
+      bindSocket.address = mkOption {
+        type = types.str;
+        default = "[::1]";
+        example = "0.0.0.0";
+        description = ''
+          Inet address to listen on.
+        '';
+      };
+
+      bindSocket.port = mkOption {
+        type = types.int;
+        default = 11990;
+        description = ''
+          Inet port to listen on.
+        '';
+      };
+
+      socketActivation = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Enable systemd socket activation for rmilter.
+
+          Disabling socket activation is not recommended when a Unix
+          domain socket is used and could lead to incorrect
+          permissions.
+        '';
+      };
+
+      rspamd = {
+        enable = mkOption {
+          type = types.bool;
+          default = rspamdCfg.enable;
+          description = "Whether to use rspamd to filter mails";
+        };
+
+        servers = mkOption {
+          type = types.listOf types.str;
+          default = ["r:/run/rspamd/rspamd.sock"];
+          description = ''
+            Spamd socket definitions.
+            Is server name is prefixed with r: it is rspamd server.
+          '';
+        };
+
+        whitelist = mkOption {
+          type = types.listOf types.str;
+          default = [ ];
+          description = "list of ips or nets that should be not checked with spamd";
+        };
+
+        rejectMessage = mkOption {
+          type = types.str;
+          default = "Spam message rejected; If this is not spam contact abuse";
+          description = "reject message for spam";
+        };
+
+        extraConfig = mkOption {
+          type = types.lines;
+          default = "";
+          description = "Custom snippet to append to end of `spamd' section";
+        };
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Custom snippet to append to rmilter config";
+      };
+
+      postfix = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Add rmilter to postfix main.conf";
+        };
+
+        configFragment = mkOption {
+          type = types.str;
+          description = "Addon to postfix configuration";
+          default = ''
+            smtpd_milters = ${rmilterSocket}
+            milter_protocol = 6
+            milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen}
+          '';
+        };
+      };
+
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+
+    (mkIf cfg.enable {
+      warnings = [
+        ''`config.services.rmilter' is deprecated, `rmilter' deprecated and unsupported by upstream, and will be removed from next releases. Use built-in rspamd milter instead.''
+      ];
+
+      users.users = singleton {
+        name = cfg.user;
+        description = "rmilter daemon";
+        uid = config.ids.uids.rmilter;
+        group = cfg.group;
+      };
+
+      users.groups = singleton {
+        name = cfg.group;
+        gid = config.ids.gids.rmilter;
+      };
+
+      systemd.services.rmilter = {
+        description = "Rmilter Service";
+
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+
+        serviceConfig = {
+          ExecStart = "${pkgs.rmilter}/bin/rmilter ${optionalString cfg.debug "-d"} -n -c ${rmilterConfigFile}";
+          ExecReload = "${pkgs.coreutils}/bin/kill -USR1 $MAINPID";
+          User = cfg.user;
+          Group = cfg.group;
+          PermissionsStartOnly = true;
+          Restart = "always";
+          RuntimeDirectory = "rmilter";
+          RuntimeDirectoryMode = "0750";
+        };
+
+      };
+
+      systemd.sockets.rmilter = mkIf cfg.socketActivation {
+        description = "Rmilter service socket";
+        wantedBy = [ "sockets.target" ];
+        socketConfig = {
+          ListenStream = systemdSocket;
+          SocketUser = cfg.user;
+          SocketGroup = cfg.group;
+          SocketMode = "0660";
+        };
+      };
+    })
+
+    (mkIf (cfg.enable && cfg.rspamd.enable && rspamdCfg.enable) {
+      users.users.${cfg.user}.extraGroups = [ rspamdCfg.group ];
+    })
+
+    (mkIf (cfg.enable && cfg.postfix.enable) {
+      services.postfix.extraConfig = cfg.postfix.configFragment;
+      users.users.${postfixCfg.user}.extraGroups = [ cfg.group ];
+    })
+  ];
+}
diff --git a/nixpkgs/nixos/modules/services/mail/roundcube.nix b/nixpkgs/nixos/modules/services/mail/roundcube.nix
new file mode 100644
index 000000000000..e8b2e11bf726
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/roundcube.nix
@@ -0,0 +1,171 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.roundcube;
+in
+{
+  options.services.roundcube = {
+    enable = mkOption {
+      type = types.bool;
+      default = false;
+      description = ''
+        Whether to enable roundcube.
+
+        Also enables nginx virtual host management.
+        Further nginx configuration can be done by adapting <literal>services.nginx.virtualHosts.&lt;name&gt;</literal>.
+        See <xref linkend="opt-services.nginx.virtualHosts"/> for further information.
+      '';
+    };
+
+    hostName = mkOption {
+      type = types.str;
+      example = "webmail.example.com";
+      description = "Hostname to use for the nginx vhost";
+    };
+
+    package = mkOption {
+      type = types.package;
+      default = pkgs.roundcube;
+
+      example = literalExample ''
+        roundcube.withPlugins (plugins: [ plugins.persistent_login ])
+      '';
+
+      description = ''
+        The package which contains roundcube's sources. Can be overriden to create
+        an environment which contains roundcube and third-party plugins.
+      '';
+    };
+
+    database = {
+      username = mkOption {
+        type = types.str;
+        default = "roundcube";
+        description = "Username for the postgresql connection";
+      };
+      host = mkOption {
+        type = types.str;
+        default = "localhost";
+        description = ''
+          Host of the postgresql server. If this is not set to
+          <literal>localhost</literal>, you have to create the
+          postgresql user and database yourself, with appropriate
+          permissions.
+        '';
+      };
+      password = mkOption {
+        type = types.str;
+        description = "Password for the postgresql connection";
+      };
+      dbname = mkOption {
+        type = types.str;
+        default = "roundcube";
+        description = "Name of the postgresql database";
+      };
+    };
+
+    plugins = mkOption {
+      type = types.listOf types.str;
+      default = [];
+      description = ''
+        List of roundcube plugins to enable. Currently, only those directly shipped with Roundcube are supported.
+      '';
+    };
+
+    extraConfig = mkOption {
+      type = types.lines;
+      default = "";
+      description = "Extra configuration for roundcube webmail instance";
+    };
+  };
+
+  config = mkIf cfg.enable {
+    environment.etc."roundcube/config.inc.php".text = ''
+      <?php
+
+      $config = array();
+      $config['db_dsnw'] = 'pgsql://${cfg.database.username}:${cfg.database.password}@${cfg.database.host}/${cfg.database.dbname}';
+      $config['log_driver'] = 'syslog';
+      $config['max_message_size'] = '25M';
+      $config['plugins'] = [${concatMapStringsSep "," (p: "'${p}'") cfg.plugins}];
+      ${cfg.extraConfig}
+    '';
+
+    services.nginx = {
+      enable = true;
+      virtualHosts = {
+        ${cfg.hostName} = {
+          forceSSL = mkDefault true;
+          enableACME = mkDefault true;
+          locations."/" = {
+            root = cfg.package;
+            index = "index.php";
+            extraConfig = ''
+              location ~* \.php$ {
+                fastcgi_split_path_info ^(.+\.php)(/.+)$;
+                fastcgi_pass unix:/run/phpfpm/roundcube;
+                include ${pkgs.nginx}/conf/fastcgi_params;
+                include ${pkgs.nginx}/conf/fastcgi.conf;
+              }
+            '';
+          };
+        };
+      };
+    };
+
+    services.postgresql = mkIf (cfg.database.host == "localhost") {
+      enable = true;
+    };
+
+    services.phpfpm.poolConfigs.roundcube = ''
+      listen = /run/phpfpm/roundcube
+      listen.owner = nginx
+      listen.group = nginx
+      listen.mode = 0660
+      user = nginx
+      pm = dynamic
+      pm.max_children = 75
+      pm.start_servers = 2
+      pm.min_spare_servers = 1
+      pm.max_spare_servers = 20
+      pm.max_requests = 500
+      php_admin_value[error_log] = 'stderr'
+      php_admin_flag[log_errors] = on
+      php_admin_value[post_max_size] = 25M
+      php_admin_value[upload_max_filesize] = 25M
+      catch_workers_output = yes
+    '';
+    systemd.services.phpfpm-roundcube.after = [ "roundcube-setup.service" ];
+
+    systemd.services.roundcube-setup = let
+      pgSuperUser = config.services.postgresql.superUser;
+    in mkMerge [
+      (mkIf (cfg.database.host == "localhost") {
+        requires = [ "postgresql.service" ];
+        after = [ "postgresql.service" ];
+        path = [ config.services.postgresql.package ];
+      })
+      {
+        wantedBy = [ "multi-user.target" ];
+        script = ''
+          mkdir -p /var/lib/roundcube
+          if [ ! -f /var/lib/roundcube/db-created ]; then
+            if [ "${cfg.database.host}" = "localhost" ]; then
+              ${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "create role ${cfg.database.username} with login password '${cfg.database.password}'";
+              ${pkgs.sudo}/bin/sudo -u ${pgSuperUser} psql postgres -c "create database ${cfg.database.dbname} with owner ${cfg.database.username}";
+            fi
+            PGPASSWORD=${cfg.database.password} ${pkgs.postgresql}/bin/psql -U ${cfg.database.username} \
+              -f ${cfg.package}/SQL/postgres.initial.sql \
+              -h ${cfg.database.host} ${cfg.database.dbname}
+            touch /var/lib/roundcube/db-created
+          fi
+
+          ${pkgs.php}/bin/php ${cfg.package}/bin/update.sh
+        '';
+        serviceConfig.Type = "oneshot";
+      }
+    ];
+  };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/rspamd.nix b/nixpkgs/nixos/modules/services/mail/rspamd.nix
new file mode 100644
index 000000000000..e59d5715de05
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/rspamd.nix
@@ -0,0 +1,418 @@
+{ config, options, pkgs, lib, ... }:
+
+with lib;
+
+let
+
+  cfg = config.services.rspamd;
+  postfixCfg = config.services.postfix;
+
+  bindSocketOpts = {options, config, ... }: {
+    options = {
+      socket = mkOption {
+        type = types.str;
+        example = "localhost:11333";
+        description = ''
+          Socket for this worker to listen on in a format acceptable by rspamd.
+        '';
+      };
+      mode = mkOption {
+        type = types.str;
+        default = "0644";
+        description = "Mode to set on unix socket";
+      };
+      owner = mkOption {
+        type = types.str;
+        default = "${cfg.user}";
+        description = "Owner to set on unix socket";
+      };
+      group = mkOption {
+        type = types.str;
+        default = "${cfg.group}";
+        description = "Group to set on unix socket";
+      };
+      rawEntry = mkOption {
+        type = types.str;
+        internal = true;
+      };
+    };
+    config.rawEntry = let
+      maybeOption = option:
+        optionalString options.${option}.isDefined " ${option}=${config.${option}}";
+    in
+      if (!(hasPrefix "/" config.socket)) then "${config.socket}"
+      else "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}";
+  };
+
+  traceWarning = w: x: builtins.trace "warning: ${w}" x;
+
+  workerOpts = { name, options, ... }: {
+    options = {
+      enable = mkOption {
+        type = types.nullOr types.bool;
+        default = null;
+        description = "Whether to run the rspamd worker.";
+      };
+      name = mkOption {
+        type = types.nullOr types.str;
+        default = name;
+        description = "Name of the worker";
+      };
+      type = mkOption {
+        type = types.nullOr (types.enum [
+          "normal" "controller" "fuzzy_storage" "rspamd_proxy" "lua" "proxy"
+        ]);
+        description = ''
+          The type of this worker. The type <literal>proxy</literal> is
+          deprecated and only kept for backwards compatibility and should be
+          replaced with <literal>rspamd_proxy</literal>.
+        '';
+        apply = let
+            from = "services.rspamd.workers.\”${name}\".type";
+            files = options.type.files;
+            warning = "The option `${from}` defined in ${showFiles files} has enum value `proxy` which has been renamed to `rspamd_proxy`";
+          in x: if x == "proxy" then traceWarning warning "rspamd_proxy" else x;
+      };
+      bindSockets = mkOption {
+        type = types.listOf (types.either types.str (types.submodule bindSocketOpts));
+        default = [];
+        description = ''
+          List of sockets to listen, in format acceptable by rspamd
+        '';
+        example = [{
+          socket = "/run/rspamd.sock";
+          mode = "0666";
+          owner = "rspamd";
+        } "*:11333"];
+        apply = value: map (each: if (isString each)
+          then if (isUnixSocket each)
+            then {socket = each; owner = cfg.user; group = cfg.group; mode = "0644"; rawEntry = "${each}";}
+            else {socket = each; rawEntry = "${each}";}
+          else each) value;
+      };
+      count = mkOption {
+        type = types.nullOr types.int;
+        default = null;
+        description = ''
+          Number of worker instances to run
+        '';
+      };
+      includes = mkOption {
+        type = types.listOf types.str;
+        default = [];
+        description = ''
+          List of files to include in configuration
+        '';
+      };
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = "Additional entries to put verbatim into worker section of rspamd config file.";
+      };
+    };
+    config = mkIf (name == "normal" || name == "controller" || name == "fuzzy" || name == "rspamd_proxy") {
+      type = mkDefault name;
+      includes = mkDefault [ "$CONFDIR/worker-${if name == "rspamd_proxy" then "proxy" else name}.inc" ];
+      bindSockets =
+        let
+          unixSocket = name: {
+            mode = "0660";
+            socket = "/run/rspamd/${name}.sock";
+            owner = cfg.user;
+            group = cfg.group;
+          };
+        in mkDefault (if name == "normal" then [(unixSocket "rspamd")]
+          else if name == "controller" then [ "localhost:11334" ]
+          else if name == "rspamd_proxy" then [ (unixSocket "proxy") ]
+          else [] );
+    };
+  };
+
+  isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket);
+
+  mkBindSockets = enabled: socks: concatStringsSep "\n  "
+    (flatten (map (each: "bind_socket = \"${each.rawEntry}\";") socks));
+
+  rspamdConfFile = pkgs.writeText "rspamd.conf"
+    ''
+      .include "$CONFDIR/common.conf"
+
+      options {
+        pidfile = "$RUNDIR/rspamd.pid";
+        .include "$CONFDIR/options.inc"
+        .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/options.inc"
+        .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/options.inc"
+      }
+
+      logging {
+        type = "syslog";
+        .include "$CONFDIR/logging.inc"
+        .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/logging.inc"
+        .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/logging.inc"
+      }
+
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: let
+          includeName = if name == "rspamd_proxy" then "proxy" else name;
+          tryOverride = if value.extraConfig == "" then "true" else "false";
+        in ''
+        worker "${value.type}" {
+          type = "${value.type}";
+          ${optionalString (value.enable != null)
+            "enabled = ${if value.enable != false then "yes" else "no"};"}
+          ${mkBindSockets value.enable value.bindSockets}
+          ${optionalString (value.count != null) "count = ${toString value.count};"}
+          ${concatStringsSep "\n  " (map (each: ".include \"${each}\"") value.includes)}
+          .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/worker-${includeName}.inc"
+          .include(try=${tryOverride}; priority=10) "$LOCAL_CONFDIR/override.d/worker-${includeName}.inc"
+        }
+      '') cfg.workers)}
+
+      ${optionalString (cfg.extraConfig != "") ''
+        .include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc"
+      ''}
+   '';
+
+  filterFiles = files: filterAttrs (n: v: v.enable) files;
+  rspamdDir = pkgs.linkFarm "etc-rspamd-dir" (
+    (mapAttrsToList (name: file: { name = "local.d/${name}"; path = file.source; }) (filterFiles cfg.locals)) ++
+    (mapAttrsToList (name: file: { name = "override.d/${name}"; path = file.source; }) (filterFiles cfg.overrides)) ++
+    (optional (cfg.localLuaRules != null) { name = "rspamd.local.lua"; path = cfg.localLuaRules; }) ++
+    [ { name = "rspamd.conf"; path = rspamdConfFile; } ]
+  );
+
+  configFileModule = prefix: { name, config, ... }: {
+    options = {
+      enable = mkOption {
+        type = types.bool;
+        default = true;
+        description = ''
+          Whether this file ${prefix} should be generated.  This
+          option allows specific ${prefix} files to be disabled.
+        '';
+      };
+
+      text = mkOption {
+        default = null;
+        type = types.nullOr types.lines;
+        description = "Text of the file.";
+      };
+
+      source = mkOption {
+        type = types.path;
+        description = "Path of the source file.";
+      };
+    };
+    config = {
+      source = mkIf (config.text != null) (
+        let name' = "rspamd-${prefix}-" + baseNameOf name;
+        in mkDefault (pkgs.writeText name' config.text));
+    };
+  };
+
+  configOverrides =
+    (mapAttrs' (n: v: nameValuePair "worker-${if n == "rspamd_proxy" then "proxy" else n}.inc" {
+      text = v.extraConfig;
+    })
+    (filterAttrs (n: v: v.extraConfig != "") cfg.workers))
+    // (if cfg.extraConfig == "" then {} else {
+      "extra-config.inc".text = cfg.extraConfig;
+    });
+in
+
+{
+
+  ###### interface
+
+  options = {
+
+    services.rspamd = {
+
+      enable = mkEnableOption "rspamd, the Rapid spam filtering system";
+
+      debug = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to run the rspamd daemon in debug mode.";
+      };
+
+      locals = mkOption {
+        type = with types; attrsOf (submodule (configFileModule "locals"));
+        default = {};
+        description = ''
+          Local configuration files, written into <filename>/etc/rspamd/local.d/{name}</filename>.
+        '';
+        example = literalExample ''
+          { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
+            "arc.conf".text = "allow_envfrom_empty = true;";
+          }
+        '';
+      };
+
+      overrides = mkOption {
+        type = with types; attrsOf (submodule (configFileModule "overrides"));
+        default = {};
+        description = ''
+          Overridden configuration files, written into <filename>/etc/rspamd/override.d/{name}</filename>.
+        '';
+        example = literalExample ''
+          { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf";
+            "arc.conf".text = "allow_envfrom_empty = true;";
+          }
+        '';
+      };
+
+      localLuaRules = mkOption {
+        default = null;
+        type = types.nullOr types.path;
+        description = ''
+          Path of file to link to <filename>/etc/rspamd/rspamd.local.lua</filename> for local
+          rules written in Lua
+        '';
+      };
+
+      workers = mkOption {
+        type = with types; attrsOf (submodule workerOpts);
+        description = ''
+          Attribute set of workers to start.
+        '';
+        default = {
+          normal = {};
+          controller = {};
+        };
+        example = literalExample ''
+          {
+            normal = {
+              includes = [ "$CONFDIR/worker-normal.inc" ];
+              bindSockets = [{
+                socket = "/run/rspamd/rspamd.sock";
+                mode = "0660";
+                owner = "${cfg.user}";
+                group = "${cfg.group}";
+              }];
+            };
+            controller = {
+              includes = [ "$CONFDIR/worker-controller.inc" ];
+              bindSockets = [ "[::1]:11334" ];
+            };
+          }
+        '';
+      };
+
+      extraConfig = mkOption {
+        type = types.lines;
+        default = "";
+        description = ''
+          Extra configuration to add at the end of the rspamd configuration
+          file.
+        '';
+      };
+
+      user = mkOption {
+        type = types.string;
+        default = "rspamd";
+        description = ''
+          User to use when no root privileges are required.
+        '';
+      };
+
+      group = mkOption {
+        type = types.string;
+        default = "rspamd";
+        description = ''
+          Group to use when no root privileges are required.
+        '';
+      };
+
+      postfix = {
+        enable = mkOption {
+          type = types.bool;
+          default = false;
+          description = "Add rspamd milter to postfix main.conf";
+        };
+
+        config = mkOption {
+          type = with types; attrsOf (oneOf [ bool str (listOf str) ]);
+          description = ''
+            Addon to postfix configuration
+          '';
+          default = {
+            smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
+            non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
+          };
+          example = {
+            smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
+            non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"];
+          };
+        };
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    services.rspamd.overrides = configOverrides;
+    services.rspamd.workers = mkIf cfg.postfix.enable {
+      controller = {};
+      rspamd_proxy = {
+        bindSockets = [ {
+          mode = "0660";
+          socket = "/run/rspamd/rspamd-milter.sock";
+          owner = cfg.user;
+          group = postfixCfg.group;
+        } ];
+        extraConfig = ''
+          upstream "local" {
+            default = yes; # Self-scan upstreams are always default
+            self_scan = yes; # Enable self-scan
+          }
+        '';
+      };
+    };
+    services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config;
+
+    # Allow users to run 'rspamc' and 'rspamadm'.
+    environment.systemPackages = [ pkgs.rspamd ];
+
+    users.users = singleton {
+      name = cfg.user;
+      description = "rspamd daemon";
+      uid = config.ids.uids.rspamd;
+      group = cfg.group;
+    };
+
+    users.groups = singleton {
+      name = cfg.group;
+      gid = config.ids.gids.rspamd;
+    };
+
+    environment.etc."rspamd".source = rspamdDir;
+
+    systemd.services.rspamd = {
+      description = "Rspamd Service";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+      restartTriggers = [ rspamdDir ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} --user=${cfg.user} --group=${cfg.group} --pid=/run/rspamd.pid -c /etc/rspamd/rspamd.conf -f";
+        Restart = "always";
+        RuntimeDirectory = "rspamd";
+        PrivateTmp = true;
+      };
+
+      preStart = ''
+        ${pkgs.coreutils}/bin/mkdir -p /var/lib/rspamd
+        ${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} /var/lib/rspamd
+      '';
+    };
+  };
+  imports = [
+    (mkRemovedOptionModule [ "services" "rspamd" "socketActivation" ]
+	     "Socket activation never worked correctly and could at this time not be fixed and so was removed")
+    (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ])
+    (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ])
+  ];
+}
diff --git a/nixpkgs/nixos/modules/services/mail/rss2email.nix b/nixpkgs/nixos/modules/services/mail/rss2email.nix
new file mode 100644
index 000000000000..df454abc8267
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/rss2email.nix
@@ -0,0 +1,134 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.rss2email;
+in {
+
+  ###### interface
+
+  options = {
+
+    services.rss2email = {
+
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = "Whether to enable rss2email.";
+      };
+
+      to = mkOption {
+        type = types.str;
+        description = "Mail address to which to send emails";
+      };
+
+      interval = mkOption {
+        type = types.str;
+        default = "12h";
+        description = "How often to check the feeds, in systemd interval format";
+      };
+
+      config = mkOption {
+        type = with types; attrsOf (oneOf [ str int bool ]);
+        default = {};
+        description = ''
+          The configuration to give rss2email.
+
+          Default will use system-wide <literal>sendmail</literal> to send the
+          email. This is rss2email's default when running
+          <literal>r2e new</literal>.
+
+          This set contains key-value associations that will be set in the
+          <literal>[DEFAULT]</literal> block along with the
+          <literal>to</literal> parameter.
+
+          See
+          <literal>https://github.com/rss2email/rss2email/blob/master/r2e.1</literal>
+          for more information on which parameters are accepted.
+        '';
+      };
+
+      feeds = mkOption {
+        description = "The feeds to watch.";
+        type = types.attrsOf (types.submodule {
+          options = {
+            url = mkOption {
+              type = types.str;
+              description = "The URL at which to fetch the feed.";
+            };
+
+            to = mkOption {
+              type = with types; nullOr str;
+              default = null;
+              description = ''
+                Email address to which to send feed items.
+
+                If <literal>null</literal>, this will not be set in the
+                configuration file, and rss2email will make it default to
+                <literal>rss2email.to</literal>.
+              '';
+            };
+          };
+        });
+      };
+    };
+
+  };
+
+
+  ###### implementation
+
+  config = mkIf cfg.enable {
+    users.groups = {
+      rss2email.gid = config.ids.gids.rss2email;
+    };
+
+    users.users = {
+      rss2email = {
+        description = "rss2email user";
+        uid = config.ids.uids.rss2email;
+        group = "rss2email";
+      };
+    };
+
+    services.rss2email.config.to = cfg.to;
+
+    systemd.tmpfiles.rules = [
+      "d /var/rss2email 0700 rss2email rss2email - -"
+    ];
+
+    systemd.services.rss2email = let
+      conf = pkgs.writeText "rss2email.cfg" (lib.generators.toINI {} ({
+          DEFAULT = cfg.config;
+        } // lib.mapAttrs' (name: feed: nameValuePair "feed.${name}" (
+          { inherit (feed) url; } //
+          lib.optionalAttrs (feed.to != null) { inherit (feed) to; }
+        )) cfg.feeds
+      ));
+    in
+    {
+      preStart = ''
+        cp ${conf} /var/rss2email/conf.cfg
+        if [ ! -f /var/rss2email/db.json ]; then
+          echo '{"version":2,"feeds":[]}' > /var/rss2email/db.json
+        fi
+      '';
+      path = [ pkgs.system-sendmail ];
+      serviceConfig = {
+        ExecStart =
+          "${pkgs.rss2email}/bin/r2e -c /var/rss2email/conf.cfg -d /var/rss2email/db.json run";
+        User = "rss2email";
+      };
+    };
+
+    systemd.timers.rss2email = {
+      partOf = [ "rss2email.service" ];
+      wantedBy = [ "timers.target" ];
+      timerConfig.OnBootSec = "0";
+      timerConfig.OnUnitActiveSec = cfg.interval;
+    };
+  };
+
+  meta.maintainers = with lib.maintainers; [ ekleog ];
+}
diff --git a/nixpkgs/nixos/modules/services/mail/spamassassin.nix b/nixpkgs/nixos/modules/services/mail/spamassassin.nix
new file mode 100644
index 000000000000..1fe77ce5a0c7
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/spamassassin.nix
@@ -0,0 +1,199 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+  cfg = config.services.spamassassin;
+  spamassassin-local-cf = pkgs.writeText "local.cf" cfg.config;
+  spamassassin-init-pre = pkgs.writeText "init.pre" cfg.initPreConf;
+
+  spamdEnv = pkgs.buildEnv {
+    name = "spamd-env";
+    paths = [];
+    postBuild = ''
+      ln -sf ${spamassassin-init-pre} $out/init.pre
+      ln -sf ${spamassassin-local-cf} $out/local.cf
+    '';
+  };
+
+in
+
+{
+  options = {
+
+    services.spamassassin = {
+      enable = mkOption {
+        default = false;
+        description = "Whether to run the SpamAssassin daemon";
+      };
+
+      debug = mkOption {
+        default = false;
+        description = "Whether to run the SpamAssassin daemon in debug mode";
+      };
+
+      config = mkOption {
+        type = types.lines;
+        description = ''
+          The SpamAssassin local.cf config
+
+          If you are using this configuration:
+            add_header all Status _YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_
+
+          Then you can Use this sieve filter:
+            require ["fileinto", "reject", "envelope"];
+
+            if header :contains "X-Spam-Flag" "YES" {
+              fileinto "spam";
+            }
+
+          Or this procmail filter:
+            :0:
+            * ^X-Spam-Flag: YES
+            /var/vpopmail/domains/lastlog.de/js/.maildir/.spam/new
+
+          To filter your messages based on the additional mail headers added by spamassassin.
+        '';
+        example = ''
+          #rewrite_header Subject [***** SPAM _SCORE_ *****]
+          required_score          5.0
+          use_bayes               1
+          bayes_auto_learn        1
+          add_header all Status _YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_
+        '';
+        default = "";
+      };
+
+      initPreConf = mkOption {
+        type = types.str;
+        description = "The SpamAssassin init.pre config.";
+        default =
+        ''
+          #
+          # to update this list, run this command in the rules directory:
+          # grep 'loadplugin.*Mail::SpamAssassin::Plugin::.*' -o -h * | sort | uniq
+          #
+
+          #loadplugin Mail::SpamAssassin::Plugin::AccessDB
+          #loadplugin Mail::SpamAssassin::Plugin::AntiVirus
+          loadplugin Mail::SpamAssassin::Plugin::AskDNS
+          # loadplugin Mail::SpamAssassin::Plugin::ASN
+          loadplugin Mail::SpamAssassin::Plugin::AutoLearnThreshold
+          #loadplugin Mail::SpamAssassin::Plugin::AWL
+          loadplugin Mail::SpamAssassin::Plugin::Bayes
+          loadplugin Mail::SpamAssassin::Plugin::BodyEval
+          loadplugin Mail::SpamAssassin::Plugin::Check
+          #loadplugin Mail::SpamAssassin::Plugin::DCC
+          loadplugin Mail::SpamAssassin::Plugin::DKIM
+          loadplugin Mail::SpamAssassin::Plugin::DNSEval
+          loadplugin Mail::SpamAssassin::Plugin::FreeMail
+          loadplugin Mail::SpamAssassin::Plugin::Hashcash
+          loadplugin Mail::SpamAssassin::Plugin::HeaderEval
+          loadplugin Mail::SpamAssassin::Plugin::HTMLEval
+          loadplugin Mail::SpamAssassin::Plugin::HTTPSMismatch
+          loadplugin Mail::SpamAssassin::Plugin::ImageInfo
+          loadplugin Mail::SpamAssassin::Plugin::MIMEEval
+          loadplugin Mail::SpamAssassin::Plugin::MIMEHeader
+          # loadplugin Mail::SpamAssassin::Plugin::PDFInfo
+          #loadplugin Mail::SpamAssassin::Plugin::PhishTag
+          loadplugin Mail::SpamAssassin::Plugin::Pyzor
+          loadplugin Mail::SpamAssassin::Plugin::Razor2
+          # loadplugin Mail::SpamAssassin::Plugin::RelayCountry
+          loadplugin Mail::SpamAssassin::Plugin::RelayEval
+          loadplugin Mail::SpamAssassin::Plugin::ReplaceTags
+          # loadplugin Mail::SpamAssassin::Plugin::Rule2XSBody
+          # loadplugin Mail::SpamAssassin::Plugin::Shortcircuit
+          loadplugin Mail::SpamAssassin::Plugin::SpamCop
+          loadplugin Mail::SpamAssassin::Plugin::SPF
+          #loadplugin Mail::SpamAssassin::Plugin::TextCat
+          # loadplugin Mail::SpamAssassin::Plugin::TxRep
+          loadplugin Mail::SpamAssassin::Plugin::URIDetail
+          loadplugin Mail::SpamAssassin::Plugin::URIDNSBL
+          loadplugin Mail::SpamAssassin::Plugin::URIEval
+          # loadplugin Mail::SpamAssassin::Plugin::URILocalBL
+          loadplugin Mail::SpamAssassin::Plugin::VBounce
+          loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject
+          loadplugin Mail::SpamAssassin::Plugin::WLBLEval
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    # Allow users to run 'spamc'.
+
+    environment = {
+      etc = singleton { source = spamdEnv; target = "spamassassin"; };
+      systemPackages = [ pkgs.spamassassin ];
+    };
+
+    users.users = singleton {
+      name = "spamd";
+      description = "Spam Assassin Daemon";
+      uid = config.ids.uids.spamd;
+      group = "spamd";
+    };
+
+    users.groups = singleton {
+      name = "spamd";
+      gid = config.ids.gids.spamd;
+    };
+
+    systemd.services.sa-update = {
+      script = ''
+        set +e
+        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/ --siteconfigpath=${spamdEnv}/" spamd
+
+        v=$?
+        set -e
+        if [ $v -gt 1 ]; then
+          echo "sa-update execution error"
+          exit $v
+        fi
+        if [ $v -eq 0 ]; then
+          systemctl reload spamd.service
+        fi
+      '';
+    };
+
+    systemd.timers.sa-update = {
+      description = "sa-update-service";
+      partOf      = [ "sa-update.service" ];
+      wantedBy    = [ "timers.target" ];
+      timerConfig = {
+        OnCalendar = "1:*";
+        Persistent = true;
+      };
+    };
+
+    systemd.services.spamd = {
+      description = "Spam Assassin Server";
+
+      wantedBy = [ "multi-user.target" ];
+      after = [ "network.target" ];
+
+      serviceConfig = {
+        ExecStart = "${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --siteconfigpath=${spamdEnv} --virtual-config-dir=/var/lib/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid";
+        ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      };
+
+      # 0 and 1 no error, exitcode > 1 means error:
+      # https://spamassassin.apache.org/full/3.1.x/doc/sa-update.html#exit_codes
+      preStart = ''
+        echo "Recreating '/var/lib/spamasassin' with creating '3.004001' (or similar) and 'sa-update-keys'"
+        mkdir -p /var/lib/spamassassin
+        chown spamd:spamd /var/lib/spamassassin -R
+        set +e
+        ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/ --siteconfigpath=${spamdEnv}/" spamd
+        v=$?
+        set -e
+        if [ $v -gt 1 ]; then
+          echo "sa-update execution error"
+          exit $v
+        fi
+        chown spamd:spamd /var/lib/spamassassin -R
+      '';
+    };
+  };
+}