diff options
Diffstat (limited to 'nixpkgs/nixos/modules/services/mail')
23 files changed, 4244 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/mail/clamsmtp.nix b/nixpkgs/nixos/modules/services/mail/clamsmtp.nix new file mode 100644 index 000000000000..fc1267c5d280 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/clamsmtp.nix @@ -0,0 +1,181 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.clamsmtp; + clamdSocket = "/run/clamav/clamd.ctl"; # See services/security/clamav.nix +in +{ + ##### interface + options = { + services.clamsmtp = { + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable clamsmtp."; + }; + + instances = mkOption { + description = "Instances of clamsmtp to run."; + type = types.listOf (types.submodule { options = { + action = mkOption { + type = types.enum [ "bounce" "drop" "pass" ]; + default = "drop"; + description = + '' + Action to take when a virus is detected. + + Note that viruses often spoof sender addresses, so bouncing is + in most cases not a good idea. + ''; + }; + + header = mkOption { + type = types.str; + default = ""; + example = "X-Virus-Scanned: ClamAV using ClamSMTP"; + description = + '' + A header to add to scanned messages. See clamsmtpd.conf(5) for + more details. Empty means no header. + ''; + }; + + keepAlives = mkOption { + type = types.int; + default = 0; + description = + '' + Number of seconds to wait between each NOOP sent to the sending + server. 0 to disable. + + This is meant for slow servers where the sending MTA times out + waiting for clamd to scan the file. + ''; + }; + + listen = mkOption { + type = types.str; + example = "127.0.0.1:10025"; + description = + '' + Address to wait for incoming SMTP connections on. See + clamsmtpd.conf(5) for more details. + ''; + }; + + quarantine = mkOption { + type = types.bool; + default = false; + description = + '' + Whether to quarantine files that contain viruses by leaving them + in the temporary directory. + ''; + }; + + maxConnections = mkOption { + type = types.int; + default = 64; + description = "Maximum number of connections to accept at once."; + }; + + outAddress = mkOption { + type = types.str; + description = + '' + Address of the SMTP server to send email to once it has been + scanned. + ''; + }; + + tempDirectory = mkOption { + type = types.str; + default = "/tmp"; + description = + '' + Temporary directory that needs to be accessible to both clamd + and clamsmtpd. + ''; + }; + + timeout = mkOption { + type = types.int; + default = 180; + description = "Time-out for network connections."; + }; + + transparentProxy = mkOption { + type = types.bool; + default = false; + description = "Enable clamsmtp's transparent proxy support."; + }; + + virusAction = mkOption { + type = with types; nullOr path; + default = null; + description = + '' + Command to run when a virus is found. Please see VIRUS ACTION in + clamsmtpd(8) for a discussion of this option and its safe use. + ''; + }; + + xClient = mkOption { + type = types.bool; + default = false; + description = + '' + Send the XCLIENT command to the receiving server, for forwarding + client addresses and connection information if the receiving + server supports this feature. + ''; + }; + };}); + }; + }; + }; + + ##### implementation + config = let + configfile = conf: pkgs.writeText "clamsmtpd.conf" + '' + Action: ${conf.action} + ClamAddress: ${clamdSocket} + Header: ${conf.header} + KeepAlives: ${toString conf.keepAlives} + Listen: ${conf.listen} + Quarantine: ${if conf.quarantine then "on" else "off"} + MaxConnections: ${toString conf.maxConnections} + OutAddress: ${conf.outAddress} + TempDirectory: ${conf.tempDirectory} + TimeOut: ${toString conf.timeout} + TransparentProxy: ${if conf.transparentProxy then "on" else "off"} + User: clamav + ${optionalString (conf.virusAction != null) "VirusAction: ${conf.virusAction}"} + XClient: ${if conf.xClient then "on" else "off"} + ''; + in + mkIf cfg.enable { + assertions = [ + { assertion = config.services.clamav.daemon.enable; + message = "clamsmtp requires clamav to be enabled"; + } + ]; + + systemd.services = listToAttrs (imap1 (i: conf: + nameValuePair "clamsmtp-${toString i}" { + description = "ClamSMTP instance ${toString i}"; + wantedBy = [ "multi-user.target" ]; + script = "exec ${pkgs.clamsmtp}/bin/clamsmtpd -f ${configfile conf}"; + after = [ "clamav-daemon.service" ]; + requires = [ "clamav-daemon.service" ]; + serviceConfig.Type = "forking"; + serviceConfig.PrivateTmp = "yes"; + unitConfig.JoinsNamespaceOf = "clamav-daemon.service"; + } + ) cfg.instances); + }; + + meta.maintainers = with lib.maintainers; [ ekleog ]; +} diff --git a/nixpkgs/nixos/modules/services/mail/davmail.nix b/nixpkgs/nixos/modules/services/mail/davmail.nix new file mode 100644 index 000000000000..374a3dd75c1c --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/davmail.nix @@ -0,0 +1,99 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.davmail; + + configType = with types; + oneOf [ (attrsOf configType) str int bool ] // { + description = "davmail config type (str, int, bool or attribute set thereof)"; + }; + + toStr = val: if isBool val then boolToString val else toString val; + + linesForAttrs = attrs: concatMap (name: let value = attrs.${name}; in + if isAttrs value + then map (line: name + "." + line) (linesForAttrs value) + else [ "${name}=${toStr value}" ] + ) (attrNames attrs); + + configFile = pkgs.writeText "davmail.properties" (concatStringsSep "\n" (linesForAttrs cfg.config)); + +in + + { + options.services.davmail = { + enable = mkEnableOption "davmail, an MS Exchange gateway"; + + url = mkOption { + type = types.str; + description = "Outlook Web Access URL to access the exchange server, i.e. the base webmail URL."; + example = "https://outlook.office365.com/EWS/Exchange.asmx"; + }; + + config = mkOption { + type = configType; + default = {}; + description = '' + Davmail configuration. Refer to + <link xlink:href="http://davmail.sourceforge.net/serversetup.html"/> + and <link xlink:href="http://davmail.sourceforge.net/advanced.html"/> + for details on supported values. + ''; + example = literalExample '' + { + davmail.allowRemote = true; + davmail.imapPort = 55555; + davmail.bindAddress = "10.0.1.2"; + davmail.smtpSaveInSent = true; + davmail.folderSizeLimit = 10; + davmail.caldavAutoSchedule = false; + log4j.logger.rootLogger = "DEBUG"; + } + ''; + }; + }; + + config = mkIf cfg.enable { + + services.davmail.config = { + davmail = mapAttrs (name: mkDefault) { + server = true; + disableUpdateCheck = true; + logFilePath = "/var/log/davmail/davmail.log"; + logFileSize = "1MB"; + mode = "auto"; + url = cfg.url; + caldavPort = 1080; + imapPort = 1143; + ldapPort = 1389; + popPort = 1110; + smtpPort = 1025; + }; + log4j = { + logger.davmail = mkDefault "WARN"; + logger.httpclient.wire = mkDefault "WARN"; + logger.org.apache.commons.httpclient = mkDefault "WARN"; + rootLogger = mkDefault "WARN"; + }; + }; + + systemd.services.davmail = { + description = "DavMail POP/IMAP/SMTP Exchange Gateway"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.davmail}/bin/davmail ${configFile}"; + Restart = "on-failure"; + DynamicUser = "yes"; + LogsDirectory = "davmail"; + }; + }; + + environment.systemPackages = [ pkgs.davmail ]; + }; + } diff --git a/nixpkgs/nixos/modules/services/mail/dkimproxy-out.nix b/nixpkgs/nixos/modules/services/mail/dkimproxy-out.nix new file mode 100644 index 000000000000..f4ac9e47007a --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/dkimproxy-out.nix @@ -0,0 +1,120 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.dkimproxy-out; + keydir = "/var/lib/dkimproxy-out"; + privkey = "${keydir}/private.key"; + pubkey = "${keydir}/public.key"; +in +{ + ##### interface + options = { + services.dkimproxy-out = { + enable = mkOption { + type = types.bool; + default = false; + description = + '' + Whether to enable dkimproxy_out. + + Note that a key will be auto-generated, and can be found in + ${keydir}. + ''; + }; + + listen = mkOption { + type = types.str; + example = "127.0.0.1:10027"; + description = "Address:port DKIMproxy should listen on."; + }; + + relay = mkOption { + type = types.str; + example = "127.0.0.1:10028"; + description = "Address:port DKIMproxy should forward mail to."; + }; + + domains = mkOption { + type = with types; listOf str; + example = [ "example.org" "example.com" ]; + description = "List of domains DKIMproxy can sign for."; + }; + + selector = mkOption { + type = types.str; + example = "selector1"; + description = + '' + The selector to use for DKIM key identification. + + For example, if 'selector1' is used here, then for each domain + 'example.org' given in `domain`, 'selector1._domainkey.example.org' + should contain the TXT record indicating the public key is the one + in ${pubkey}: "v=DKIM1; t=s; p=[THE PUBLIC KEY]". + ''; + }; + + keySize = mkOption { + type = types.int; + default = 2048; + description = + '' + Size of the RSA key to use to sign outgoing emails. Note that the + maximum mandatorily verified as per RFC6376 is 2048. + ''; + }; + + # TODO: allow signature for other schemes than dkim(c=relaxed/relaxed)? + # This being the scheme used by gmail, maybe nothing more is needed for + # reasonable use. + }; + }; + + ##### implementation + config = let + configfile = pkgs.writeText "dkimproxy_out.conf" + '' + listen ${cfg.listen} + relay ${cfg.relay} + + domain ${concatStringsSep "," cfg.domains} + selector ${cfg.selector} + + signature dkim(c=relaxed/relaxed) + + keyfile ${privkey} + ''; + in + mkIf cfg.enable { + users.groups.dkimproxy-out = {}; + users.users.dkimproxy-out = { + description = "DKIMproxy_out daemon"; + group = "dkimproxy-out"; + isSystemUser = true; + }; + + systemd.services.dkimproxy-out = { + description = "DKIMproxy_out"; + wantedBy = [ "multi-user.target" ]; + preStart = '' + if [ ! -d "${keydir}" ]; then + mkdir -p "${keydir}" + chmod 0700 "${keydir}" + ${pkgs.openssl}/bin/openssl genrsa -out "${privkey}" ${toString cfg.keySize} + ${pkgs.openssl}/bin/openssl rsa -in "${privkey}" -pubout -out "${pubkey}" + chown -R dkimproxy-out:dkimproxy-out "${keydir}" + fi + ''; + script = '' + exec ${pkgs.dkimproxy}/bin/dkimproxy.out --conf_file=${configfile} + ''; + serviceConfig = { + User = "dkimproxy-out"; + PermissionsStartOnly = true; + }; + }; + }; + + meta.maintainers = with lib.maintainers; [ ekleog ]; +} diff --git a/nixpkgs/nixos/modules/services/mail/dovecot.nix b/nixpkgs/nixos/modules/services/mail/dovecot.nix new file mode 100644 index 000000000000..cdbb776454b6 --- /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 000000000000..72b8c4c08b92 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/dspam.nix @@ -0,0 +1,150 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.dspam; + + dspam = pkgs.dspam; + + defaultSock = "/run/dspam/dspam.sock"; + + cfgfile = pkgs.writeText "dspam.conf" '' + Home /var/lib/dspam + StorageDriver ${dspam}/lib/dspam/lib${cfg.storageDriver}_drv.so + + Trust root + Trust ${cfg.user} + SystemLog on + UserLog on + + ${optionalString (cfg.domainSocket != null) '' + ServerDomainSocketPath "${cfg.domainSocket}" + ClientHost "${cfg.domainSocket}" + ''} + + ${cfg.extraConfig} + ''; + +in { + + ###### interface + + options = { + + services.dspam = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the dspam spam filter."; + }; + + user = mkOption { + type = types.str; + default = "dspam"; + description = "User for the dspam daemon."; + }; + + group = mkOption { + type = types.str; + default = "dspam"; + description = "Group for the dspam daemon."; + }; + + storageDriver = mkOption { + type = types.str; + default = "hash"; + description = "Storage driver backend to use for dspam."; + }; + + domainSocket = mkOption { + type = types.nullOr types.path; + default = defaultSock; + description = "Path to local domain socket which is used for communication with the daemon. Set to null to disable UNIX socket."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Additional dspam configuration."; + }; + + maintenanceInterval = mkOption { + type = types.nullOr types.str; + default = null; + description = "If set, maintenance script will be run at specified (in systemd.timer format) interval"; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable (mkMerge [ + { + users.users = optionalAttrs (cfg.user == "dspam") (singleton + { name = "dspam"; + group = cfg.group; + uid = config.ids.uids.dspam; + }); + + users.groups = optionalAttrs (cfg.group == "dspam") (singleton + { name = "dspam"; + gid = config.ids.gids.dspam; + }); + + environment.systemPackages = [ dspam ]; + + environment.etc."dspam/dspam.conf".source = cfgfile; + + systemd.services.dspam = { + description = "dspam spam filtering daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "postgresql.service" ]; + restartTriggers = [ cfgfile ]; + + serviceConfig = { + ExecStart = "${dspam}/bin/dspam --daemon --nofork"; + User = cfg.user; + Group = cfg.group; + RuntimeDirectory = optional (cfg.domainSocket == defaultSock) "dspam"; + RuntimeDirectoryMode = optional (cfg.domainSocket == defaultSock) "0750"; + StateDirectory = "dspam"; + StateDirectoryMode = "0750"; + LogsDirectory = "dspam"; + LogsDirectoryMode = "0750"; + # DSPAM segfaults on just about every error + Restart = "on-abort"; + RestartSec = "1s"; + }; + }; + } + + (mkIf (cfg.maintenanceInterval != null) { + systemd.timers.dspam-maintenance = { + description = "Timer for dspam maintenance script"; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = cfg.maintenanceInterval; + Unit = "dspam-maintenance.service"; + }; + }; + + systemd.services.dspam-maintenance = { + description = "dspam maintenance script"; + restartTriggers = [ cfgfile ]; + + serviceConfig = { + ExecStart = "${dspam}/bin/dspam_maintenance --verbose"; + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + }; + }; + }) + ]); +} diff --git a/nixpkgs/nixos/modules/services/mail/exim.nix b/nixpkgs/nixos/modules/services/mail/exim.nix new file mode 100644 index 000000000000..c05811291359 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/exim.nix @@ -0,0 +1,122 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) mkIf mkOption singleton types; + inherit (pkgs) coreutils; + cfg = config.services.exim; +in + +{ + + ###### interface + + options = { + + services.exim = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the Exim mail transfer agent."; + }; + + config = mkOption { + type = types.string; + default = ""; + description = '' + Verbatim Exim configuration. This should not contain exim_user, + exim_group, exim_path, or spool_directory. + ''; + }; + + user = mkOption { + type = types.string; + default = "exim"; + description = '' + User to use when no root privileges are required. + In particular, this applies when receiving messages and when doing + remote deliveries. (Local deliveries run as various non-root users, + typically as the owner of a local mailbox.) Specifying this value + as root is not supported. + ''; + }; + + group = mkOption { + type = types.string; + default = "exim"; + description = '' + Group to use when no root privileges are required. + ''; + }; + + spoolDir = mkOption { + type = types.string; + default = "/var/spool/exim"; + description = '' + Location of the spool directory of exim. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.exim; + defaultText = "pkgs.exim"; + description = '' + The Exim derivation to use. + This can be used to enable features such as LDAP or PAM support. + ''; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + environment = { + etc."exim.conf".text = '' + exim_user = ${cfg.user} + exim_group = ${cfg.group} + exim_path = /run/wrappers/bin/exim + spool_directory = ${cfg.spoolDir} + ${cfg.config} + ''; + systemPackages = [ cfg.package ]; + }; + + users.users = singleton { + name = cfg.user; + description = "Exim mail transfer agent user"; + uid = config.ids.uids.exim; + group = cfg.group; + }; + + users.groups = singleton { + name = cfg.group; + gid = config.ids.gids.exim; + }; + + security.wrappers.exim.source = "${cfg.package}/bin/exim"; + + systemd.services.exim = { + description = "Exim Mail Daemon"; + wantedBy = [ "multi-user.target" ]; + restartTriggers = [ config.environment.etc."exim.conf".source ]; + serviceConfig = { + ExecStart = "${cfg.package}/bin/exim -bdf -q30m"; + ExecReload = "${coreutils}/bin/kill -HUP $MAINPID"; + }; + preStart = '' + if ! test -d ${cfg.spoolDir}; then + ${coreutils}/bin/mkdir -p ${cfg.spoolDir} + ${coreutils}/bin/chown ${cfg.user}:${cfg.group} ${cfg.spoolDir} + fi + ''; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/mail/freepops.nix b/nixpkgs/nixos/modules/services/mail/freepops.nix new file mode 100644 index 000000000000..5b729ca50a5e --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/freepops.nix @@ -0,0 +1,89 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.mail.freepopsd; +in + +{ + options = { + services.mail.freepopsd = { + enable = mkOption { + default = false; + type = with types; bool; + description = '' + Enables Freepops, a POP3 webmail wrapper. + ''; + }; + + port = mkOption { + default = 2000; + type = with types; uniq int; + description = '' + Port on which the pop server will listen. + ''; + }; + + threads = mkOption { + default = 5; + type = with types; uniq int; + description = '' + Max simultaneous connections. + ''; + }; + + bind = mkOption { + default = "0.0.0.0"; + type = types.str; + description = '' + Bind over an IPv4 address instead of any. + ''; + }; + + logFile = mkOption { + default = "/var/log/freepopsd"; + example = "syslog"; + type = types.str; + description = '' + Filename of the log file or syslog to rely on the logging daemon. + ''; + }; + + suid = { + user = mkOption { + default = "nobody"; + type = types.str; + description = '' + User name under which freepopsd will be after binding the port. + ''; + }; + + group = mkOption { + default = "nogroup"; + type = types.str; + description = '' + Group under which freepopsd will be after binding the port. + ''; + }; + }; + + }; + }; + + config = mkIf cfg.enable { + systemd.services.freepopsd = { + description = "Freepopsd (webmail over POP3)"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + script = '' + ${pkgs.freepops}/bin/freepopsd \ + -p ${toString cfg.port} \ + -t ${toString cfg.threads} \ + -b ${cfg.bind} \ + -vv -l ${cfg.logFile} \ + -s ${cfg.suid.user}.${cfg.suid.group} + ''; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/mail/mail.nix b/nixpkgs/nixos/modules/services/mail/mail.nix new file mode 100644 index 000000000000..fed313e4738e --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/mail.nix @@ -0,0 +1,33 @@ +{ config, lib, ... }: + +with lib; + +{ + + ###### interface + + options = { + + services.mail = { + + sendmailSetuidWrapper = mkOption { + default = null; + internal = true; + description = '' + Configuration for the sendmail setuid wapper. + ''; + }; + + }; + + }; + + ###### implementation + + config = mkIf (config.services.mail.sendmailSetuidWrapper != null) { + + security.wrappers.sendmail = config.services.mail.sendmailSetuidWrapper; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/mail/mailcatcher.nix b/nixpkgs/nixos/modules/services/mail/mailcatcher.nix new file mode 100644 index 000000000000..fa8d41e918d3 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/mailcatcher.nix @@ -0,0 +1,60 @@ +{ config, pkgs, lib, ... }: + +let + cfg = config.services.mailcatcher; + + inherit (lib) mkEnableOption mkIf mkOption types; +in +{ + # interface + + options = { + + services.mailcatcher = { + enable = mkEnableOption "MailCatcher"; + + http.ip = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The ip address of the http server."; + }; + + http.port = mkOption { + type = types.port; + default = 1080; + description = "The port address of the http server."; + }; + + smtp.ip = mkOption { + type = types.str; + default = "127.0.0.1"; + description = "The ip address of the smtp server."; + }; + + smtp.port = mkOption { + type = types.port; + default = 1025; + description = "The port address of the smtp server."; + }; + }; + + }; + + # implementation + + config = mkIf cfg.enable { + environment.systemPackages = [ pkgs.mailcatcher ]; + + systemd.services.mailcatcher = { + description = "MailCatcher Service"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + DynamicUser = true; + Restart = "always"; + ExecStart = "${pkgs.mailcatcher}/bin/mailcatcher --foreground --no-quit --http-ip ${cfg.http.ip} --http-port ${toString cfg.http.port} --smtp-ip ${cfg.smtp.ip} --smtp-port ${toString cfg.smtp.port}"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/mail/mailhog.nix b/nixpkgs/nixos/modules/services/mail/mailhog.nix new file mode 100644 index 000000000000..b78f4c8e0e66 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/mailhog.nix @@ -0,0 +1,43 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.mailhog; +in { + ###### interface + + options = { + + services.mailhog = { + enable = mkEnableOption "MailHog"; + user = mkOption { + type = types.str; + default = "mailhog"; + description = "User account under which mailhog runs."; + }; + }; + }; + + + ###### implementation + + config = mkIf cfg.enable { + + users.users.mailhog = { + name = cfg.user; + description = "MailHog service user"; + }; + + systemd.services.mailhog = { + description = "MailHog service"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.mailhog}/bin/MailHog"; + User = cfg.user; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/mail/mlmmj.nix b/nixpkgs/nixos/modules/services/mail/mlmmj.nix new file mode 100644 index 000000000000..11565bc02f89 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/mlmmj.nix @@ -0,0 +1,156 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + concatMapLines = f: l: lib.concatStringsSep "\n" (map f l); + + cfg = config.services.mlmmj; + stateDir = "/var/lib/mlmmj"; + spoolDir = "/var/spool/mlmmj"; + listDir = domain: list: "${spoolDir}/${domain}/${list}"; + listCtl = domain: list: "${listDir domain list}/control"; + transport = domain: list: "${domain}--${list}@local.list.mlmmj mlmmj:${domain}/${list}"; + virtual = domain: list: "${list}@${domain} ${domain}--${list}@local.list.mlmmj"; + alias = domain: list: "${list}: \"|${pkgs.mlmmj}/bin/mlmmj-receive -L ${listDir domain list}/\""; + subjectPrefix = list: "[${list}]"; + listAddress = domain: list: "${list}@${domain}"; + customHeaders = domain: list: [ "List-Id: ${list}" "Reply-To: ${list}@${domain}" ]; + footer = domain: list: "To unsubscribe send a mail to ${list}+unsubscribe@${domain}"; + createList = d: l: + let ctlDir = listCtl d l; in + '' + for DIR in incoming queue queue/discarded archive text subconf unsubconf \ + bounce control moderation subscribers.d digesters.d requeue \ + nomailsubs.d + do + mkdir -p '${listDir d l}'/"$DIR" + done + ${pkgs.coreutils}/bin/mkdir -p ${ctlDir} + echo ${listAddress d l} > '${ctlDir}/listaddress' + [ ! -e ${ctlDir}/customheaders ] && \ + echo "${lib.concatStringsSep "\n" (customHeaders d l)}" > '${ctlDir}/customheaders' + [ ! -e ${ctlDir}/footer ] && \ + echo ${footer d l} > '${ctlDir}/footer' + [ ! -e ${ctlDir}/prefix ] && \ + echo ${subjectPrefix l} > '${ctlDir}/prefix' + ''; +in + +{ + + ###### interface + + options = { + + services.mlmmj = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Enable mlmmj"; + }; + + user = mkOption { + type = types.str; + default = "mlmmj"; + description = "mailinglist local user"; + }; + + group = mkOption { + type = types.str; + default = "mlmmj"; + description = "mailinglist local group"; + }; + + listDomain = mkOption { + type = types.str; + default = "localhost"; + description = "Set the mailing list domain"; + }; + + mailLists = mkOption { + type = types.listOf types.str; + default = []; + description = "The collection of hosted maillists"; + }; + + maintInterval = mkOption { + type = types.str; + default = "20min"; + description = '' + Time interval between mlmmj-maintd runs, see + <citerefentry><refentrytitle>systemd.time</refentrytitle> + <manvolnum>7</manvolnum></citerefentry> for format information. + ''; + }; + + }; + + }; + + ###### implementation + + config = mkIf cfg.enable { + + users.users = singleton { + name = cfg.user; + description = "mlmmj user"; + home = stateDir; + createHome = true; + uid = config.ids.uids.mlmmj; + group = cfg.group; + useDefaultShell = true; + }; + + users.groups = singleton { + name = cfg.group; + gid = config.ids.gids.mlmmj; + }; + + services.postfix = { + enable = true; + recipientDelimiter= "+"; + extraMasterConf = '' + mlmmj unix - n n - - pipe flags=ORhu user=mlmmj argv=${pkgs.mlmmj}/bin/mlmmj-receive -F -L ${spoolDir}/$nexthop + ''; + + extraAliases = concatMapLines (alias cfg.listDomain) cfg.mailLists; + + extraConfig = '' + transport_maps = hash:${stateDir}/transports + virtual_alias_maps = hash:${stateDir}/virtuals + propagate_unmatched_extensions = virtual + ''; + }; + + environment.systemPackages = [ pkgs.mlmmj ]; + + system.activationScripts.mlmmj = '' + ${pkgs.coreutils}/bin/mkdir -p ${stateDir} ${spoolDir}/${cfg.listDomain} + ${pkgs.coreutils}/bin/chown -R ${cfg.user}:${cfg.group} ${spoolDir} + ${concatMapLines (createList cfg.listDomain) cfg.mailLists} + echo "${concatMapLines (virtual cfg.listDomain) cfg.mailLists}" > ${stateDir}/virtuals + echo "${concatMapLines (transport cfg.listDomain) cfg.mailLists}" > ${stateDir}/transports + ${pkgs.postfix}/bin/postmap ${stateDir}/virtuals + ${pkgs.postfix}/bin/postmap ${stateDir}/transports + ''; + + systemd.services."mlmmj-maintd" = { + description = "mlmmj maintenance daemon"; + serviceConfig = { + User = cfg.user; + Group = cfg.group; + ExecStart = "${pkgs.mlmmj}/bin/mlmmj-maintd -F -d ${spoolDir}/${cfg.listDomain}"; + }; + }; + + systemd.timers."mlmmj-maintd" = { + description = "mlmmj maintenance timer"; + timerConfig.OnUnitActiveSec = cfg.maintInterval; + wantedBy = [ "timers.target" ]; + }; + }; + +} diff --git a/nixpkgs/nixos/modules/services/mail/nullmailer.nix b/nixpkgs/nixos/modules/services/mail/nullmailer.nix new file mode 100644 index 000000000000..9997d287013e --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/nullmailer.nix @@ -0,0 +1,246 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + + options = { + + services.nullmailer = { + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable nullmailer daemon."; + }; + + user = mkOption { + type = types.string; + default = "nullmailer"; + description = '' + User to use to run nullmailer-send. + ''; + }; + + group = mkOption { + type = types.string; + default = "nullmailer"; + description = '' + Group to use to run nullmailer-send. + ''; + }; + + setSendmail = mkOption { + type = types.bool; + default = true; + description = "Whether to set the system sendmail to nullmailer's."; + }; + + remotesFile = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Path to the <code>remotes</code> control file. This file contains a + list of remote servers to which to send each message. + + See <code>man 8 nullmailer-send</code> for syntax and available + options. + ''; + }; + + config = { + adminaddr = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + If set, all recipients to users at either "localhost" (the literal string) + or the canonical host name (from the me control attribute) are remapped to this address. + This is provided to allow local daemons to be able to send email to + "somebody@localhost" and have it go somewhere sensible instead of being bounced + by your relay host. To send to multiple addresses, + put them all on one line separated by a comma. + ''; + }; + + allmailfrom = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + If set, content will override the envelope sender on all messages. + ''; + }; + + defaultdomain = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The content of this attribute is appended to any host name that + does not contain a period (except localhost), including defaulthost + and idhost. Defaults to the value of the me attribute, if it exists, + otherwise the literal name defauldomain. + ''; + }; + + defaulthost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The content of this attribute is appended to any address that + is missing a host name. Defaults to the value of the me control + attribute, if it exists, otherwise the literal name defaulthost. + ''; + }; + + doublebounceto = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + If the original sender was empty (the original message was a + delivery status or disposition notification), the double bounce + is sent to the address in this attribute. + ''; + }; + + helohost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + Sets the environment variable $HELOHOST which is used by the + SMTP protocol module to set the parameter given to the HELO command. + Defaults to the value of the me configuration attribute. + ''; + }; + + idhost = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The content of this attribute is used when building the message-id + string for the message. Defaults to the canonicalized value of defaulthost. + ''; + }; + + maxpause = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The maximum time to pause between successive queue runs, in seconds. + Defaults to 24 hours (86400). + ''; + }; + + me = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The fully-qualifiled host name of the computer running nullmailer. + Defaults to the literal name me. + ''; + }; + + pausetime = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The minimum time to pause between successive queue runs when there + are messages in the queue, in seconds. Defaults to 1 minute (60). + Each time this timeout is reached, the timeout is doubled to a + maximum of maxpause. After new messages are injected, the timeout + is reset. If this is set to 0, nullmailer-send will exit + immediately after going through the queue once (one-shot mode). + ''; + }; + + remotes = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + A list of remote servers to which to send each message. Each line + contains a remote host name or address followed by an optional + protocol string, separated by white space. + + See <code>man 8 nullmailer-send</code> for syntax and available + options. + + WARNING: This is stored world-readable in the nix store. If you need + to specify any secret credentials here, consider using the + <code>remotesFile</code> option instead. + ''; + }; + + sendtimeout = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The time to wait for a remote module listed above to complete sending + a message before killing it and trying again, in seconds. + Defaults to 1 hour (3600). If this is set to 0, nullmailer-send + will wait forever for messages to complete sending. + ''; + }; + }; + }; + }; + + config = let + cfg = config.services.nullmailer; + in mkIf cfg.enable { + + assertions = [ + { assertion = cfg.config.remotes == null || cfg.remotesFile == null; + message = "Only one of `remotesFile` or `config.remotes` may be used at a time."; + } + ]; + + environment = { + systemPackages = [ pkgs.nullmailer ]; + etc = let + validAttrs = filterAttrs (name: value: value != null) cfg.config; + in + (foldl' (as: name: as // { "nullmailer/${name}".text = validAttrs.${name}; }) {} (attrNames validAttrs)) + // optionalAttrs (cfg.remotesFile != null) { "nullmailer/remotes".source = cfg.remotesFile; }; + }; + + users = { + users = singleton { + name = cfg.user; + description = "Nullmailer relay-only mta user"; + group = cfg.group; + }; + + groups = singleton { + name = cfg.group; + }; + }; + + systemd.tmpfiles.rules = [ + "d /var/spool/nullmailer - ${cfg.user} - - -" + ]; + + systemd.services.nullmailer = { + description = "nullmailer"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + preStart = '' + mkdir -p /var/spool/nullmailer/{queue,tmp} + rm -f /var/spool/nullmailer/trigger && mkfifo -m 660 /var/spool/nullmailer/trigger + ''; + + serviceConfig = { + User = cfg.user; + Group = cfg.group; + ExecStart = "${pkgs.nullmailer}/bin/nullmailer-send"; + Restart = "always"; + }; + }; + + services.mail.sendmailSetuidWrapper = mkIf cfg.setSendmail { + program = "sendmail"; + source = "${pkgs.nullmailer}/bin/sendmail"; + owner = cfg.user; + group = cfg.group; + setuid = true; + setgid = true; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/mail/offlineimap.nix b/nixpkgs/nixos/modules/services/mail/offlineimap.nix new file mode 100644 index 000000000000..294e3806f94a --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/offlineimap.nix @@ -0,0 +1,72 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.offlineimap; +in { + + options.services.offlineimap = { + enable = mkEnableOption "OfflineIMAP, a software to dispose your mailbox(es) as a local Maildir(s)"; + + install = mkOption { + type = types.bool; + default = false; + description = '' + Whether to install a user service for Offlineimap. Once + the service is started, emails will be fetched automatically. + + The service must be manually started for each user with + "systemctl --user start offlineimap" or globally through + <varname>services.offlineimap.enable</varname>. + ''; + }; + + package = mkOption { + type = types.package; + default = pkgs.offlineimap; + defaultText = "pkgs.offlineimap"; + description = "Offlineimap derivation to use."; + }; + + path = mkOption { + type = types.listOf types.path; + default = []; + example = literalExample "[ pkgs.pass pkgs.bash pkgs.notmuch ]"; + description = "List of derivations to put in Offlineimap's path."; + }; + + onCalendar = mkOption { + type = types.str; + default = "*:0/3"; # every 3 minutes + description = "How often is offlineimap started. Default is '*:0/3' meaning every 3 minutes. See systemd.time(7) for more information about the format."; + }; + + timeoutStartSec = mkOption { + type = types.str; + default = "120sec"; # Kill if still alive after 2 minutes + description = "How long waiting for offlineimap before killing it. Default is '120sec' meaning every 2 minutes. See systemd.time(7) for more information about the format."; + }; + }; + config = mkIf (cfg.enable || cfg.install) { + systemd.user.services.offlineimap = { + description = "Offlineimap: a software to dispose your mailbox(es) as a local Maildir(s)"; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${cfg.package}/bin/offlineimap -u syslog -o -1"; + TimeoutStartSec = cfg.timeoutStartSec; + }; + path = cfg.path; + }; + environment.systemPackages = [ cfg.package ]; + systemd.user.timers.offlineimap = { + description = "offlineimap timer"; + timerConfig = { + Unit = "offlineimap.service"; + OnCalendar = cfg.onCalendar; + # start immediately after computer is started: + Persistent = "true"; + }; + } // optionalAttrs cfg.enable { wantedBy = [ "default.target" ]; }; + }; +} diff --git a/nixpkgs/nixos/modules/services/mail/opendkim.nix b/nixpkgs/nixos/modules/services/mail/opendkim.nix new file mode 100644 index 000000000000..253823cbaf9c --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/opendkim.nix @@ -0,0 +1,133 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.opendkim; + + defaultSock = "local:/run/opendkim/opendkim.sock"; + + keyFile = "${cfg.keyPath}/${cfg.selector}.private"; + + args = [ "-f" "-l" + "-p" cfg.socket + "-d" cfg.domains + "-k" keyFile + "-s" cfg.selector + ] ++ optionals (cfg.configFile != null) [ "-x" cfg.configFile ]; + +in { + + ###### interface + + options = { + + services.opendkim = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the OpenDKIM sender authentication system."; + }; + + socket = mkOption { + type = types.str; + default = defaultSock; + description = "Socket which is used for communication with OpenDKIM."; + }; + + user = mkOption { + type = types.str; + default = "opendkim"; + description = "User for the daemon."; + }; + + group = mkOption { + type = types.str; + default = "opendkim"; + description = "Group for the daemon."; + }; + + domains = mkOption { + type = types.str; + default = "csl:${config.networking.hostName}"; + example = "csl:example.com,mydomain.net"; + description = '' + Local domains set (see <literal>opendkim(8)</literal> for more information on datasets). + Messages from them are signed, not verified. + ''; + }; + + keyPath = mkOption { + type = types.path; + description = '' + The path that opendkim should put its generated private keys into. + The DNS settings will be found in this directory with the name selector.txt. + ''; + default = "/var/lib/opendkim/keys"; + }; + + selector = mkOption { + type = types.str; + description = "Selector to use when signing."; + }; + + configFile = mkOption { + type = types.nullOr types.path; + default = null; + description = "Additional opendkim configuration."; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + users.users = optionalAttrs (cfg.user == "opendkim") (singleton + { name = "opendkim"; + group = cfg.group; + uid = config.ids.uids.opendkim; + }); + + users.groups = optionalAttrs (cfg.group == "opendkim") (singleton + { name = "opendkim"; + gid = config.ids.gids.opendkim; + }); + + environment.systemPackages = [ pkgs.opendkim ]; + + systemd.tmpfiles.rules = [ + "d '${cfg.keyPath}' - ${cfg.user} ${cfg.group} - -" + ]; + + systemd.services.opendkim = { + description = "OpenDKIM signing and verification daemon"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + preStart = '' + cd "${cfg.keyPath}" + if ! test -f ${cfg.selector}.private; then + ${pkgs.opendkim}/bin/opendkim-genkey -s ${cfg.selector} -d all-domains-generic-key + echo "Generated OpenDKIM key! Please update your DNS settings:\n" + echo "-------------------------------------------------------------" + cat ${cfg.selector}.txt + echo "-------------------------------------------------------------" + fi + ''; + + serviceConfig = { + ExecStart = "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}"; + User = cfg.user; + Group = cfg.group; + RuntimeDirectory = optional (cfg.socket == defaultSock) "opendkim"; + }; + }; + + }; +} diff --git a/nixpkgs/nixos/modules/services/mail/opensmtpd.nix b/nixpkgs/nixos/modules/services/mail/opensmtpd.nix new file mode 100644 index 000000000000..a870550ba50b --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/opensmtpd.nix @@ -0,0 +1,131 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.opensmtpd; + conf = pkgs.writeText "smtpd.conf" cfg.serverConfiguration; + args = concatStringsSep " " cfg.extraServerArgs; + + sendmail = pkgs.runCommand "opensmtpd-sendmail" { preferLocalBuild = true; } '' + mkdir -p $out/bin + ln -s ${cfg.package}/sbin/smtpctl $out/bin/sendmail + ''; + +in { + + ###### interface + + options = { + + services.opensmtpd = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the OpenSMTPD server."; + }; + + package = mkOption { + type = types.package; + default = pkgs.opensmtpd; + defaultText = "pkgs.opensmtpd"; + description = "The OpenSMTPD package to use."; + }; + + addSendmailToSystemPath = mkOption { + type = types.bool; + default = true; + description = '' + Whether to add OpenSMTPD's sendmail binary to the + system path or not. + ''; + }; + + extraServerArgs = mkOption { + type = types.listOf types.str; + default = []; + example = [ "-v" "-P mta" ]; + description = '' + Extra command line arguments provided when the smtpd process + is started. + ''; + }; + + serverConfiguration = mkOption { + type = types.lines; + example = '' + listen on lo + accept for any deliver to lmtp localhost:24 + ''; + description = '' + The contents of the smtpd.conf configuration file. See the + OpenSMTPD documentation for syntax information. + ''; + }; + + procPackages = mkOption { + type = types.listOf types.package; + default = []; + description = '' + Packages to search for filters, tables, queues, and schedulers. + + Add OpenSMTPD-extras here if you want to use the filters, etc. from + that package. + ''; + }; + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + users.groups = { + smtpd.gid = config.ids.gids.smtpd; + smtpq.gid = config.ids.gids.smtpq; + }; + + users.users = { + smtpd = { + description = "OpenSMTPD process user"; + uid = config.ids.uids.smtpd; + group = "smtpd"; + }; + smtpq = { + description = "OpenSMTPD queue user"; + uid = config.ids.uids.smtpq; + group = "smtpq"; + }; + }; + + systemd.services.opensmtpd = let + procEnv = pkgs.buildEnv { + name = "opensmtpd-procs"; + paths = [ cfg.package ] ++ cfg.procPackages; + pathsToLink = [ "/libexec/opensmtpd" ]; + }; + in { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + preStart = '' + mkdir -p /var/spool/smtpd + chmod 711 /var/spool/smtpd + + mkdir -p /var/spool/smtpd/offline + chown root.smtpq /var/spool/smtpd/offline + chmod 770 /var/spool/smtpd/offline + + mkdir -p /var/spool/smtpd/purge + chown smtpq.root /var/spool/smtpd/purge + chmod 700 /var/spool/smtpd/purge + ''; + serviceConfig.ExecStart = "${cfg.package}/sbin/smtpd -d -f ${conf} ${args}"; + environment.OPENSMTPD_PROC_PATH = "${procEnv}/libexec/opensmtpd"; + }; + + environment.systemPackages = mkIf cfg.addSendmailToSystemPath [ sendmail ]; + }; +} diff --git a/nixpkgs/nixos/modules/services/mail/pfix-srsd.nix b/nixpkgs/nixos/modules/services/mail/pfix-srsd.nix new file mode 100644 index 000000000000..9599854352c9 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/pfix-srsd.nix @@ -0,0 +1,56 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + + ###### interface + + options = { + + services.pfix-srsd = { + enable = mkOption { + default = false; + type = types.bool; + description = "Whether to run the postfix sender rewriting scheme daemon."; + }; + + domain = mkOption { + description = "The domain for which to enable srs"; + type = types.str; + example = "example.com"; + }; + + secretsFile = mkOption { + description = '' + The secret data used to encode the SRS address. + to generate, use a command like: + <literal>for n in $(seq 5); do dd if=/dev/urandom count=1 bs=1024 status=none | sha256sum | sed 's/ -$//' | sed 's/^/ /'; done</literal> + ''; + type = types.path; + default = "/var/lib/pfix-srsd/secrets"; + }; + }; + }; + + ###### implementation + + config = mkIf config.services.pfix-srsd.enable { + environment = { + systemPackages = [ pkgs.pfixtools ]; + }; + + systemd.services."pfix-srsd" = { + description = "Postfix sender rewriting scheme daemon"; + before = [ "postfix.service" ]; + #note that we use requires rather than wants because postfix + #is unable to process (almost) all mail without srsd + requiredBy = [ "postfix.service" ]; + serviceConfig = { + Type = "forking"; + PIDFile = "/run/pfix-srsd.pid"; + ExecStart = "${pkgs.pfixtools}/bin/pfix-srsd -p /run/pfix-srsd.pid -I ${config.services.pfix-srsd.domain} ${config.services.pfix-srsd.secretsFile}"; + }; + }; + }; +} \ No newline at end of file diff --git a/nixpkgs/nixos/modules/services/mail/postfix.nix b/nixpkgs/nixos/modules/services/mail/postfix.nix new file mode 100644 index 000000000000..2b08ab1e6aa6 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/postfix.nix @@ -0,0 +1,898 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.postfix; + user = cfg.user; + group = cfg.group; + setgidGroup = cfg.setgidGroup; + + haveAliases = cfg.postmasterAlias != "" || cfg.rootAlias != "" + || cfg.extraAliases != ""; + haveTransport = cfg.transport != ""; + haveVirtual = cfg.virtual != ""; + haveLocalRecipients = cfg.localRecipients != null; + + clientAccess = + optional (cfg.dnsBlacklistOverrides != "") + "check_client_access hash:/etc/postfix/client_access"; + + dnsBl = + optionals (cfg.dnsBlacklists != []) + (map (s: "reject_rbl_client " + s) cfg.dnsBlacklists); + + clientRestrictions = concatStringsSep ", " (clientAccess ++ dnsBl); + + mainCf = let + escape = replaceStrings ["$"] ["$$"]; + mkList = items: "\n " + concatStringsSep ",\n " items; + mkVal = value: + if isList value then mkList value + else " " + (if value == true then "yes" + else if value == false then "no" + else toString value); + mkEntry = name: value: "${escape name} =${mkVal value}"; + in + concatStringsSep "\n" (mapAttrsToList mkEntry cfg.config) + + "\n" + cfg.extraConfig; + + masterCfOptions = { options, config, name, ... }: { + options = { + name = mkOption { + type = types.str; + default = name; + example = "smtp"; + description = '' + The name of the service to run. Defaults to the attribute set key. + ''; + }; + + type = mkOption { + type = types.enum [ "inet" "unix" "fifo" "pass" ]; + default = "unix"; + example = "inet"; + description = "The type of the service"; + }; + + private = mkOption { + type = types.bool; + example = false; + description = '' + Whether the service's sockets and storage directory is restricted to + be only available via the mail system. If <literal>null</literal> is + given it uses the postfix default <literal>true</literal>. + ''; + }; + + privileged = mkOption { + type = types.bool; + example = true; + description = ""; + }; + + chroot = mkOption { + type = types.bool; + example = true; + description = '' + Whether the service is chrooted to have only access to the + <option>services.postfix.queueDir</option> and the closure of + store paths specified by the <option>program</option> option. + ''; + }; + + wakeup = mkOption { + type = types.int; + example = 60; + description = '' + Automatically wake up the service after the specified number of + seconds. If <literal>0</literal> is given, never wake the service + up. + ''; + }; + + wakeupUnusedComponent = mkOption { + type = types.bool; + example = false; + description = '' + If set to <literal>false</literal> the component will only be woken + up if it is used. This is equivalent to postfix' notion of adding a + question mark behind the wakeup time in + <filename>master.cf</filename> + ''; + }; + + maxproc = mkOption { + type = types.int; + example = 1; + description = '' + The maximum number of processes to spawn for this service. If the + value is <literal>0</literal> it doesn't have any limit. If + <literal>null</literal> is given it uses the postfix default of + <literal>100</literal>. + ''; + }; + + command = mkOption { + type = types.str; + default = name; + example = "smtpd"; + description = '' + A program name specifying a Postfix service/daemon process. + By default it's the attribute <option>name</option>. + ''; + }; + + args = mkOption { + type = types.listOf types.str; + default = []; + example = [ "-o" "smtp_helo_timeout=5" ]; + description = '' + Arguments to pass to the <option>command</option>. There is no shell + processing involved and shell syntax is passed verbatim to the + process. + ''; + }; + + rawEntry = mkOption { + type = types.listOf types.str; + default = []; + internal = true; + description = '' + The raw configuration line for the <filename>master.cf</filename>. + ''; + }; + }; + + config.rawEntry = let + mkBool = bool: if bool then "y" else "n"; + mkArg = arg: "${optionalString (hasPrefix "-" arg) "\n "}${arg}"; + + maybeOption = fun: option: + if options.${option}.isDefined then fun config.${option} else "-"; + + # This is special, because we have two options for this value. + wakeup = let + wakeupDefined = options.wakeup.isDefined; + wakeupUCDefined = options.wakeupUnusedComponent.isDefined; + finalValue = toString config.wakeup + + optionalString (wakeupUCDefined && !config.wakeupUnusedComponent) "?"; + in if wakeupDefined then finalValue else "-"; + + in [ + config.name + config.type + (maybeOption mkBool "private") + (maybeOption (b: mkBool (!b)) "privileged") + (maybeOption mkBool "chroot") + wakeup + (maybeOption toString "maxproc") + (config.command + " " + concatMapStringsSep " " mkArg config.args) + ]; + }; + + masterCfContent = let + + labels = [ + "# service" "type" "private" "unpriv" "chroot" "wakeup" "maxproc" + "command + args" + ]; + + labelDefaults = [ + "# " "" "(yes)" "(yes)" "(no)" "(never)" "(100)" "" "" + ]; + + masterCf = mapAttrsToList (const (getAttr "rawEntry")) cfg.masterConfig; + + # A list of the maximum width of the columns across all lines and labels + maxWidths = let + foldLine = line: acc: let + columnLengths = map stringLength line; + in zipListsWith max acc columnLengths; + # We need to handle the last column specially here, because it's + # open-ended (command + args). + lines = [ labels labelDefaults ] ++ (map (l: init l ++ [""]) masterCf); + in fold foldLine (genList (const 0) (length labels)) lines; + + # Pad a string with spaces from the right (opposite of fixedWidthString). + pad = width: str: let + padWidth = width - stringLength str; + padding = concatStrings (genList (const " ") padWidth); + in str + optionalString (padWidth > 0) padding; + + # It's + 2 here, because that's the amount of spacing between columns. + fullWidth = fold (width: acc: acc + width + 2) 0 maxWidths; + + formatLine = line: concatStringsSep " " (zipListsWith pad maxWidths line); + + formattedLabels = let + sep = "# " + concatStrings (genList (const "=") (fullWidth + 5)); + lines = [ sep (formatLine labels) (formatLine labelDefaults) sep ]; + in concatStringsSep "\n" lines; + + in formattedLabels + "\n" + concatMapStringsSep "\n" formatLine masterCf + "\n" + cfg.extraMasterConf; + + headerCheckOptions = { ... }: + { + options = { + pattern = mkOption { + type = types.str; + default = "/^.*/"; + example = "/^X-Mailer:/"; + description = "A regexp pattern matching the header"; + }; + action = mkOption { + type = types.str; + default = "DUNNO"; + example = "BCC mail@example.com"; + description = "The action to be executed when the pattern is matched"; + }; + }; + }; + + headerChecks = concatStringsSep "\n" (map (x: "${x.pattern} ${x.action}") cfg.headerChecks) + cfg.extraHeaderChecks; + + aliases = let seperator = if cfg.aliasMapType == "hash" then ":" else ""; in + optionalString (cfg.postmasterAlias != "") '' + postmaster${seperator} ${cfg.postmasterAlias} + '' + + optionalString (cfg.rootAlias != "") '' + root${seperator} ${cfg.rootAlias} + '' + + cfg.extraAliases + ; + + aliasesFile = pkgs.writeText "postfix-aliases" aliases; + virtualFile = pkgs.writeText "postfix-virtual" cfg.virtual; + localRecipientMapFile = pkgs.writeText "postfix-local-recipient-map" (concatMapStrings (x: x + " ACCEPT\n") cfg.localRecipients); + checkClientAccessFile = pkgs.writeText "postfix-check-client-access" cfg.dnsBlacklistOverrides; + mainCfFile = pkgs.writeText "postfix-main.cf" mainCf; + masterCfFile = pkgs.writeText "postfix-master.cf" masterCfContent; + transportFile = pkgs.writeText "postfix-transport" cfg.transport; + headerChecksFile = pkgs.writeText "postfix-header-checks" headerChecks; + +in + +{ + + ###### interface + + options = { + + services.postfix = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to run the Postfix mail server."; + }; + + enableSmtp = mkOption { + default = true; + description = "Whether to enable smtp in master.cf."; + }; + + enableSubmission = mkOption { + type = types.bool; + default = false; + description = "Whether to enable smtp submission."; + }; + + submissionOptions = mkOption { + type = types.attrs; + default = { + smtpd_tls_security_level = "encrypt"; + smtpd_sasl_auth_enable = "yes"; + smtpd_client_restrictions = "permit_sasl_authenticated,reject"; + milter_macro_daemon_name = "ORIGINATING"; + }; + example = { + smtpd_tls_security_level = "encrypt"; + smtpd_sasl_auth_enable = "yes"; + smtpd_sasl_type = "dovecot"; + smtpd_client_restrictions = "permit_sasl_authenticated,reject"; + milter_macro_daemon_name = "ORIGINATING"; + }; + description = "Options for the submission config in master.cf"; + }; + + setSendmail = mkOption { + type = types.bool; + default = true; + description = "Whether to set the system sendmail to postfix's."; + }; + + user = mkOption { + type = types.str; + default = "postfix"; + description = "What to call the Postfix user (must be used only for postfix)."; + }; + + group = mkOption { + type = types.str; + default = "postfix"; + description = "What to call the Postfix group (must be used only for postfix)."; + }; + + setgidGroup = mkOption { + type = types.str; + default = "postdrop"; + description = " + How to call postfix setgid group (for postdrop). Should + be uniquely used group. + "; + }; + + networks = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = ["192.168.0.1/24"]; + description = " + Net masks for trusted - allowed to relay mail to third parties - + hosts. Leave empty to use mynetworks_style configuration or use + default (localhost-only). + "; + }; + + networksStyle = mkOption { + type = types.str; + default = ""; + description = " + Name of standard way of trusted network specification to use, + leave blank if you specify it explicitly or if you want to use + default (localhost-only). + "; + }; + + hostname = mkOption { + type = types.str; + default = ""; + description =" + Hostname to use. Leave blank to use just the hostname of machine. + It should be FQDN. + "; + }; + + domain = mkOption { + type = types.str; + default = ""; + description =" + Domain to use. Leave blank to use hostname minus first component. + "; + }; + + origin = mkOption { + type = types.str; + default = ""; + description =" + Origin to use in outgoing e-mail. Leave blank to use hostname. + "; + }; + + destination = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = ["localhost"]; + description = " + Full (!) list of domains we deliver locally. Leave blank for + acceptable Postfix default. + "; + }; + + relayDomains = mkOption { + type = types.nullOr (types.listOf types.str); + default = null; + example = ["localdomain"]; + description = " + List of domains we agree to relay to. Default is empty. + "; + }; + + relayHost = mkOption { + type = types.str; + default = ""; + description = " + Mail relay for outbound mail. + "; + }; + + relayPort = mkOption { + type = types.int; + default = 25; + description = " + SMTP port for relay mail relay. + "; + }; + + lookupMX = mkOption { + type = types.bool; + default = false; + description = " + Whether relay specified is just domain whose MX must be used. + "; + }; + + postmasterAlias = mkOption { + type = types.str; + default = "root"; + description = " + Who should receive postmaster e-mail. Multiple values can be added by + separating values with comma. + "; + }; + + rootAlias = mkOption { + type = types.str; + default = ""; + description = " + Who should receive root e-mail. Blank for no redirection. + Multiple values can be added by separating values with comma. + "; + }; + + extraAliases = mkOption { + type = types.lines; + default = ""; + description = " + Additional entries to put verbatim into aliases file, cf. man-page aliases(8). + "; + }; + + aliasMapType = mkOption { + type = with types; enum [ "hash" "regexp" "pcre" ]; + default = "hash"; + example = "regexp"; + description = "The format the alias map should have. Use regexp if you want to use regular expressions."; + }; + + config = mkOption { + type = with types; attrsOf (oneOf [ bool str (listOf str) ]); + description = '' + The main.cf configuration file as key value set. + ''; + example = { + mail_owner = "postfix"; + smtp_use_tls = true; + }; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = " + Extra lines to be added verbatim to the main.cf configuration file. + "; + }; + + sslCert = mkOption { + type = types.str; + default = ""; + description = "SSL certificate to use."; + }; + + sslCACert = mkOption { + type = types.str; + default = ""; + description = "SSL certificate of CA."; + }; + + sslKey = mkOption { + type = types.str; + default = ""; + description = "SSL key to use."; + }; + + recipientDelimiter = mkOption { + type = types.str; + default = ""; + example = "+"; + description = " + Delimiter for address extension: so mail to user+test can be handled by ~user/.forward+test + "; + }; + + virtual = mkOption { + type = types.lines; + default = ""; + description = " + Entries for the virtual alias map, cf. man-page virtual(8). + "; + }; + + virtualMapType = mkOption { + type = types.enum ["hash" "regexp" "pcre"]; + default = "hash"; + description = '' + What type of virtual alias map file to use. Use <literal>"regexp"</literal> for regular expressions. + ''; + }; + + localRecipients = mkOption { + type = with types; nullOr (listOf string); + default = null; + description = '' + List of accepted local users. Specify a bare username, an + <literal>"@domain.tld"</literal> wild-card, or a complete + <literal>"user@domain.tld"</literal> address. If set, these names end + up in the local recipient map -- see the local(8) man-page -- and + effectively replace the system user database lookup that's otherwise + used by default. + ''; + }; + + transport = mkOption { + default = ""; + description = " + Entries for the transport map, cf. man-page transport(8). + "; + }; + + dnsBlacklists = mkOption { + default = []; + type = with types; listOf string; + description = "dns blacklist servers to use with smtpd_client_restrictions"; + }; + + dnsBlacklistOverrides = mkOption { + default = ""; + description = "contents of check_client_access for overriding dnsBlacklists"; + }; + + masterConfig = mkOption { + type = types.attrsOf (types.submodule masterCfOptions); + default = {}; + example = + { submission = { + type = "inet"; + args = [ "-o" "smtpd_tls_security_level=encrypt" ]; + }; + }; + description = '' + An attribute set of service options, which correspond to the service + definitions usually done within the Postfix + <filename>master.cf</filename> file. + ''; + }; + + extraMasterConf = mkOption { + type = types.lines; + default = ""; + example = "submission inet n - n - - smtpd"; + description = "Extra lines to append to the generated master.cf file."; + }; + + enableHeaderChecks = mkOption { + type = types.bool; + default = false; + example = true; + description = "Whether to enable postfix header checks"; + }; + + headerChecks = mkOption { + type = types.listOf (types.submodule headerCheckOptions); + default = []; + example = [ { pattern = "/^X-Spam-Flag:/"; action = "REDIRECT spam@example.com"; } ]; + description = "Postfix header checks."; + }; + + extraHeaderChecks = mkOption { + type = types.lines; + default = ""; + example = "/^X-Spam-Flag:/ REDIRECT spam@example.com"; + description = "Extra lines to /etc/postfix/header_checks file."; + }; + + aliasFiles = mkOption { + type = types.attrsOf types.path; + default = {}; + description = "Aliases' tables to be compiled and placed into /var/lib/postfix/conf."; + }; + + mapFiles = mkOption { + type = types.attrsOf types.path; + default = {}; + description = "Maps to be compiled and placed into /var/lib/postfix/conf."; + }; + + useSrs = mkOption { + type = types.bool; + default = false; + description = "Whether to enable sender rewriting scheme"; + }; + + }; + + }; + + + ###### implementation + + config = mkIf config.services.postfix.enable (mkMerge [ + { + + environment = { + etc = singleton + { source = "/var/lib/postfix/conf"; + target = "postfix"; + }; + + # This makes it comfortable to run 'postqueue/postdrop' for example. + systemPackages = [ pkgs.postfix ]; + }; + + services.pfix-srsd.enable = config.services.postfix.useSrs; + + services.mail.sendmailSetuidWrapper = mkIf config.services.postfix.setSendmail { + program = "sendmail"; + source = "${pkgs.postfix}/bin/sendmail"; + group = setgidGroup; + setuid = false; + setgid = true; + }; + + security.wrappers.postqueue = { + program = "postqueue"; + source = "${pkgs.postfix}/bin/postqueue"; + group = setgidGroup; + setuid = false; + setgid = true; + }; + + security.wrappers.postdrop = { + program = "postdrop"; + source = "${pkgs.postfix}/bin/postdrop"; + group = setgidGroup; + setuid = false; + setgid = true; + }; + + users.users = optional (user == "postfix") + { name = "postfix"; + description = "Postfix mail server user"; + uid = config.ids.uids.postfix; + group = group; + }; + + users.groups = + optional (group == "postfix") + { name = group; + gid = config.ids.gids.postfix; + } + ++ optional (setgidGroup == "postdrop") + { name = setgidGroup; + gid = config.ids.gids.postdrop; + }; + + systemd.services.postfix = + { description = "Postfix mail server"; + + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + path = [ pkgs.postfix ]; + + serviceConfig = { + Type = "forking"; + Restart = "always"; + PIDFile = "/var/lib/postfix/queue/pid/master.pid"; + ExecStart = "${pkgs.postfix}/bin/postfix start"; + ExecStop = "${pkgs.postfix}/bin/postfix stop"; + ExecReload = "${pkgs.postfix}/bin/postfix reload"; + }; + + preStart = '' + # Backwards compatibility + if [ ! -d /var/lib/postfix ] && [ -d /var/postfix ]; then + mkdir -p /var/lib + mv /var/postfix /var/lib/postfix + fi + + # All permissions set according ${pkgs.postfix}/etc/postfix/postfix-files script + mkdir -p /var/lib/postfix /var/lib/postfix/queue/{pid,public,maildrop} + chmod 0755 /var/lib/postfix + chown root:root /var/lib/postfix + + rm -rf /var/lib/postfix/conf + mkdir -p /var/lib/postfix/conf + chmod 0755 /var/lib/postfix/conf + ln -sf ${pkgs.postfix}/etc/postfix/postfix-files /var/lib/postfix/conf/postfix-files + ln -sf ${mainCfFile} /var/lib/postfix/conf/main.cf + ln -sf ${masterCfFile} /var/lib/postfix/conf/master.cf + + ${concatStringsSep "\n" (mapAttrsToList (to: from: '' + ln -sf ${from} /var/lib/postfix/conf/${to} + ${pkgs.postfix}/bin/postalias /var/lib/postfix/conf/${to} + '') cfg.aliasFiles)} + ${concatStringsSep "\n" (mapAttrsToList (to: from: '' + ln -sf ${from} /var/lib/postfix/conf/${to} + ${pkgs.postfix}/bin/postmap /var/lib/postfix/conf/${to} + '') cfg.mapFiles)} + + mkdir -p /var/spool/mail + chown root:root /var/spool/mail + chmod a+rwxt /var/spool/mail + ln -sf /var/spool/mail /var/ + + #Finally delegate to postfix checking remain directories in /var/lib/postfix and set permissions on them + ${pkgs.postfix}/bin/postfix set-permissions config_directory=/var/lib/postfix/conf + ''; + }; + + services.postfix.config = (mapAttrs (_: v: mkDefault v) { + compatibility_level = "9999"; + mail_owner = cfg.user; + default_privs = "nobody"; + + # NixOS specific locations + data_directory = "/var/lib/postfix/data"; + queue_directory = "/var/lib/postfix/queue"; + + # Default location of everything in package + meta_directory = "${pkgs.postfix}/etc/postfix"; + command_directory = "${pkgs.postfix}/bin"; + sample_directory = "/etc/postfix"; + newaliases_path = "${pkgs.postfix}/bin/newaliases"; + mailq_path = "${pkgs.postfix}/bin/mailq"; + readme_directory = false; + sendmail_path = "${pkgs.postfix}/bin/sendmail"; + daemon_directory = "${pkgs.postfix}/libexec/postfix"; + manpage_directory = "${pkgs.postfix}/share/man"; + html_directory = "${pkgs.postfix}/share/postfix/doc/html"; + shlib_directory = false; + mail_spool_directory = "/var/spool/mail/"; + setgid_group = cfg.setgidGroup; + }) + // optionalAttrs (cfg.relayHost != "") { relayhost = if cfg.lookupMX + then "${cfg.relayHost}:${toString cfg.relayPort}" + else "[${cfg.relayHost}]:${toString cfg.relayPort}"; } + // optionalAttrs config.networking.enableIPv6 { inet_protocols = mkDefault "all"; } + // optionalAttrs (cfg.networks != null) { mynetworks = cfg.networks; } + // optionalAttrs (cfg.networksStyle != "") { mynetworks_style = cfg.networksStyle; } + // optionalAttrs (cfg.hostname != "") { myhostname = cfg.hostname; } + // optionalAttrs (cfg.domain != "") { mydomain = cfg.domain; } + // optionalAttrs (cfg.origin != "") { myorigin = cfg.origin; } + // optionalAttrs (cfg.destination != null) { mydestination = cfg.destination; } + // optionalAttrs (cfg.relayDomains != null) { relay_domains = cfg.relayDomains; } + // optionalAttrs (cfg.recipientDelimiter != "") { recipient_delimiter = cfg.recipientDelimiter; } + // optionalAttrs haveAliases { alias_maps = [ "${cfg.aliasMapType}:/etc/postfix/aliases" ]; } + // optionalAttrs haveTransport { transport_maps = [ "hash:/etc/postfix/transport" ]; } + // optionalAttrs haveVirtual { virtual_alias_maps = [ "${cfg.virtualMapType}:/etc/postfix/virtual" ]; } + // optionalAttrs haveLocalRecipients { local_recipient_maps = [ "hash:/etc/postfix/local_recipients" ] ++ optional haveAliases "$alias_maps"; } + // optionalAttrs (cfg.dnsBlacklists != []) { smtpd_client_restrictions = clientRestrictions; } + // optionalAttrs cfg.useSrs { + sender_canonical_maps = [ "tcp:127.0.0.1:10001" ]; + sender_canonical_classes = [ "envelope_sender" ]; + recipient_canonical_maps = [ "tcp:127.0.0.1:10002" ]; + recipient_canonical_classes = [ "envelope_recipient" ]; + } + // optionalAttrs cfg.enableHeaderChecks { header_checks = [ "regexp:/etc/postfix/header_checks" ]; } + // optionalAttrs (cfg.sslCert != "") { + smtp_tls_CAfile = cfg.sslCACert; + smtp_tls_cert_file = cfg.sslCert; + smtp_tls_key_file = cfg.sslKey; + + smtp_use_tls = true; + + smtpd_tls_CAfile = cfg.sslCACert; + smtpd_tls_cert_file = cfg.sslCert; + smtpd_tls_key_file = cfg.sslKey; + + smtpd_use_tls = true; + }; + + services.postfix.masterConfig = { + smtp_inet = { + name = "smtp"; + type = "inet"; + private = false; + command = "smtpd"; + }; + pickup = { + private = false; + wakeup = 60; + maxproc = 1; + }; + cleanup = { + private = false; + maxproc = 0; + }; + qmgr = { + private = false; + wakeup = 300; + maxproc = 1; + }; + tlsmgr = { + wakeup = 1000; + wakeupUnusedComponent = false; + maxproc = 1; + }; + rewrite = { + command = "trivial-rewrite"; + }; + bounce = { + maxproc = 0; + }; + defer = { + maxproc = 0; + command = "bounce"; + }; + trace = { + maxproc = 0; + command = "bounce"; + }; + verify = { + maxproc = 1; + }; + flush = { + private = false; + wakeup = 1000; + wakeupUnusedComponent = false; + maxproc = 0; + }; + proxymap = { + command = "proxymap"; + }; + proxywrite = { + maxproc = 1; + command = "proxymap"; + }; + showq = { + private = false; + }; + error = {}; + retry = { + command = "error"; + }; + discard = {}; + local = { + privileged = true; + }; + virtual = { + privileged = true; + }; + lmtp = { + }; + anvil = { + maxproc = 1; + }; + scache = { + maxproc = 1; + }; + } // optionalAttrs cfg.enableSubmission { + submission = { + type = "inet"; + private = false; + command = "smtpd"; + args = let + mkKeyVal = opt: val: [ "-o" (opt + "=" + val) ]; + in concatLists (mapAttrsToList mkKeyVal cfg.submissionOptions); + }; + } // optionalAttrs cfg.enableSmtp { + smtp = {}; + relay = { + command = "smtp"; + args = [ "-o" "smtp_fallback_relay=" ]; + }; + }; + } + + (mkIf haveAliases { + services.postfix.aliasFiles."aliases" = aliasesFile; + }) + (mkIf haveTransport { + services.postfix.mapFiles."transport" = transportFile; + }) + (mkIf haveVirtual { + services.postfix.mapFiles."virtual" = virtualFile; + }) + (mkIf haveLocalRecipients { + services.postfix.mapFiles."local_recipients" = localRecipientMapFile; + }) + (mkIf cfg.enableHeaderChecks { + services.postfix.mapFiles."header_checks" = headerChecksFile; + }) + (mkIf (cfg.dnsBlacklists != []) { + services.postfix.mapFiles."client_access" = checkClientAccessFile; + }) + ]); +} diff --git a/nixpkgs/nixos/modules/services/mail/postgrey.nix b/nixpkgs/nixos/modules/services/mail/postgrey.nix new file mode 100644 index 000000000000..8e2b9c5dbc56 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/postgrey.nix @@ -0,0 +1,194 @@ +{ config, lib, pkgs, ... }: + +with lib; let + + cfg = config.services.postgrey; + + natural = with types; addCheck int (x: x >= 0); + natural' = with types; addCheck int (x: x > 0); + + socket = with types; addCheck (either (submodule unixSocket) (submodule inetSocket)) (x: x ? "path" || x ? "port"); + + inetSocket = with types; { + options = { + addr = mkOption { + type = nullOr string; + default = null; + example = "127.0.0.1"; + description = "The address to bind to. Localhost if null"; + }; + port = mkOption { + type = natural'; + default = 10030; + description = "Tcp port to bind to"; + }; + }; + }; + + unixSocket = with types; { + options = { + path = mkOption { + type = path; + default = "/run/postgrey.sock"; + description = "Path of the unix socket"; + }; + + mode = mkOption { + type = string; + default = "0777"; + description = "Mode of the unix socket"; + }; + }; + }; + +in { + + options = { + services.postgrey = with types; { + enable = mkOption { + type = bool; + default = false; + description = "Whether to run the Postgrey daemon"; + }; + socket = mkOption { + type = socket; + default = { + path = "/run/postgrey.sock"; + mode = "0777"; + }; + example = { + addr = "127.0.0.1"; + port = 10030; + }; + description = "Socket to bind to"; + }; + greylistText = mkOption { + type = string; + default = "Greylisted for %%s seconds"; + description = "Response status text for greylisted messages; use %%s for seconds left until greylisting is over and %%r for mail domain of recipient"; + }; + greylistAction = mkOption { + type = string; + default = "DEFER_IF_PERMIT"; + description = "Response status for greylisted messages (see access(5))"; + }; + greylistHeader = mkOption { + type = string; + default = "X-Greylist: delayed %%t seconds by postgrey-%%v at %%h; %%d"; + description = "Prepend header to greylisted mails; use %%t for seconds delayed due to greylisting, %%v for the version of postgrey, %%d for the date, and %%h for the host"; + }; + delay = mkOption { + type = natural; + default = 300; + description = "Greylist for N seconds"; + }; + maxAge = mkOption { + type = natural; + default = 35; + description = "Delete entries from whitelist if they haven't been seen for N days"; + }; + retryWindow = mkOption { + type = either string natural; + default = 2; + example = "12h"; + description = "Allow N days for the first retry. Use string with appended 'h' to specify time in hours"; + }; + lookupBySubnet = mkOption { + type = bool; + default = true; + description = "Strip the last N bits from IP addresses, determined by IPv4CIDR and IPv6CIDR"; + }; + IPv4CIDR = mkOption { + type = natural; + default = 24; + description = "Strip N bits from IPv4 addresses if lookupBySubnet is true"; + }; + IPv6CIDR = mkOption { + type = natural; + default = 64; + description = "Strip N bits from IPv6 addresses if lookupBySubnet is true"; + }; + privacy = mkOption { + type = bool; + default = true; + description = "Store data using one-way hash functions (SHA1)"; + }; + autoWhitelist = mkOption { + type = nullOr natural'; + default = 5; + description = "Whitelist clients after successful delivery of N messages"; + }; + whitelistClients = mkOption { + type = listOf path; + default = []; + description = "Client address whitelist files (see postgrey(8))"; + }; + whitelistRecipients = mkOption { + type = listOf path; + default = []; + description = "Recipient address whitelist files (see postgrey(8))"; + }; + }; + }; + + config = mkIf cfg.enable { + + environment.systemPackages = [ pkgs.postgrey ]; + + users = { + users = { + postgrey = { + description = "Postgrey Daemon"; + uid = config.ids.uids.postgrey; + group = "postgrey"; + }; + }; + groups = { + postgrey = { + gid = config.ids.gids.postgrey; + }; + }; + }; + + systemd.services.postgrey = let + bind-flag = if cfg.socket ? "path" then + ''--unix=${cfg.socket.path} --socketmode=${cfg.socket.mode}'' + else + ''--inet=${optionalString (cfg.socket.addr != null) (cfg.socket.addr + ":")}${toString cfg.socket.port}''; + in { + description = "Postfix Greylisting Service"; + wantedBy = [ "multi-user.target" ]; + before = [ "postfix.service" ]; + preStart = '' + mkdir -p /var/postgrey + chown postgrey:postgrey /var/postgrey + chmod 0770 /var/postgrey + ''; + serviceConfig = { + Type = "simple"; + ExecStart = ''${pkgs.postgrey}/bin/postgrey \ + ${bind-flag} \ + --group=postgrey --user=postgrey \ + --dbdir=/var/postgrey \ + --delay=${toString cfg.delay} \ + --max-age=${toString cfg.maxAge} \ + --retry-window=${toString cfg.retryWindow} \ + ${if cfg.lookupBySubnet then "--lookup-by-subnet" else "--lookup-by-host"} \ + --ipv4cidr=${toString cfg.IPv4CIDR} --ipv6cidr=${toString cfg.IPv6CIDR} \ + ${optionalString cfg.privacy "--privacy"} \ + --auto-whitelist-clients=${toString (if cfg.autoWhitelist == null then 0 else cfg.autoWhitelist)} \ + --greylist-action=${cfg.greylistAction} \ + --greylist-text="${cfg.greylistText}" \ + --x-greylist-header="${cfg.greylistHeader}" \ + ${concatMapStringsSep " " (x: "--whitelist-clients=" + x) cfg.whitelistClients} \ + ${concatMapStringsSep " " (x: "--whitelist-recipients=" + x) cfg.whitelistRecipients} + ''; + Restart = "always"; + RestartSec = 5; + TimeoutSec = 10; + }; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/mail/postsrsd.nix b/nixpkgs/nixos/modules/services/mail/postsrsd.nix new file mode 100644 index 000000000000..8f12a16906c5 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/postsrsd.nix @@ -0,0 +1,135 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.postsrsd; + +in { + + ###### interface + + options = { + + services.postsrsd = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable the postsrsd SRS server for Postfix."; + }; + + secretsFile = mkOption { + type = types.path; + default = "/var/lib/postsrsd/postsrsd.secret"; + description = "Secret keys used for signing and verification"; + }; + + domain = mkOption { + type = types.str; + description = "Domain name for rewrite"; + }; + + separator = mkOption { + type = types.enum ["-" "=" "+"]; + default = "="; + description = "First separator character in generated addresses"; + }; + + # bindAddress = mkOption { # uncomment once 1.5 is released + # type = types.str; + # default = "127.0.0.1"; + # description = "Socket listen address"; + # }; + + forwardPort = mkOption { + type = types.int; + default = 10001; + description = "Port for the forward SRS lookup"; + }; + + reversePort = mkOption { + type = types.int; + default = 10002; + description = "Port for the reverse SRS lookup"; + }; + + timeout = mkOption { + type = types.int; + default = 1800; + description = "Timeout for idle client connections in seconds"; + }; + + excludeDomains = mkOption { + type = types.listOf types.str; + default = []; + description = "Origin domains to exclude from rewriting in addition to primary domain"; + }; + + user = mkOption { + type = types.str; + default = "postsrsd"; + description = "User for the daemon"; + }; + + group = mkOption { + type = types.str; + default = "postsrsd"; + description = "Group for the daemon"; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + services.postsrsd.domain = mkDefault config.networking.hostName; + + users.users = optionalAttrs (cfg.user == "postsrsd") (singleton + { name = "postsrsd"; + group = cfg.group; + uid = config.ids.uids.postsrsd; + }); + + users.groups = optionalAttrs (cfg.group == "postsrsd") (singleton + { name = "postsrsd"; + gid = config.ids.gids.postsrsd; + }); + + systemd.services.postsrsd = { + description = "PostSRSd SRS rewriting server"; + after = [ "network.target" ]; + before = [ "postfix.service" ]; + wantedBy = [ "multi-user.target" ]; + + path = [ pkgs.coreutils ]; + + serviceConfig = { + ExecStart = ''${pkgs.postsrsd}/sbin/postsrsd "-s${cfg.secretsFile}" "-d${cfg.domain}" -a${cfg.separator} -f${toString cfg.forwardPort} -r${toString cfg.reversePort} -t${toString cfg.timeout} "-X${concatStringsSep "," cfg.excludeDomains}"''; + User = cfg.user; + Group = cfg.group; + PermissionsStartOnly = true; + }; + + preStart = '' + if [ ! -e "${cfg.secretsFile}" ]; then + echo "WARNING: secrets file not found, autogenerating!" + DIR="$(dirname "${cfg.secretsFile}")" + if [ ! -d "$DIR" ]; then + mkdir -p -m750 "$DIR" + chown "${cfg.user}:${cfg.group}" "$DIR" + fi + dd if=/dev/random bs=18 count=1 | base64 > "${cfg.secretsFile}" + chmod 600 "${cfg.secretsFile}" + fi + chown "${cfg.user}:${cfg.group}" "${cfg.secretsFile}" + ''; + }; + + }; +} diff --git a/nixpkgs/nixos/modules/services/mail/roundcube.nix b/nixpkgs/nixos/modules/services/mail/roundcube.nix new file mode 100644 index 000000000000..bdedfa1bb701 --- /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.<name></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 000000000000..e59d5715de05 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/rspamd.nix @@ -0,0 +1,418 @@ +{ config, options, pkgs, lib, ... }: + +with lib; + +let + + cfg = config.services.rspamd; + postfixCfg = config.services.postfix; + + bindSocketOpts = {options, config, ... }: { + options = { + socket = mkOption { + type = types.str; + example = "localhost:11333"; + description = '' + Socket for this worker to listen on in a format acceptable by rspamd. + ''; + }; + mode = mkOption { + type = types.str; + default = "0644"; + description = "Mode to set on unix socket"; + }; + owner = mkOption { + type = types.str; + default = "${cfg.user}"; + description = "Owner to set on unix socket"; + }; + group = mkOption { + type = types.str; + default = "${cfg.group}"; + description = "Group to set on unix socket"; + }; + rawEntry = mkOption { + type = types.str; + internal = true; + }; + }; + config.rawEntry = let + maybeOption = option: + optionalString options.${option}.isDefined " ${option}=${config.${option}}"; + in + if (!(hasPrefix "/" config.socket)) then "${config.socket}" + else "${config.socket}${maybeOption "mode"}${maybeOption "owner"}${maybeOption "group"}"; + }; + + traceWarning = w: x: builtins.trace "[1;31mwarning: ${w}[0m" x; + + workerOpts = { name, options, ... }: { + options = { + enable = mkOption { + type = types.nullOr types.bool; + default = null; + description = "Whether to run the rspamd worker."; + }; + name = mkOption { + type = types.nullOr types.str; + default = name; + description = "Name of the worker"; + }; + type = mkOption { + type = types.nullOr (types.enum [ + "normal" "controller" "fuzzy_storage" "rspamd_proxy" "lua" "proxy" + ]); + description = '' + The type of this worker. The type <literal>proxy</literal> is + deprecated and only kept for backwards compatibility and should be + replaced with <literal>rspamd_proxy</literal>. + ''; + apply = let + from = "services.rspamd.workers.\”${name}\".type"; + files = options.type.files; + warning = "The option `${from}` defined in ${showFiles files} has enum value `proxy` which has been renamed to `rspamd_proxy`"; + in x: if x == "proxy" then traceWarning warning "rspamd_proxy" else x; + }; + bindSockets = mkOption { + type = types.listOf (types.either types.str (types.submodule bindSocketOpts)); + default = []; + description = '' + List of sockets to listen, in format acceptable by rspamd + ''; + example = [{ + socket = "/run/rspamd.sock"; + mode = "0666"; + owner = "rspamd"; + } "*:11333"]; + apply = value: map (each: if (isString each) + then if (isUnixSocket each) + then {socket = each; owner = cfg.user; group = cfg.group; mode = "0644"; rawEntry = "${each}";} + else {socket = each; rawEntry = "${each}";} + else each) value; + }; + count = mkOption { + type = types.nullOr types.int; + default = null; + description = '' + Number of worker instances to run + ''; + }; + includes = mkOption { + type = types.listOf types.str; + default = []; + description = '' + List of files to include in configuration + ''; + }; + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Additional entries to put verbatim into worker section of rspamd config file."; + }; + }; + config = mkIf (name == "normal" || name == "controller" || name == "fuzzy" || name == "rspamd_proxy") { + type = mkDefault name; + includes = mkDefault [ "$CONFDIR/worker-${if name == "rspamd_proxy" then "proxy" else name}.inc" ]; + bindSockets = + let + unixSocket = name: { + mode = "0660"; + socket = "/run/rspamd/${name}.sock"; + owner = cfg.user; + group = cfg.group; + }; + in mkDefault (if name == "normal" then [(unixSocket "rspamd")] + else if name == "controller" then [ "localhost:11334" ] + else if name == "rspamd_proxy" then [ (unixSocket "proxy") ] + else [] ); + }; + }; + + isUnixSocket = socket: hasPrefix "/" (if (isString socket) then socket else socket.socket); + + mkBindSockets = enabled: socks: concatStringsSep "\n " + (flatten (map (each: "bind_socket = \"${each.rawEntry}\";") socks)); + + rspamdConfFile = pkgs.writeText "rspamd.conf" + '' + .include "$CONFDIR/common.conf" + + options { + pidfile = "$RUNDIR/rspamd.pid"; + .include "$CONFDIR/options.inc" + .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/options.inc" + .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/options.inc" + } + + logging { + type = "syslog"; + .include "$CONFDIR/logging.inc" + .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/logging.inc" + .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/logging.inc" + } + + ${concatStringsSep "\n" (mapAttrsToList (name: value: let + includeName = if name == "rspamd_proxy" then "proxy" else name; + tryOverride = if value.extraConfig == "" then "true" else "false"; + in '' + worker "${value.type}" { + type = "${value.type}"; + ${optionalString (value.enable != null) + "enabled = ${if value.enable != false then "yes" else "no"};"} + ${mkBindSockets value.enable value.bindSockets} + ${optionalString (value.count != null) "count = ${toString value.count};"} + ${concatStringsSep "\n " (map (each: ".include \"${each}\"") value.includes)} + .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/worker-${includeName}.inc" + .include(try=${tryOverride}; priority=10) "$LOCAL_CONFDIR/override.d/worker-${includeName}.inc" + } + '') cfg.workers)} + + ${optionalString (cfg.extraConfig != "") '' + .include(priority=10) "$LOCAL_CONFDIR/override.d/extra-config.inc" + ''} + ''; + + filterFiles = files: filterAttrs (n: v: v.enable) files; + rspamdDir = pkgs.linkFarm "etc-rspamd-dir" ( + (mapAttrsToList (name: file: { name = "local.d/${name}"; path = file.source; }) (filterFiles cfg.locals)) ++ + (mapAttrsToList (name: file: { name = "override.d/${name}"; path = file.source; }) (filterFiles cfg.overrides)) ++ + (optional (cfg.localLuaRules != null) { name = "rspamd.local.lua"; path = cfg.localLuaRules; }) ++ + [ { name = "rspamd.conf"; path = rspamdConfFile; } ] + ); + + configFileModule = prefix: { name, config, ... }: { + options = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether this file ${prefix} should be generated. This + option allows specific ${prefix} files to be disabled. + ''; + }; + + text = mkOption { + default = null; + type = types.nullOr types.lines; + description = "Text of the file."; + }; + + source = mkOption { + type = types.path; + description = "Path of the source file."; + }; + }; + config = { + source = mkIf (config.text != null) ( + let name' = "rspamd-${prefix}-" + baseNameOf name; + in mkDefault (pkgs.writeText name' config.text)); + }; + }; + + configOverrides = + (mapAttrs' (n: v: nameValuePair "worker-${if n == "rspamd_proxy" then "proxy" else n}.inc" { + text = v.extraConfig; + }) + (filterAttrs (n: v: v.extraConfig != "") cfg.workers)) + // (if cfg.extraConfig == "" then {} else { + "extra-config.inc".text = cfg.extraConfig; + }); +in + +{ + + ###### interface + + options = { + + services.rspamd = { + + enable = mkEnableOption "rspamd, the Rapid spam filtering system"; + + debug = mkOption { + type = types.bool; + default = false; + description = "Whether to run the rspamd daemon in debug mode."; + }; + + locals = mkOption { + type = with types; attrsOf (submodule (configFileModule "locals")); + default = {}; + description = '' + Local configuration files, written into <filename>/etc/rspamd/local.d/{name}</filename>. + ''; + example = literalExample '' + { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf"; + "arc.conf".text = "allow_envfrom_empty = true;"; + } + ''; + }; + + overrides = mkOption { + type = with types; attrsOf (submodule (configFileModule "overrides")); + default = {}; + description = '' + Overridden configuration files, written into <filename>/etc/rspamd/override.d/{name}</filename>. + ''; + example = literalExample '' + { "redis.conf".source = "/nix/store/.../etc/dir/redis.conf"; + "arc.conf".text = "allow_envfrom_empty = true;"; + } + ''; + }; + + localLuaRules = mkOption { + default = null; + type = types.nullOr types.path; + description = '' + Path of file to link to <filename>/etc/rspamd/rspamd.local.lua</filename> for local + rules written in Lua + ''; + }; + + workers = mkOption { + type = with types; attrsOf (submodule workerOpts); + description = '' + Attribute set of workers to start. + ''; + default = { + normal = {}; + controller = {}; + }; + example = literalExample '' + { + normal = { + includes = [ "$CONFDIR/worker-normal.inc" ]; + bindSockets = [{ + socket = "/run/rspamd/rspamd.sock"; + mode = "0660"; + owner = "${cfg.user}"; + group = "${cfg.group}"; + }]; + }; + controller = { + includes = [ "$CONFDIR/worker-controller.inc" ]; + bindSockets = [ "[::1]:11334" ]; + }; + } + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = '' + Extra configuration to add at the end of the rspamd configuration + file. + ''; + }; + + user = mkOption { + type = types.string; + default = "rspamd"; + description = '' + User to use when no root privileges are required. + ''; + }; + + group = mkOption { + type = types.string; + default = "rspamd"; + description = '' + Group to use when no root privileges are required. + ''; + }; + + postfix = { + enable = mkOption { + type = types.bool; + default = false; + description = "Add rspamd milter to postfix main.conf"; + }; + + config = mkOption { + type = with types; attrsOf (oneOf [ bool str (listOf str) ]); + description = '' + Addon to postfix configuration + ''; + default = { + smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"]; + non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"]; + }; + example = { + smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"]; + non_smtpd_milters = ["unix:/run/rspamd/rspamd-milter.sock"]; + }; + }; + }; + }; + }; + + + ###### implementation + + config = mkIf cfg.enable { + services.rspamd.overrides = configOverrides; + services.rspamd.workers = mkIf cfg.postfix.enable { + controller = {}; + rspamd_proxy = { + bindSockets = [ { + mode = "0660"; + socket = "/run/rspamd/rspamd-milter.sock"; + owner = cfg.user; + group = postfixCfg.group; + } ]; + extraConfig = '' + upstream "local" { + default = yes; # Self-scan upstreams are always default + self_scan = yes; # Enable self-scan + } + ''; + }; + }; + services.postfix.config = mkIf cfg.postfix.enable cfg.postfix.config; + + # Allow users to run 'rspamc' and 'rspamadm'. + environment.systemPackages = [ pkgs.rspamd ]; + + users.users = singleton { + name = cfg.user; + description = "rspamd daemon"; + uid = config.ids.uids.rspamd; + group = cfg.group; + }; + + users.groups = singleton { + name = cfg.group; + gid = config.ids.gids.rspamd; + }; + + environment.etc."rspamd".source = rspamdDir; + + systemd.services.rspamd = { + description = "Rspamd Service"; + + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + restartTriggers = [ rspamdDir ]; + + serviceConfig = { + ExecStart = "${pkgs.rspamd}/bin/rspamd ${optionalString cfg.debug "-d"} --user=${cfg.user} --group=${cfg.group} --pid=/run/rspamd.pid -c /etc/rspamd/rspamd.conf -f"; + Restart = "always"; + RuntimeDirectory = "rspamd"; + PrivateTmp = true; + }; + + preStart = '' + ${pkgs.coreutils}/bin/mkdir -p /var/lib/rspamd + ${pkgs.coreutils}/bin/chown ${cfg.user}:${cfg.group} /var/lib/rspamd + ''; + }; + }; + imports = [ + (mkRemovedOptionModule [ "services" "rspamd" "socketActivation" ] + "Socket activation never worked correctly and could at this time not be fixed and so was removed") + (mkRenamedOptionModule [ "services" "rspamd" "bindSocket" ] [ "services" "rspamd" "workers" "normal" "bindSockets" ]) + (mkRenamedOptionModule [ "services" "rspamd" "bindUISocket" ] [ "services" "rspamd" "workers" "controller" "bindSockets" ]) + ]; +} diff --git a/nixpkgs/nixos/modules/services/mail/rss2email.nix b/nixpkgs/nixos/modules/services/mail/rss2email.nix new file mode 100644 index 000000000000..df454abc8267 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/rss2email.nix @@ -0,0 +1,134 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.rss2email; +in { + + ###### interface + + options = { + + services.rss2email = { + + enable = mkOption { + type = types.bool; + default = false; + description = "Whether to enable rss2email."; + }; + + to = mkOption { + type = types.str; + description = "Mail address to which to send emails"; + }; + + interval = mkOption { + type = types.str; + default = "12h"; + description = "How often to check the feeds, in systemd interval format"; + }; + + config = mkOption { + type = with types; attrsOf (oneOf [ str int bool ]); + default = {}; + description = '' + The configuration to give rss2email. + + Default will use system-wide <literal>sendmail</literal> to send the + email. This is rss2email's default when running + <literal>r2e new</literal>. + + This set contains key-value associations that will be set in the + <literal>[DEFAULT]</literal> block along with the + <literal>to</literal> parameter. + + See + <literal>https://github.com/rss2email/rss2email/blob/master/r2e.1</literal> + for more information on which parameters are accepted. + ''; + }; + + feeds = mkOption { + description = "The feeds to watch."; + type = types.attrsOf (types.submodule { + options = { + url = mkOption { + type = types.str; + description = "The URL at which to fetch the feed."; + }; + + to = mkOption { + type = with types; nullOr str; + default = null; + description = '' + Email address to which to send feed items. + + If <literal>null</literal>, this will not be set in the + configuration file, and rss2email will make it default to + <literal>rss2email.to</literal>. + ''; + }; + }; + }); + }; + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + users.groups = { + rss2email.gid = config.ids.gids.rss2email; + }; + + users.users = { + rss2email = { + description = "rss2email user"; + uid = config.ids.uids.rss2email; + group = "rss2email"; + }; + }; + + services.rss2email.config.to = cfg.to; + + systemd.tmpfiles.rules = [ + "d /var/rss2email 0700 rss2email rss2email - -" + ]; + + systemd.services.rss2email = let + conf = pkgs.writeText "rss2email.cfg" (lib.generators.toINI {} ({ + DEFAULT = cfg.config; + } // lib.mapAttrs' (name: feed: nameValuePair "feed.${name}" ( + { inherit (feed) url; } // + lib.optionalAttrs (feed.to != null) { inherit (feed) to; } + )) cfg.feeds + )); + in + { + preStart = '' + cp ${conf} /var/rss2email/conf.cfg + if [ ! -f /var/rss2email/db.json ]; then + echo '{"version":2,"feeds":[]}' > /var/rss2email/db.json + fi + ''; + path = [ pkgs.system-sendmail ]; + serviceConfig = { + ExecStart = + "${pkgs.rss2email}/bin/r2e -c /var/rss2email/conf.cfg -d /var/rss2email/db.json run"; + User = "rss2email"; + }; + }; + + systemd.timers.rss2email = { + partOf = [ "rss2email.service" ]; + wantedBy = [ "timers.target" ]; + timerConfig.OnBootSec = "0"; + timerConfig.OnUnitActiveSec = cfg.interval; + }; + }; + + meta.maintainers = with lib.maintainers; [ ekleog ]; +} diff --git a/nixpkgs/nixos/modules/services/mail/spamassassin.nix b/nixpkgs/nixos/modules/services/mail/spamassassin.nix new file mode 100644 index 000000000000..1fe77ce5a0c7 --- /dev/null +++ b/nixpkgs/nixos/modules/services/mail/spamassassin.nix @@ -0,0 +1,199 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.spamassassin; + spamassassin-local-cf = pkgs.writeText "local.cf" cfg.config; + spamassassin-init-pre = pkgs.writeText "init.pre" cfg.initPreConf; + + spamdEnv = pkgs.buildEnv { + name = "spamd-env"; + paths = []; + postBuild = '' + ln -sf ${spamassassin-init-pre} $out/init.pre + ln -sf ${spamassassin-local-cf} $out/local.cf + ''; + }; + +in + +{ + options = { + + services.spamassassin = { + enable = mkOption { + default = false; + description = "Whether to run the SpamAssassin daemon"; + }; + + debug = mkOption { + default = false; + description = "Whether to run the SpamAssassin daemon in debug mode"; + }; + + config = mkOption { + type = types.lines; + description = '' + The SpamAssassin local.cf config + + If you are using this configuration: + add_header all Status _YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_ + + Then you can Use this sieve filter: + require ["fileinto", "reject", "envelope"]; + + if header :contains "X-Spam-Flag" "YES" { + fileinto "spam"; + } + + Or this procmail filter: + :0: + * ^X-Spam-Flag: YES + /var/vpopmail/domains/lastlog.de/js/.maildir/.spam/new + + To filter your messages based on the additional mail headers added by spamassassin. + ''; + example = '' + #rewrite_header Subject [***** SPAM _SCORE_ *****] + required_score 5.0 + use_bayes 1 + bayes_auto_learn 1 + add_header all Status _YESNO_, score=_SCORE_ required=_REQD_ tests=_TESTS_ autolearn=_AUTOLEARN_ version=_VERSION_ + ''; + default = ""; + }; + + initPreConf = mkOption { + type = types.str; + description = "The SpamAssassin init.pre config."; + default = + '' + # + # to update this list, run this command in the rules directory: + # grep 'loadplugin.*Mail::SpamAssassin::Plugin::.*' -o -h * | sort | uniq + # + + #loadplugin Mail::SpamAssassin::Plugin::AccessDB + #loadplugin Mail::SpamAssassin::Plugin::AntiVirus + loadplugin Mail::SpamAssassin::Plugin::AskDNS + # loadplugin Mail::SpamAssassin::Plugin::ASN + loadplugin Mail::SpamAssassin::Plugin::AutoLearnThreshold + #loadplugin Mail::SpamAssassin::Plugin::AWL + loadplugin Mail::SpamAssassin::Plugin::Bayes + loadplugin Mail::SpamAssassin::Plugin::BodyEval + loadplugin Mail::SpamAssassin::Plugin::Check + #loadplugin Mail::SpamAssassin::Plugin::DCC + loadplugin Mail::SpamAssassin::Plugin::DKIM + loadplugin Mail::SpamAssassin::Plugin::DNSEval + loadplugin Mail::SpamAssassin::Plugin::FreeMail + loadplugin Mail::SpamAssassin::Plugin::Hashcash + loadplugin Mail::SpamAssassin::Plugin::HeaderEval + loadplugin Mail::SpamAssassin::Plugin::HTMLEval + loadplugin Mail::SpamAssassin::Plugin::HTTPSMismatch + loadplugin Mail::SpamAssassin::Plugin::ImageInfo + loadplugin Mail::SpamAssassin::Plugin::MIMEEval + loadplugin Mail::SpamAssassin::Plugin::MIMEHeader + # loadplugin Mail::SpamAssassin::Plugin::PDFInfo + #loadplugin Mail::SpamAssassin::Plugin::PhishTag + loadplugin Mail::SpamAssassin::Plugin::Pyzor + loadplugin Mail::SpamAssassin::Plugin::Razor2 + # loadplugin Mail::SpamAssassin::Plugin::RelayCountry + loadplugin Mail::SpamAssassin::Plugin::RelayEval + loadplugin Mail::SpamAssassin::Plugin::ReplaceTags + # loadplugin Mail::SpamAssassin::Plugin::Rule2XSBody + # loadplugin Mail::SpamAssassin::Plugin::Shortcircuit + loadplugin Mail::SpamAssassin::Plugin::SpamCop + loadplugin Mail::SpamAssassin::Plugin::SPF + #loadplugin Mail::SpamAssassin::Plugin::TextCat + # loadplugin Mail::SpamAssassin::Plugin::TxRep + loadplugin Mail::SpamAssassin::Plugin::URIDetail + loadplugin Mail::SpamAssassin::Plugin::URIDNSBL + loadplugin Mail::SpamAssassin::Plugin::URIEval + # loadplugin Mail::SpamAssassin::Plugin::URILocalBL + loadplugin Mail::SpamAssassin::Plugin::VBounce + loadplugin Mail::SpamAssassin::Plugin::WhiteListSubject + loadplugin Mail::SpamAssassin::Plugin::WLBLEval + ''; + }; + }; + }; + + config = mkIf cfg.enable { + + # Allow users to run 'spamc'. + + environment = { + etc = singleton { source = spamdEnv; target = "spamassassin"; }; + systemPackages = [ pkgs.spamassassin ]; + }; + + users.users = singleton { + name = "spamd"; + description = "Spam Assassin Daemon"; + uid = config.ids.uids.spamd; + group = "spamd"; + }; + + users.groups = singleton { + name = "spamd"; + gid = config.ids.gids.spamd; + }; + + systemd.services.sa-update = { + script = '' + set +e + ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/ --siteconfigpath=${spamdEnv}/" spamd + + v=$? + set -e + if [ $v -gt 1 ]; then + echo "sa-update execution error" + exit $v + fi + if [ $v -eq 0 ]; then + systemctl reload spamd.service + fi + ''; + }; + + systemd.timers.sa-update = { + description = "sa-update-service"; + partOf = [ "sa-update.service" ]; + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "1:*"; + Persistent = true; + }; + }; + + systemd.services.spamd = { + description = "Spam Assassin Server"; + + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + ExecStart = "${pkgs.spamassassin}/bin/spamd ${optionalString cfg.debug "-D"} --username=spamd --groupname=spamd --siteconfigpath=${spamdEnv} --virtual-config-dir=/var/lib/spamassassin/user-%u --allow-tell --pidfile=/run/spamd.pid"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + + # 0 and 1 no error, exitcode > 1 means error: + # https://spamassassin.apache.org/full/3.1.x/doc/sa-update.html#exit_codes + preStart = '' + echo "Recreating '/var/lib/spamasassin' with creating '3.004001' (or similar) and 'sa-update-keys'" + mkdir -p /var/lib/spamassassin + chown spamd:spamd /var/lib/spamassassin -R + set +e + ${pkgs.su}/bin/su -s "${pkgs.bash}/bin/bash" -c "${pkgs.spamassassin}/bin/sa-update --gpghomedir=/var/lib/spamassassin/sa-update-keys/ --siteconfigpath=${spamdEnv}/" spamd + v=$? + set -e + if [ $v -gt 1 ]; then + echo "sa-update execution error" + exit $v + fi + chown spamd:spamd /var/lib/spamassassin -R + ''; + }; + }; +} |