aboutsummaryrefslogtreecommitdiff
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.nix400
-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.nix61
-rw-r--r--nixpkgs/nixos/modules/services/mail/mailhog.nix43
-rw-r--r--nixpkgs/nixos/modules/services/mail/mailman.nix297
-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/roundcube.nix175
-rw-r--r--nixpkgs/nixos/modules/services/mail/rspamd.nix418
-rw-r--r--nixpkgs/nixos/modules/services/mail/rss2email.nix133
-rw-r--r--nixpkgs/nixos/modules/services/mail/spamassassin.nix199
24 files changed, 4541 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 00000000000..fc1267c5d28
--- /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 00000000000..374a3dd75c1
--- /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 00000000000..f4ac9e47007
--- /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 00000000000..cdbb776454b
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/dovecot.nix
@@ -0,0 +1,400 @@
+{ 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 = [ "network.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 00000000000..72b8c4c08b9
--- /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 00000000000..47812dd1e40
--- /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.lines;
+ default = "";
+ description = ''
+ Verbatim Exim configuration. This should not contain exim_user,
+ exim_group, exim_path, or spool_directory.
+ '';
+ };
+
+ user = mkOption {
+ type = types.str;
+ 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.str;
+ default = "exim";
+ description = ''
+ Group to use when no root privileges are required.
+ '';
+ };
+
+ spoolDir = mkOption {
+ type = types.path;
+ 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 00000000000..5b729ca50a5
--- /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 00000000000..fed313e4738
--- /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 00000000000..f5b4508b335
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/mailcatcher.nix
@@ -0,0 +1,61 @@
+{ config, pkgs, lib, ... }:
+
+let
+ cfg = config.services.mailcatcher;
+
+ inherit (lib) mkEnableOption mkIf mkOption types optionalString;
+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}";
+ AmbientCapabilities = optionalString (cfg.http.port < 1024 || cfg.smtp.port < 1024) "cap_net_bind_service";
+ };
+ };
+ };
+}
diff --git a/nixpkgs/nixos/modules/services/mail/mailhog.nix b/nixpkgs/nixos/modules/services/mail/mailhog.nix
new file mode 100644
index 00000000000..b78f4c8e0e6
--- /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/mailman.nix b/nixpkgs/nixos/modules/services/mail/mailman.nix
new file mode 100644
index 00000000000..e917209f3d1
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/mailman.nix
@@ -0,0 +1,297 @@
+{ config, pkgs, lib, ... }: # mailman.nix
+
+with lib;
+
+let
+
+ cfg = config.services.mailman;
+
+ mailmanPyEnv = pkgs.python3.withPackages (ps: with ps; [mailman mailman-hyperkitty]);
+
+ mailmanExe = with pkgs; stdenv.mkDerivation {
+ name = "mailman-" + python3Packages.mailman.version;
+ buildInputs = [makeWrapper];
+ unpackPhase = ":";
+ installPhase = ''
+ mkdir -p $out/bin
+ makeWrapper ${mailmanPyEnv}/bin/mailman $out/bin/mailman \
+ --set MAILMAN_CONFIG_FILE /etc/mailman.cfg
+ '';
+ };
+
+ mailmanWeb = pkgs.python3Packages.mailman-web.override {
+ serverEMail = cfg.siteOwner;
+ archiverKey = cfg.hyperkittyApiKey;
+ allowedHosts = cfg.webHosts;
+ };
+
+ mailmanWebPyEnv = pkgs.python3.withPackages (x: with x; [mailman-web]);
+
+ mailmanWebExe = with pkgs; stdenv.mkDerivation {
+ inherit (mailmanWeb) name;
+ buildInputs = [makeWrapper];
+ unpackPhase = ":";
+ installPhase = ''
+ mkdir -p $out/bin
+ makeWrapper ${mailmanWebPyEnv}/bin/django-admin $out/bin/mailman-web \
+ --set DJANGO_SETTINGS_MODULE settings
+ '';
+ };
+
+ mailmanCfg = ''
+ [mailman]
+ site_owner: ${cfg.siteOwner}
+ layout: fhs
+
+ [paths.fhs]
+ bin_dir: ${pkgs.python3Packages.mailman}/bin
+ var_dir: /var/lib/mailman
+ queue_dir: $var_dir/queue
+ template_dir: $var_dir/templates
+ log_dir: $var_dir/log
+ lock_dir: $var_dir/lock
+ etc_dir: /etc
+ ext_dir: $etc_dir/mailman.d
+ pid_file: /run/mailman/master.pid
+ '' + optionalString (cfg.hyperkittyApiKey != null) ''
+ [archiver.hyperkitty]
+ class: mailman_hyperkitty.Archiver
+ enable: yes
+ configuration: ${pkgs.writeText "mailman-hyperkitty.cfg" mailmanHyperkittyCfg}
+ '';
+
+ mailmanHyperkittyCfg = ''
+ [general]
+ # This is your HyperKitty installation, preferably on the localhost. This
+ # address will be used by Mailman to forward incoming emails to HyperKitty
+ # for archiving. It does not need to be publicly available, in fact it's
+ # better if it is not.
+ base_url: ${cfg.hyperkittyBaseUrl}
+
+ # Shared API key, must be the identical to the value in HyperKitty's
+ # settings.
+ api_key: ${cfg.hyperkittyApiKey}
+ '';
+
+in {
+
+ ###### interface
+
+ options = {
+
+ services.mailman = {
+
+ enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = "Enable Mailman on this host. Requires an active Postfix installation.";
+ };
+
+ siteOwner = mkOption {
+ type = types.str;
+ default = "postmaster@example.org";
+ description = ''
+ Certain messages that must be delivered to a human, but which can't
+ be delivered to a list owner (e.g. a bounce from a list owner), will
+ be sent to this address. It should point to a human.
+ '';
+ };
+
+ webRoot = mkOption {
+ type = types.path;
+ default = "${mailmanWeb}/${pkgs.python3.sitePackages}";
+ defaultText = "pkgs.python3Packages.mailman-web";
+ description = ''
+ The web root for the Hyperkity + Postorius apps provided by Mailman.
+ This variable can be set, of course, but it mainly exists so that site
+ admins can refer to it in their own hand-written httpd configuration files.
+ '';
+ };
+
+ webHosts = mkOption {
+ type = types.listOf types.str;
+ default = [];
+ description = ''
+ The list of hostnames and/or IP addresses from which the Mailman Web
+ UI will accept requests. By default, "localhost" and "127.0.0.1" are
+ enabled. All additional names under which your web server accepts
+ requests for the UI must be listed here or incoming requests will be
+ rejected.
+ '';
+ };
+
+ hyperkittyBaseUrl = mkOption {
+ type = types.str;
+ default = "http://localhost/hyperkitty/";
+ description = ''
+ Where can Mailman connect to Hyperkitty's internal API, preferably on
+ localhost?
+ '';
+ };
+
+ hyperkittyApiKey = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = ''
+ The shared secret used to authenticate Mailman's internal
+ communication with Hyperkitty. Must be set to enable support for the
+ Hyperkitty archiver. Note that this secret is going to be visible to
+ all local users in the Nix store.
+ '';
+ };
+
+ };
+ };
+
+ ###### implementation
+
+ config = mkIf cfg.enable {
+
+ assertions = [
+ { assertion = cfg.enable -> config.services.postfix.enable;
+ message = "Mailman requires Postfix";
+ }
+ ];
+
+ users.users.mailman = { description = "GNU Mailman"; isSystemUser = true; };
+
+ environment = {
+ systemPackages = [ mailmanExe mailmanWebExe pkgs.sassc ];
+ etc."mailman.cfg".text = mailmanCfg;
+ };
+
+ services.postfix = {
+ relayDomains = [ "hash:/var/lib/mailman/data/postfix_domains" ];
+ recipientDelimiter = "+"; # bake recipient addresses in mail envelopes via VERP
+ config = {
+ transport_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
+ local_recipient_maps = [ "hash:/var/lib/mailman/data/postfix_lmtp" ];
+ owner_request_special = "no"; # Mailman handles -owner addresses on its own
+ };
+ };
+
+ systemd.services.mailman = {
+ description = "GNU Mailman Master Process";
+ after = [ "network.target" ];
+ wantedBy = [ "multi-user.target" ];
+ serviceConfig = {
+ ExecStart = "${mailmanExe}/bin/mailman start";
+ ExecStop = "${mailmanExe}/bin/mailman stop";
+ User = "mailman";
+ Type = "forking";
+ StateDirectory = "mailman";
+ StateDirectoryMode = "0700";
+ RuntimeDirectory = "mailman";
+ PIDFile = "/run/mailman/master.pid";
+ };
+ };
+
+ systemd.services.mailman-web = {
+ description = "Init Postorius DB";
+ before = [ "httpd.service" ];
+ requiredBy = [ "httpd.service" ];
+ script = ''
+ ${mailmanWebExe}/bin/mailman-web migrate
+ rm -rf static
+ ${mailmanWebExe}/bin/mailman-web collectstatic
+ ${mailmanWebExe}/bin/mailman-web compress
+ '';
+ serviceConfig = {
+ User = config.services.httpd.user;
+ Type = "oneshot";
+ StateDirectory = "mailman-web";
+ StateDirectoryMode = "0700";
+ WorkingDirectory = "/var/lib/mailman-web";
+ };
+ };
+
+ systemd.services.mailman-daily = {
+ description = "Trigger daily Mailman events";
+ startAt = "daily";
+ serviceConfig = {
+ ExecStart = "${mailmanExe}/bin/mailman digests --send";
+ User = "mailman";
+ };
+ };
+
+ systemd.services.hyperkitty = {
+ enable = cfg.hyperkittyApiKey != null;
+ description = "GNU Hyperkitty QCluster Process";
+ after = [ "network.target" ];
+ wantedBy = [ "mailman.service" "multi-user.target" ];
+ serviceConfig = {
+ ExecStart = "${mailmanWebExe}/bin/mailman-web qcluster";
+ User = config.services.httpd.user;
+ WorkingDirectory = "/var/lib/mailman-web";
+ };
+ };
+
+ systemd.services.hyperkitty-minutely = {
+ enable = cfg.hyperkittyApiKey != null;
+ description = "Trigger minutely Hyperkitty events";
+ startAt = "minutely";
+ serviceConfig = {
+ ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs minutely";
+ User = config.services.httpd.user;
+ WorkingDirectory = "/var/lib/mailman-web";
+ };
+ };
+
+ systemd.services.hyperkitty-quarter-hourly = {
+ enable = cfg.hyperkittyApiKey != null;
+ description = "Trigger quarter-hourly Hyperkitty events";
+ startAt = "*:00/15";
+ serviceConfig = {
+ ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs quarter_hourly";
+ User = config.services.httpd.user;
+ WorkingDirectory = "/var/lib/mailman-web";
+ };
+ };
+
+ systemd.services.hyperkitty-hourly = {
+ enable = cfg.hyperkittyApiKey != null;
+ description = "Trigger hourly Hyperkitty events";
+ startAt = "hourly";
+ serviceConfig = {
+ ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs hourly";
+ User = config.services.httpd.user;
+ WorkingDirectory = "/var/lib/mailman-web";
+ };
+ };
+
+ systemd.services.hyperkitty-daily = {
+ enable = cfg.hyperkittyApiKey != null;
+ description = "Trigger daily Hyperkitty events";
+ startAt = "daily";
+ serviceConfig = {
+ ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs daily";
+ User = config.services.httpd.user;
+ WorkingDirectory = "/var/lib/mailman-web";
+ };
+ };
+
+ systemd.services.hyperkitty-weekly = {
+ enable = cfg.hyperkittyApiKey != null;
+ description = "Trigger weekly Hyperkitty events";
+ startAt = "weekly";
+ serviceConfig = {
+ ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs weekly";
+ User = config.services.httpd.user;
+ WorkingDirectory = "/var/lib/mailman-web";
+ };
+ };
+
+ systemd.services.hyperkitty-yearly = {
+ enable = cfg.hyperkittyApiKey != null;
+ description = "Trigger yearly Hyperkitty events";
+ startAt = "yearly";
+ serviceConfig = {
+ ExecStart = "${mailmanWebExe}/bin/mailman-web runjobs yearly";
+ User = config.services.httpd.user;
+ WorkingDirectory = "/var/lib/mailman-web";
+ };
+ };
+
+ };
+
+}
diff --git a/nixpkgs/nixos/modules/services/mail/mlmmj.nix b/nixpkgs/nixos/modules/services/mail/mlmmj.nix
new file mode 100644
index 00000000000..7ae00f3e501
--- /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 00000000000..2c2910e0aa9
--- /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.str;
+ default = "nullmailer";
+ description = ''
+ User to use to run nullmailer-send.
+ '';
+ };
+
+ group = mkOption {
+ type = types.str;
+ 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 00000000000..294e3806f94
--- /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 00000000000..253823cbaf9
--- /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 00000000000..a870550ba50
--- /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 00000000000..38984f896d6
--- /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 00000000000..d5fd76da970
--- /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 str);
+ default = null;
+ description = ''
+ List of accepted local users. Specify a bare username, an
+ <literal>"@domain.tld"</literal> wild-card, or a complete
+ <literal>"user@domain.tld"</literal> address. If set, these names end
+ up in the local recipient map -- see the local(8) man-page -- and
+ effectively replace the system user database lookup that's otherwise
+ used by default.
+ '';
+ };
+
+ transport = mkOption {
+ default = "";
+ description = "
+ Entries for the transport map, cf. man-page transport(8).
+ ";
+ };
+
+ dnsBlacklists = mkOption {
+ default = [];
+ type = with types; listOf str;
+ description = "dns blacklist servers to use with smtpd_client_restrictions";
+ };
+
+ dnsBlacklistOverrides = mkOption {
+ default = "";
+ 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 00000000000..88fb7f0b4ad
--- /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 str;
+ 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 = str;
+ 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 = str;
+ 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 = str;
+ default = "DEFER_IF_PERMIT";
+ description = "Response status for greylisted messages (see access(5))";
+ };
+ greylistHeader = mkOption {
+ type = str;
+ 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 str 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 00000000000..8f12a16906c
--- /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/roundcube.nix b/nixpkgs/nixos/modules/services/mail/roundcube.nix
new file mode 100644
index 00000000000..bdedfa1bb70
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/roundcube.nix
@@ -0,0 +1,175 @@
+{ lib, config, pkgs, ... }:
+
+with lib;
+
+let
+ cfg = config.services.roundcube;
+ fpm = config.services.phpfpm.pools.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:${fpm.socket};
+ include ${pkgs.nginx}/conf/fastcgi_params;
+ include ${pkgs.nginx}/conf/fastcgi.conf;
+ }
+ '';
+ };
+ };
+ };
+ };
+
+ services.postgresql = mkIf (cfg.database.host == "localhost") {
+ enable = true;
+ };
+
+ services.phpfpm.pools.roundcube = {
+ user = "nginx";
+ phpOptions = ''
+ error_log = 'stderr'
+ log_errors = on
+ post_max_size = 25M
+ upload_max_filesize = 25M
+ '';
+ settings = mapAttrs (name: mkDefault) {
+ "listen.owner" = "nginx";
+ "listen.group" = "nginx";
+ "listen.mode" = "0660";
+ "pm" = "dynamic";
+ "pm.max_children" = 75;
+ "pm.start_servers" = 2;
+ "pm.min_spare_servers" = 1;
+ "pm.max_spare_servers" = 20;
+ "pm.max_requests" = 500;
+ "catch_workers_output" = true;
+ };
+ };
+ 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 00000000000..4db35d9e89a
--- /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.str;
+ default = "rspamd";
+ description = ''
+ User to use when no root privileges are required.
+ '';
+ };
+
+ group = mkOption {
+ type = types.str;
+ 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 00000000000..c1e5964c453
--- /dev/null
+++ b/nixpkgs/nixos/modules/services/mail/rss2email.nix
@@ -0,0 +1,133 @@
+{ 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>man r2e</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 00000000000..1fe77ce5a0c
--- /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
+ '';
+ };
+ };
+}