From 0fbfbafb6e05197cc5bc5be57e6ebb5ad89be77a Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Fri, 4 May 2018 14:56:33 +0200 Subject: [PATCH 01/11] Make dovecot sockets use postfix user/group options --- mail-server/dovecot.nix | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 4294a2d..655b92a 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -28,6 +28,8 @@ let # maildir in format "/${domain}/${user}" dovecotMaildir = "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"; + + postfixCfg = config.services.postfix; in { config = with cfg; lib.mkIf enable { @@ -83,9 +85,9 @@ in service lmtp { unix_listener /var/lib/postfix/queue/private/dovecot-lmtp { - group = postfix + group = ${postfixCfg.group} mode = 0600 - user = postfix # TODO: < make variable + user = ${postfixCfg.user} } } @@ -106,8 +108,8 @@ in service auth { unix_listener /var/lib/postfix/queue/private/auth { mode = 0660 - user = postfix # TODO: < make variable - group = postfix # TODO: < make variable + user = ${postfixCfg.user} + group = ${postfixCfg.group} } } From 8a27b941bf1acec57b872ecd69e5792badab9c02 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Fri, 4 May 2018 16:52:58 +0200 Subject: [PATCH 02/11] Start dovecot before postfix and add target for certificates It seemed weird to me that preStart on postfix was used to generate files not needed directly by postfix and for the self-signed certificate which is also needed by dovecot. nginx.service was also used as a proxy for when ACME certificate generation was done. So I have created mailserver-certificates.target for when certificates are available for other services. For self-signed that means that a new oneshot service called mailserver-selfsigned-certificate has been run. And for ACME this means that the target acme-selfsigned-certificates has been reached (which is when acme has created the self-signed certificates used before the actual certificates provided by LetsEncrypt are created). This setup has the added bonus that if you want to run a service to provide your own certificates you can set that to run before mailserver-certificates.target. DH Parameters are only needed by dovecot so generation of that file has been moved to the dovecot2 preStart. And lastly the only remaining reason to for dovecot to start before postfix was that the auth and lmtp sockets where located in a directory created by postfix. But since they could just as well be located in /run/dovecot2 as long as postfix has access to them I have moved them there. --- mail-server/dovecot.nix | 4 +- mail-server/postfix.nix | 6 +- mail-server/systemd.nix | 124 +++++++++++++++++++++++----------------- 3 files changed, 78 insertions(+), 56 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 655b92a..bb85c21 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -84,7 +84,7 @@ in ''} service lmtp { - unix_listener /var/lib/postfix/queue/private/dovecot-lmtp { + unix_listener dovecot-lmtp { group = ${postfixCfg.group} mode = 0600 user = ${postfixCfg.user} @@ -106,7 +106,7 @@ in } service auth { - unix_listener /var/lib/postfix/queue/private/auth { + unix_listener auth { mode = 0660 user = ${postfixCfg.user} group = ${postfixCfg.group} diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index b145122..c35c10e 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -121,11 +121,11 @@ in virtual_mailbox_domains = ${vhosts_file} virtual_mailbox_maps = hash:/var/lib/postfix/conf/valias virtual_alias_maps = hash:/var/lib/postfix/conf/valias - virtual_transport = lmtp:unix:private/dovecot-lmtp + virtual_transport = lmtp:unix:/run/dovecot2/dovecot-lmtp # sasl with dovecot smtpd_sasl_type = dovecot - smtpd_sasl_path = private/auth + smtpd_sasl_path = /run/dovecot2/auth smtpd_sasl_auth_enable = yes smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination @@ -158,7 +158,7 @@ in smtpd_tls_security_level = "encrypt"; smtpd_sasl_auth_enable = "yes"; smtpd_sasl_type = "dovecot"; - smtpd_sasl_path = "private/auth"; + smtpd_sasl_path = "/run/dovecot2/auth"; smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_local_domain = "$myhostname"; smtpd_client_restrictions = "permit_sasl_authenticated,reject"; diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 1a3f8b9..e083d85 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -19,37 +19,6 @@ let cfg = config.mailserver; - create_certificate = if cfg.certificateScheme == 2 then - '' - # Create certificates if they do not exist yet - dir="${cfg.certificateDirectory}" - fqdn="${cfg.fqdn}" - case $fqdn in /*) fqdn=$(cat "$fqdn");; esac - key="''${dir}/key-${cfg.fqdn}.pem"; - cert="''${dir}/cert-${cfg.fqdn}.pem"; - - if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ] - then - mkdir -p "${cfg.certificateDirectory}" - (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "''${key}" 2048) && - "${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \ - -days 3650 -out "''${cert}" - fi - '' - else ""; - - createDhParameterFile = - '' - # Create a dh parameter file - if [ ! -s "${cfg.certificateDirectory}/dh.pem" ] - then - mkdir -p "${cfg.certificateDirectory}" - ${pkgs.openssl}/bin/openssl \ - dhparam ${builtins.toString cfg.dhParamBitLength} \ - > "${cfg.certificateDirectory}/dh.pem" - fi - ''; - createDomainDkimCert = dom: let dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key"; @@ -76,32 +45,85 @@ let chown -R rmilter:rmilter "${cfg.dkimKeyDirectory}" ''; + + createDhParameterFile = let + dovecotVersion = builtins.fromJSON + (builtins.readFile (pkgs.callPackage ./dovecot-version.nix {})); + in lib.optionalString + (dovecotVersion.major == 2 && dovecotVersion.minor >= 3) + '' + # Create a dh parameter file + if [ ! -s "${cfg.certificateDirectory}/dh.pem" ] + then + mkdir -p "${cfg.certificateDirectory}" + ${pkgs.openssl}/bin/openssl \ + dhparam ${builtins.toString cfg.dhParamBitLength} \ + > "${cfg.certificateDirectory}/dh.pem" + fi + ''; + + preliminarySelfsigned = config.security.acme.preliminarySelfsigned; + acmeWantsTarget = [ "acme-certificates.target" ] + ++ (lib.optional preliminarySelfsigned "acme-selfsigned-certificates.target"); + acmeAfterTarget = if preliminarySelfsigned + then [ "acme-selfsigned-certificates.target" ] + else [ "acme-certificates.target" ]; in { config = with cfg; lib.mkIf enable { - # Make sure postfix gets started first, so that the certificates are in place - systemd.services.dovecot2.after = [ "postfix.service" ]; + # Add target for when certificates are available + systemd.targets."mailserver-certificates" = { + wants = lib.mkIf (cfg.certificateScheme == 3) acmeWantsTarget; + after = lib.mkIf (cfg.certificateScheme == 3) acmeAfterTarget; + }; - # Create certificates and maildir folder - systemd.services.postfix = { - after = (if (certificateScheme == 3) then [ "nginx.service" ] else []); - preStart = - '' - # Create mail directory and set permissions. See - # . - mkdir -p "${mailDirectory}" - chgrp "${vmailGroupName}" "${mailDirectory}" - chmod 02770 "${mailDirectory}" + # Create self signed certificate + systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) { + wantedBy = [ "mailserver-certificates.target" ]; + after = [ "local-fs.target" ]; + before = [ "mailserver-certificates.target" ]; + script = '' + # Create certificates if they do not exist yet + dir="${cfg.certificateDirectory}" + fqdn="${cfg.fqdn}" + case $fqdn in /*) fqdn=$(cat "$fqdn");; esac + key="''${dir}/key-${cfg.fqdn}.pem"; + cert="''${dir}/cert-${cfg.fqdn}.pem"; - ${create_certificate} - - ${let - dovecotVersion = builtins.fromJSON - (builtins.readFile (pkgs.callPackage ./dovecot-version.nix {})); - in lib.optionalString - (dovecotVersion.major == 2 && dovecotVersion.minor >= 3) - createDhParameterFile} + if [ ! -f "''${key}" ] || [ ! -f "''${cert}" ] + then + mkdir -p "${cfg.certificateDirectory}" + (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "''${key}" 2048) && + "${pkgs.openssl}/bin/openssl" req -new -key "''${key}" -x509 -subj "/CN=''${fqdn}" \ + -days 3650 -out "''${cert}" + fi ''; + serviceConfig = { + Type = "oneshot"; + PrivateTmp = true; + }; + }; + + # Create maildir folder and dh parameters before dovecot startup + systemd.services.dovecot2 = { + after = [ "mailserver-certificates.target" ]; + wants = [ "mailserver-certificates.target" ]; + preStart = '' + # Create mail directory and set permissions. See + # . + mkdir -p "${mailDirectory}" + chgrp "${vmailGroupName}" "${mailDirectory}" + chmod 02770 "${mailDirectory}" + + ${createDhParameterFile} + ''; + }; + + # Postfix requires rmilter socket, dovecot lmtp socket, dovecot auth socket and certificate to work + systemd.services.postfix = { + after = [ "rmilter.socket" "dovecot2.service" "mailserver-certificates.target" ]; + wants = [ "mailserver-certificates.target" ]; + requires = [ "rmilter.socket" "dovecot2.service" ]; }; # Create dkim certificates From 0c883d8bcd8b5c0ec17552f5050db5ca3352c59b Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Fri, 4 May 2018 18:12:05 +0200 Subject: [PATCH 03/11] tests/extern: Fix small errors 1. The lowquota test used the wrong fetchmail username and password 2. When running interactively repeatedly mkdir fails since dir exists --- tests/extern.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/extern.nix b/tests/extern.nix index b552552..30a871e 100644 --- a/tests/extern.nix +++ b/tests/extern.nix @@ -87,7 +87,7 @@ import { "root/.fetchmailRcLowQuota" = { text = '' poll ${serverIP} with proto IMAP - user 'lowquota\@example.com' there with password 'user1' is 'root' here + user 'lowquota@example.com' there with password 'user2' is 'root' here mda procmail ''; mode = "0700"; @@ -217,7 +217,7 @@ import { $client->waitForUnit("multi-user.target"); $client->execute("cp -p /etc/root/.* ~/"); - $client->succeed("mkdir ~/mail"); + $client->succeed("mkdir -p ~/mail"); $client->succeed("ls -la ~/ >&2"); $client->succeed("cat ~/.fetchmailrc >&2"); $client->succeed("cat ~/.procmailrc >&2"); From 7036371f750f5f930305f35d162d84d90c26c067 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Fri, 4 May 2018 18:17:51 +0200 Subject: [PATCH 04/11] Use OpenDKIM instead of rmilter for DKIM As part of #61 this moves DKIM handling from rmilter to OpenDKIM. --- default.nix | 1 + mail-server/opendkim.nix | 90 ++++++++++++++++++++++++++++++++++++++++ mail-server/postfix.nix | 16 +++++++ mail-server/rmilter.nix | 23 ++-------- mail-server/systemd.nix | 38 ++--------------- tests/extern.nix | 8 ++++ 6 files changed, 122 insertions(+), 54 deletions(-) create mode 100644 mail-server/opendkim.nix diff --git a/default.nix b/default.nix index 36d0848..ebeb782 100644 --- a/default.nix +++ b/default.nix @@ -733,6 +733,7 @@ in ./mail-server/networking.nix ./mail-server/systemd.nix ./mail-server/dovecot.nix + ./mail-server/opendkim.nix ./mail-server/postfix.nix ./mail-server/rmilter.nix ./mail-server/nginx.nix diff --git a/mail-server/opendkim.nix b/mail-server/opendkim.nix new file mode 100644 index 0000000..d060323 --- /dev/null +++ b/mail-server/opendkim.nix @@ -0,0 +1,90 @@ +# nixos-mailserver: a simple mail server +# Copyright (C) 2017 Brian Olsen +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.mailserver; + + dkimUser = config.services.opendkim.user; + dkimGroup = config.services.opendkim.group; + + createDomainDkimCert = dom: + let + dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key"; + dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt"; + in + '' + if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ] + then + ${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \ + -d "${dom}" \ + --directory="${cfg.dkimKeyDirectory}" + mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}" + mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}" + echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}" + fi + ''; + createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains); + create_dkim_cert = + '' + # Create dkim dir + mkdir -p "${cfg.dkimKeyDirectory}" + chown ${dkimUser}:${dkimGroup} "${cfg.dkimKeyDirectory}" + + ${createAllCerts} + + chown -R ${dkimUser}:${dkimGroup} "${cfg.dkimKeyDirectory}" + ''; + + keyTable = pkgs.writeText "opendkim-KeyTable" + (lib.concatStringsSep "\n" (lib.flip map cfg.domains + (dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key"))); + signingTable = pkgs.writeText "opendkim-SigningTable" + (lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}"))); + + dkim = config.services.opendkim; + args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ]; +in +{ + config = mkIf (cfg.dkimSigning && cfg.enable) { + services.opendkim = { + enable = true; + selector = cfg.dkimSelector; + domains = "csl:${builtins.concatStringsSep "," cfg.domains}"; + configFile = pkgs.writeText "opendkim.conf" ('' + Canonicalization relaxed/simple + UMask 0002 + Socket ${dkim.socket} + KeyTable file:${keyTable} + SigningTable file:${signingTable} + '' + (lib.optionalString cfg.debug '' + Syslog yes + SyslogSuccess yes + LogWhy yes + '')); + }; + + users.users = optionalAttrs (config.services.postfix.user == "postfix") { + postfix.extraGroups = [ "${config.services.opendkim.group}" ]; + }; + systemd.services.opendkim = { + preStart = create_dkim_cert; + serviceConfig.ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}"; + }; + }; +} \ No newline at end of file diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index c35c10e..aad87bb 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -90,6 +90,17 @@ let /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}> ''); + + inetSocket = addr: port: "inet:[${toString port}@${addr}]"; + unixSocket = sock: "unix:${sock}"; + + rmilter = config.services.rmilter; + rmilterSocket = if rmilter.bindSocket.type == "unix" then unixSocket rmilter.bindSocket.path + else inetSocket rmilter.bindSocket.address rmilter.bindSocket.port; + + smtpdMilters = + (lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock") + ++ [ rmilterSocket ]; in { config = with cfg; lib.mkIf enable { @@ -151,6 +162,11 @@ in # Configure a non blocking source of randomness tls_random_source = dev:/dev/urandom + + smtpd_milters = ${lib.concatStringsSep "," smtpdMilters} + ${lib.optionalString cfg.dkimSigning "non_smtpd_milters = unix:/run/opendkim/opendkim.sock"} + milter_protocol = 6 + milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer} ''; submissionOptions = diff --git a/mail-server/rmilter.nix b/mail-server/rmilter.nix index ed2d019..aed5009 100644 --- a/mail-server/rmilter.nix +++ b/mail-server/rmilter.nix @@ -27,23 +27,8 @@ let }; '' else ""; - dkim = if cfg.dkimSigning - # Note: domain = "*"; causes Rmilter to try to search key in the key path - # as keypath/domain.selector.key for any domain. - then - '' - dkim { - domain { - key = "${cfg.dkimKeyDirectory}"; - domain = "*"; - selector = "${cfg.dkimSelector}"; - }; - sign_alg = sha256; - auth_only = yes; - header_canon = relaxed; - } - '' - else ""; + postfixCfg = config.services.postfix; + rmilter = config.services.rmilter; in { config = with cfg; lib.mkIf enable { @@ -54,7 +39,6 @@ in services.rmilter = { inherit debug; enable = true; - postfix.enable = true; rspamd = { enable = true; extraConfig = "extended_spam_headers = yes;"; @@ -65,10 +49,9 @@ in max_size = 20M; ${clamav} - - ${dkim} ''; }; + users.extraUsers.${postfixCfg.user}.extraGroups = [ rmilter.group ]; }; } diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index e083d85..9066eab 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -19,33 +19,6 @@ let cfg = config.mailserver; - createDomainDkimCert = dom: - let - dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key"; - dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt"; - in - '' - if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ] - then - ${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \ - -d "${dom}" \ - --directory="${cfg.dkimKeyDirectory}" - mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}" - mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}" - fi - ''; - createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains); - create_dkim_cert = - '' - # Create dkim dir - mkdir -p "${cfg.dkimKeyDirectory}" - chown rmilter:rmilter "${cfg.dkimKeyDirectory}" - - ${createAllCerts} - - chown -R rmilter:rmilter "${cfg.dkimKeyDirectory}" - ''; - createDhParameterFile = let dovecotVersion = builtins.fromJSON (builtins.readFile (pkgs.callPackage ./dovecot-version.nix {})); @@ -121,19 +94,16 @@ in # Postfix requires rmilter socket, dovecot lmtp socket, dovecot auth socket and certificate to work systemd.services.postfix = { - after = [ "rmilter.socket" "dovecot2.service" "mailserver-certificates.target" ]; + after = [ "rmilter.socket" "dovecot2.service" "mailserver-certificates.target" ] + ++ (lib.optional cfg.dkimSigning "opendkim.service"); wants = [ "mailserver-certificates.target" ]; - requires = [ "rmilter.socket" "dovecot2.service" ]; + requires = [ "rmilter.socket" "dovecot2.service" ] + ++ (lib.optional cfg.dkimSigning "opendkim.service"); }; - # Create dkim certificates systemd.services.rmilter = { requires = [ "rmilter.socket" ]; after = [ "rmilter.socket" ]; - preStart = - '' - ${create_dkim_cert} - ''; }; }; } diff --git a/tests/extern.nix b/tests/extern.nix index 30a871e..4ad2ff7 100644 --- a/tests/extern.nix +++ b/tests/extern.nix @@ -23,6 +23,14 @@ import { ../default.nix ]; + services.rsyslogd = { + enable = true; + defaultConfig = '' + *.* /dev/console + ''; + }; + + mailserver = { enable = true; debug = true; From f209fa3bf3425405b945cc3b336d204d6fbcbf02 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Sat, 12 May 2018 18:00:14 +0200 Subject: [PATCH 05/11] postfix: use masterConfig option instead of extraMasterConf extraMasterConf is just a string while masterConfig is a nix module so the options are more explicit and has help text. --- mail-server/postfix.nix | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index aad87bb..30f92ec 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -183,11 +183,16 @@ in smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; cleanup_service_name = "submission-header-cleanup"; }; - - extraMasterConf = '' - submission-header-cleanup unix n - n - 0 cleanup - -o header_checks=pcre:${submissionHeaderCleanupRules} - ''; + masterConfig = { + "submission-header-cleanup" = { + type = "unix"; + private = false; + chroot = false; + maxproc = 0; + command = "cleanup"; + args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"]; + }; + }; }; }; } From e32a915489a694a8c98e7ba1daae6c067f920e61 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Sat, 12 May 2018 18:02:37 +0200 Subject: [PATCH 06/11] postfix: Use pypolicyd-spf for SPF checking --- mail-server/postfix.nix | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 30f92ec..130162d 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -101,6 +101,20 @@ let smtpdMilters = (lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock") ++ [ rmilterSocket ]; + + policyd-spf = pkgs.writeText "policyd-spf.conf" ('' + TestOnly = 1 + + HELO_reject = Fail + Mail_From_reject = Fail + + PermError_reject = False + TempError_Defer = False + + skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 + '' + (lib.optionalString cfg.debug '' + debugLevel = 4 + '')); in { config = with cfg; lib.mkIf enable { @@ -140,8 +154,13 @@ in smtpd_sasl_auth_enable = yes smtpd_relay_restrictions = permit_mynetworks, permit_sasl_authenticated, reject_unauth_destination - # reject selected recipients, quota - smtpd_recipient_restrictions = check_recipient_access hash:/var/lib/postfix/conf/reject_recipients, check_policy_service inet:localhost:12340 + policy-spf_time_limit = 3600s + + # quota and spf checking + smtpd_recipient_restrictions = + check_recipient_access hash:/var/lib/postfix/conf/reject_recipients, + check_policy_service inet:localhost:12340, + check_policy_service unix:private/policy-spf # TLS settings, inspired by https://github.com/jeaye/nix-files # Submission by mail clients is handled in submissionOptions @@ -184,6 +203,13 @@ in cleanup_service_name = "submission-header-cleanup"; }; masterConfig = { + "policy-spf" = { + type = "unix"; + privileged = true; + chroot = false; + command = "spawn"; + args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"]; + }; "submission-header-cleanup" = { type = "unix"; private = false; From 1c76e0a11985142235ec49ee168c74b70b05e771 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Sun, 13 May 2018 02:40:58 +0200 Subject: [PATCH 07/11] tests: Add ClamAV test and fix errors in virus scanning --- .gitattributes | 1 + .travis.yml | 1 + mail-server/rmilter.nix | 2 +- mail-server/systemd.nix | 4 +- tests/clamav.nix | 228 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 233 insertions(+), 3 deletions(-) create mode 100644 .gitattributes create mode 100644 tests/clamav.nix diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2e6588b --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.cvd filter=lfs diff=lfs merge=lfs -text diff --git a/.travis.yml b/.travis.yml index 8d276fe..39faf05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,3 +5,4 @@ env: script: - nix-build tests/intern.nix - nix-build tests/extern.nix + - nix-build tests/clamav.nix diff --git a/mail-server/rmilter.nix b/mail-server/rmilter.nix index aed5009..9401ece 100644 --- a/mail-server/rmilter.nix +++ b/mail-server/rmilter.nix @@ -23,7 +23,7 @@ let then '' clamav { - servers = /var/run/clamav/clamd.ctl; + servers = /run/clamav/clamd.ctl; }; '' else ""; diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 9066eab..9567a98 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -102,8 +102,8 @@ in }; systemd.services.rmilter = { - requires = [ "rmilter.socket" ]; - after = [ "rmilter.socket" ]; + requires = [ "rmilter.socket" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); + after = [ "rmilter.socket" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); }; }; } diff --git a/tests/clamav.nix b/tests/clamav.nix new file mode 100644 index 0000000..166a5f8 --- /dev/null +++ b/tests/clamav.nix @@ -0,0 +1,228 @@ +# nixos-mailserver: a simple mail server +# Copyright (C) 2016-2018 Robin Raymond +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see + +import { + + nodes = { + server = { config, pkgs, lib, ... }: + let + clamav-db = pkgs.srcOnly { + name = "ClamAV-db"; + src = pkgs.fetchurl { + url = "https://files.griff.name/ClamAV-db.tar"; + sha256 = "eecad99f4c071d216bd91565f84c0d90a1f93e5e3e22d8f3087686ba3bd219e7"; + }; + }; + in + { + imports = [ + ../default.nix + ]; + + virtualisation.memorySize = 1500; + + services.rsyslogd = { + enable = true; + defaultConfig = '' + *.* /dev/console + ''; + }; + + services.clamav.updater.enable = lib.mkForce false; + systemd.services.old-clam = { + before = [ "clamav-daemon.service" ]; + requiredBy = [ "clamav-daemon.service" ]; + description = "ClamAV virus database"; + + preStart = '' + mkdir -m 0755 -p /var/lib/clamav + chown clamav:clamav /var/lib/clamav + ''; + + script = '' + cp ${clamav-db}/bytecode.cvd /var/lib/clamav/ + cp ${clamav-db}/main.cvd /var/lib/clamav/ + cp ${clamav-db}/daily.cvd /var/lib/clamav/ + chown clamav:clamav /var/lib/clamav/* + ''; + + serviceConfig = { + Type = "oneshot"; + PrivateTmp = "yes"; + PrivateDevices = "yes"; + }; + }; + + mailserver = { + enable = true; + debug = true; + fqdn = "mail.example.com"; + domains = [ "example.com" "example2.com" ]; + dhParamBitLength = 512; + virusScanning = true; + + loginAccounts = { + "user1@example.com" = { + hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; + aliases = [ "postmaster@example.com" ]; + catchAll = [ "example.com" ]; + }; + "user@example2.com" = { + hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; + }; + }; + enableImap = true; + }; + + environment.etc = { + "root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; + }; + }; + client = { nodes, config, pkgs, ... }: let + serverIP = nodes.server.config.networking.primaryIPAddress; + clientIP = nodes.client.config.networking.primaryIPAddress; + grep-ip = pkgs.writeScriptBin "grep-ip" '' + #!${pkgs.stdenv.shell} + echo grep '${clientIP}' "$@" >&2 + exec grep '${clientIP}' "$@" + ''; + in { + environment.systemPackages = with pkgs; [ + fetchmail msmtp procmail findutils grep-ip + ]; + environment.etc = { + "root/.fetchmailrc" = { + text = '' + poll ${serverIP} with proto IMAP + user 'user1@example.com' there with password 'user1' is 'root' here + mda procmail + ''; + mode = "0700"; + }; + "root/.procmailrc" = { + text = "DEFAULT=$HOME/mail"; + }; + "root/.msmtprc" = { + text = '' + account test2 + host ${serverIP} + port 587 + from user@example2.com + user user@example2.com + password user2 + ''; + }; + "root/virus-email".text = '' + From: User2 + Content-Type: multipart/mixed; + boundary="Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607" + Mime-Version: 1.0 (Mac OS X Mail 11.3 \(3445.6.18\)) + Subject: Testy McTest + Message-Id: <94550DD9-1FF1-4ED1-9F09-8812FF2E59AA@example.com> + Date: Sat, 12 May 2018 14:15:44 +0200 + To: User1 + X-Mailer: Apple Mail (2.3445.6.18) + + + --Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607 + Content-Transfer-Encoding: 7bit + Content-Type: text/plain; + charset=us-ascii + + Hello + + I have attached a dangerous virus. + + Mfg. + User2 + + + --Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607 + Content-Disposition: attachment; + filename=eicar.com.txt + Content-Type: text/plain; + x-unix-mode=0644; + name="eicar.com.txt" + Content-Transfer-Encoding: 7bit + + X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H* + --Apple-Mail=_2689C63E-FD18-4E4D-8822-54797BDA9607-- + ''; + "root/email2".text = '' + From: User + To: User1 + Cc: + Bcc: + Subject: This is a test Email from user@example2.com to user1 + Reply-To: + + Hello User1, + + how are you doing today? + + XOXO User1 + ''; + }; + }; + }; + + testScript = + '' + startAll; + + $server->waitForUnit("multi-user.target"); + $client->waitForUnit("multi-user.target"); + + $client->execute("cp -p /etc/root/.* ~/"); + $client->succeed("mkdir -p ~/mail"); + $client->succeed("ls -la ~/ >&2"); + $client->succeed("cat ~/.fetchmailrc >&2"); + $client->succeed("cat ~/.procmailrc >&2"); + $client->succeed("cat ~/.msmtprc >&2"); + + # fetchmail returns EXIT_CODE 1 when no new mail + $client->succeed("fetchmail -v || [ \$? -eq 1 ] >&2"); + + # Verify that mail can be sent and received before testing virus scanner + $client->execute("rm ~/mail/*"); + $client->succeed("msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email2 >&2"); + # give the mail server some time to process the mail + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + $client->execute("rm ~/mail/*"); + # fetchmail returns EXIT_CODE 0 when it retrieves mail + $client->succeed("fetchmail -v >&2"); + $client->execute("rm ~/mail/*"); + + + subtest "virus scan file", sub { + $server->fail("clamscan --follow-file-symlinks=2 -r /etc/root/ >&2"); + }; + + subtest "virus scanner", sub { + $client->fail("msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/virus-email >&2"); + # give the mail server some time to process the mail + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + }; + + subtest "no warnings or errors", sub { + $server->fail("journalctl -u postfix | grep -i error >&2"); + $server->fail("journalctl -u postfix | grep -i warning >&2"); + $server->fail("journalctl -u dovecot2 | grep -i error >&2"); + $server->fail("journalctl -u dovecot2 | grep -i warning >&2"); + }; + + ''; +} From 410c6c410b214373ee44599eb01ea2ab75fee82f Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Sun, 13 May 2018 03:17:22 +0200 Subject: [PATCH 08/11] Use nixpkgs functions to check dovecot version --- mail-server/dovecot-version.nix | 12 ------------ mail-server/dovecot.nix | 5 +---- mail-server/systemd.nix | 7 ++----- 3 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 mail-server/dovecot-version.nix diff --git a/mail-server/dovecot-version.nix b/mail-server/dovecot-version.nix deleted file mode 100644 index 4c00972..0000000 --- a/mail-server/dovecot-version.nix +++ /dev/null @@ -1,12 +0,0 @@ -{ dovecot, gawk, gnused, jq, runCommand }: - -runCommand "dovecot-version" { - buildInputs = [dovecot gnused jq]; -} '' - jq -n \ - --arg dovecot_version "$(dovecot --version | - sed 's/\([0-9.]*\).*/\1/' | - awk -F '.' '{ print $1"."$2"."$3 }')" \ - '[$dovecot_version | split("."), ["major", "minor", "patch"]] - | transpose | map( { (.[1]): .[0] | tonumber }) | add' > $out -'' diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index bb85c21..604a62e 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -23,9 +23,6 @@ let maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; - dovecotVersion = builtins.fromJSON - (builtins.readFile (pkgs.callPackage ./dovecot-version.nix {})); - # maildir in format "/${domain}/${user}" dovecotMaildir = "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"; @@ -79,7 +76,7 @@ in mail_access_groups = ${vmailGroupName} ssl = required - ${lib.optionalString (dovecotVersion.major == 2 && dovecotVersion.minor >= 3) '' + ${lib.optionalString (lib.versionAtLeast (lib.getVersion pkgs.dovecot) "2.3") '' ssl_dh = <${certificateDirectory}/dh.pem ''} diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 9567a98..3bc78bf 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -19,11 +19,8 @@ let cfg = config.mailserver; - createDhParameterFile = let - dovecotVersion = builtins.fromJSON - (builtins.readFile (pkgs.callPackage ./dovecot-version.nix {})); - in lib.optionalString - (dovecotVersion.major == 2 && dovecotVersion.minor >= 3) + createDhParameterFile = + lib.optionalString (lib.versionAtLeast (lib.getVersion pkgs.dovecot) "2.3") '' # Create a dh parameter file if [ ! -s "${cfg.certificateDirectory}/dh.pem" ] From 616d779e1f73872deddd7c38d32f54dd006f0347 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Sun, 13 May 2018 03:35:03 +0200 Subject: [PATCH 09/11] Move from rmilter to rspamd #25 --- default.nix | 2 +- mail-server/environment.nix | 2 +- mail-server/postfix.nix | 6 +-- mail-server/rmilter.nix | 57 --------------------------- mail-server/rspamd.nix | 78 +++++++++++++++++++++++++++++++++++++ mail-server/systemd.nix | 11 ++---- 6 files changed, 84 insertions(+), 72 deletions(-) delete mode 100644 mail-server/rmilter.nix create mode 100644 mail-server/rspamd.nix diff --git a/default.nix b/default.nix index ebeb782..376d47a 100644 --- a/default.nix +++ b/default.nix @@ -735,7 +735,7 @@ in ./mail-server/dovecot.nix ./mail-server/opendkim.nix ./mail-server/postfix.nix - ./mail-server/rmilter.nix + ./mail-server/rspamd.nix ./mail-server/nginx.nix ./mail-server/kresd.nix ./mail-server/post-upgrade-check.nix diff --git a/mail-server/environment.nix b/mail-server/environment.nix index e0c902a..cc85202 100644 --- a/mail-server/environment.nix +++ b/mail-server/environment.nix @@ -22,7 +22,7 @@ in { config = with cfg; lib.mkIf enable { environment.systemPackages = with pkgs; [ - dovecot opendkim openssh postfix rspamd rmilter + dovecot opendkim openssh postfix rspamd ] ++ (if certificateScheme == 2 then [ openssl ] else []); }; } diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 130162d..01006ef 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -94,13 +94,9 @@ let inetSocket = addr: port: "inet:[${toString port}@${addr}]"; unixSocket = sock: "unix:${sock}"; - rmilter = config.services.rmilter; - rmilterSocket = if rmilter.bindSocket.type == "unix" then unixSocket rmilter.bindSocket.path - else inetSocket rmilter.bindSocket.address rmilter.bindSocket.port; - smtpdMilters = (lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock") - ++ [ rmilterSocket ]; + ++ [ "unix:/run/rspamd/rspamd-milter.sock" ]; policyd-spf = pkgs.writeText "policyd-spf.conf" ('' TestOnly = 1 diff --git a/mail-server/rmilter.nix b/mail-server/rmilter.nix deleted file mode 100644 index 9401ece..0000000 --- a/mail-server/rmilter.nix +++ /dev/null @@ -1,57 +0,0 @@ -# nixos-mailserver: a simple mail server -# Copyright (C) 2016-2018 Robin Raymond -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see - -{ config, pkgs, lib, ... }: - -let - cfg = config.mailserver; - - clamav = if cfg.virusScanning - then - '' - clamav { - servers = /run/clamav/clamd.ctl; - }; - '' - else ""; - postfixCfg = config.services.postfix; - rmilter = config.services.rmilter; -in -{ - config = with cfg; lib.mkIf enable { - services.rspamd = { - enable = true; - }; - - services.rmilter = { - inherit debug; - enable = true; - rspamd = { - enable = true; - extraConfig = "extended_spam_headers = yes;"; - }; - extraConfig = - '' - use_redis = true; - max_size = 20M; - - ${clamav} - ''; - }; - users.extraUsers.${postfixCfg.user}.extraGroups = [ rmilter.group ]; - }; -} - diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix new file mode 100644 index 0000000..e7e80a8 --- /dev/null +++ b/mail-server/rspamd.nix @@ -0,0 +1,78 @@ +# nixos-mailserver: a simple mail server +# Copyright (C) 2016-2018 Robin Raymond +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see + +{ config, pkgs, lib, ... }: + +let + cfg = config.mailserver; + + postfixCfg = config.services.postfix; + rspamdCfg = config.services.rspamd; + rspamdSocket = if rspamdCfg.socketActivation + then "rspamd-rspamd_proxy-1.socket" + else "rspamd.service"; +in +{ + config = with cfg; lib.mkIf enable { + services.rspamd = { + enable = true; + socketActivation = false; + extraConfig = '' + extended_spam_headers = yes; + '' + (lib.optionalString cfg.virusScanning '' + antivirus { + clamav { + action = "reject"; + symbol = "CLAM_VIRUS"; + type = "clamav"; + log_clean = true; + servers = "/run/clamav/clamd.ctl"; + } + } + ''); + + workers.rspamd_proxy = { + type = "proxy"; + bindSockets = [{ + socket = "/run/rspamd/rspamd-milter.sock"; + mode = "0664"; + }]; + count = 1; # Do not spawn too many processes of this type + extraConfig = '' + milter = yes; # Enable milter mode + timeout = 120s; # Needed for Milter usually + + upstream "local" { + default = yes; # Self-scan upstreams are always default + self_scan = yes; # Enable self-scan + } + ''; + }; + }; + systemd.services.rspamd = { + requires = (lib.optional cfg.virusScanning "clamav-daemon.service"); + after = (lib.optional cfg.virusScanning "clamav-daemon.service"); + }; + + systemd.services.postfix = { + after = [ rspamdSocket ]; + requires = [ rspamdSocket ]; + }; + + users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ]; + }; +} + diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix index 3bc78bf..a4a9285 100644 --- a/mail-server/systemd.nix +++ b/mail-server/systemd.nix @@ -89,18 +89,13 @@ in ''; }; - # Postfix requires rmilter socket, dovecot lmtp socket, dovecot auth socket and certificate to work + # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work systemd.services.postfix = { - after = [ "rmilter.socket" "dovecot2.service" "mailserver-certificates.target" ] + after = [ "dovecot2.service" "mailserver-certificates.target" ] ++ (lib.optional cfg.dkimSigning "opendkim.service"); wants = [ "mailserver-certificates.target" ]; - requires = [ "rmilter.socket" "dovecot2.service" ] + requires = [ "dovecot2.service" ] ++ (lib.optional cfg.dkimSigning "opendkim.service"); }; - - systemd.services.rmilter = { - requires = [ "rmilter.socket" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); - after = [ "rmilter.socket" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); - }; }; } From 61df799036bafa3cf39d1fab39d7ad5110e67c12 Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Tue, 15 May 2018 04:17:18 +0200 Subject: [PATCH 10/11] dovecot: Add spam filter traning using imapsieve --- mail-server/dovecot.nix | 47 +++++++++++++ .../dovecot/imap_sieve/report-ham.sieve | 15 ++++ .../dovecot/imap_sieve/report-spam.sieve | 7 ++ mail-server/dovecot/pipe_bin/sa-learn-ham.sh | 3 + mail-server/dovecot/pipe_bin/sa-learn-spam.sh | 3 + mail-server/rspamd.nix | 10 +++ tests/extern.nix | 69 ++++++++++++++++++- 7 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 mail-server/dovecot/imap_sieve/report-ham.sieve create mode 100644 mail-server/dovecot/imap_sieve/report-spam.sieve create mode 100755 mail-server/dovecot/pipe_bin/sa-learn-ham.sh create mode 100755 mail-server/dovecot/pipe_bin/sa-learn-spam.sh diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index 604a62e..2be417d 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -27,6 +27,26 @@ let dovecotMaildir = "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"; postfixCfg = config.services.postfix; + dovecot2Cfg = config.services.dovecot2; + + stateDir = "/var/lib/dovecot"; + + pipeBin = pkgs.stdenv.mkDerivation { + name = "pipe_bin"; + src = ./dovecot/pipe_bin; + buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ]; + buildCommand = '' + mkdir -p $out/pipe/bin + cp $src/* $out/pipe/bin/ + chmod a+x $out/pipe/bin/* + patchShebangs $out/pipe/bin + + for file in $out/pipe/bin/*; do + wrapProgram $file \ + --set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin" + done + ''; + }; in { config = with cfg; lib.mkIf enable { @@ -68,6 +88,7 @@ in protocol imap { mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser} + mail_plugins = $mail_plugins imap_sieve } protocol pop3 { @@ -118,14 +139,40 @@ in } plugin { + sieve_plugins = sieve_imapsieve sieve_extprograms sieve = file:/var/sieve/%u/scripts;active=/var/sieve/%u/active.sieve sieve_default = file:/var/sieve/%u/default.sieve sieve_default_name = default + + # From elsewhere to Spam folder + imapsieve_mailbox1_name = Junk + imapsieve_mailbox1_causes = COPY + imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve + + # From Spam folder to elsewhere + imapsieve_mailbox2_name = * + imapsieve_mailbox2_from = Junk + imapsieve_mailbox2_causes = COPY + imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve + + sieve_pipe_bin_dir = ${pipeBin}/pipe/bin + + sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment } lda_mailbox_autosubscribe = yes lda_mailbox_autocreate = yes ''; }; + + systemd.services.dovecot2.preStart = '' + rm -rf '${stateDir}/imap_sieve' + mkdir '${stateDir}/imap_sieve' + cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/' + for k in "${stateDir}/imap_sieve"/*.sieve ; do + ${pkgs.dovecot_pigeonhole}/bin/sievec "$k" + done + chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve' + ''; }; } diff --git a/mail-server/dovecot/imap_sieve/report-ham.sieve b/mail-server/dovecot/imap_sieve/report-ham.sieve new file mode 100644 index 0000000..da74b34 --- /dev/null +++ b/mail-server/dovecot/imap_sieve/report-ham.sieve @@ -0,0 +1,15 @@ +require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; + +if environment :matches "imap.mailbox" "*" { + set "mailbox" "${1}"; +} + +if string "${mailbox}" "Trash" { + stop; +} + +if environment :matches "imap.user" "*" { + set "username" "${1}"; +} + +pipe :copy "sa-learn-ham.sh" [ "${username}" ]; \ No newline at end of file diff --git a/mail-server/dovecot/imap_sieve/report-spam.sieve b/mail-server/dovecot/imap_sieve/report-spam.sieve new file mode 100644 index 0000000..4024b7a --- /dev/null +++ b/mail-server/dovecot/imap_sieve/report-spam.sieve @@ -0,0 +1,7 @@ +require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; + +if environment :matches "imap.user" "*" { + set "username" "${1}"; +} + +pipe :copy "sa-learn-spam.sh" [ "${username}" ]; \ No newline at end of file diff --git a/mail-server/dovecot/pipe_bin/sa-learn-ham.sh b/mail-server/dovecot/pipe_bin/sa-learn-ham.sh new file mode 100755 index 0000000..76fc4ed --- /dev/null +++ b/mail-server/dovecot/pipe_bin/sa-learn-ham.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -o errexit +exec rspamc -h /run/rspamd/worker-controller.sock learn_ham \ No newline at end of file diff --git a/mail-server/dovecot/pipe_bin/sa-learn-spam.sh b/mail-server/dovecot/pipe_bin/sa-learn-spam.sh new file mode 100755 index 0000000..2a2f766 --- /dev/null +++ b/mail-server/dovecot/pipe_bin/sa-learn-spam.sh @@ -0,0 +1,3 @@ +#!/bin/bash +set -o errexit +exec rspamc -h /run/rspamd/worker-controller.sock learn_spam \ No newline at end of file diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix index e7e80a8..950ae56 100644 --- a/mail-server/rspamd.nix +++ b/mail-server/rspamd.nix @@ -61,6 +61,16 @@ in } ''; }; + workers.controller = { + type = "controller"; + count = 1; + bindSockets = [{ + socket = "/run/rspamd/worker-controller.sock"; + mode = "0666"; + }]; + includes = []; + }; + }; systemd.services.rspamd = { requires = (lib.optional cfg.virusScanning "clamav-daemon.service"); diff --git a/tests/extern.nix b/tests/extern.nix index 4ad2ff7..7ccd9ca 100644 --- a/tests/extern.nix +++ b/tests/extern.nix @@ -64,6 +64,7 @@ import { }; enableImap = true; + enableImapSsl = true; }; }; client = { nodes, config, pkgs, ... }: let @@ -79,9 +80,63 @@ import { echo grep '^Message-ID:.*@mail.example.com>$' "$@" >&2 exec grep '^Message-ID:.*@mail.example.com>$' "$@" ''; + test-imap-spam = pkgs.writeScriptBin "imap-mark-spam" '' + #!${pkgs.python3.interpreter} + import imaplib + + with imaplib.IMAP4_SSL('${serverIP}') as imap: + imap.login('user1@example.com', 'user1') + imap.select() + status, [response] = imap.search(None, 'ALL') + msg_ids = response.decode("utf-8").split(' ') + print(msg_ids) + assert status == 'OK' + assert len(msg_ids) == 1 + + imap.copy(','.join(msg_ids), 'Junk') + for num in msg_ids: + imap.store(num, '+FLAGS', '\\Deleted') + imap.expunge() + + imap.select('Junk') + status, [response] = imap.search(None, 'ALL') + msg_ids = response.decode("utf-8").split(' ') + print(msg_ids) + assert status == 'OK' + assert len(msg_ids) == 1 + + imap.close() + ''; + test-imap-ham = pkgs.writeScriptBin "imap-mark-ham" '' + #!${pkgs.python3.interpreter} + import imaplib + + with imaplib.IMAP4_SSL('${serverIP}') as imap: + imap.login('user1@example.com', 'user1') + imap.select('Junk') + status, [response] = imap.search(None, 'ALL') + msg_ids = response.decode("utf-8").split(' ') + print(msg_ids) + assert status == 'OK' + assert len(msg_ids) == 1 + + imap.copy(','.join(msg_ids), 'INBOX') + for num in msg_ids: + imap.store(num, '+FLAGS', '\\Deleted') + imap.expunge() + + imap.select('INBOX') + status, [response] = imap.search(None, 'ALL') + msg_ids = response.decode("utf-8").split(' ') + print(msg_ids) + assert status == 'OK' + assert len(msg_ids) == 1 + + imap.close() + ''; in { environment.systemPackages = with pkgs; [ - fetchmail msmtp procmail findutils grep-ip check-mail-id + fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham ]; environment.etc = { "root/.fetchmailrc" = { @@ -325,6 +380,18 @@ import { }; + subtest "imap sieve junk trainer", sub { + # send email from user2 to user1 + $client->succeed("msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"); + # give the mail server some time to process the mail + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + + $client->succeed("imap-mark-spam >&2"); + $server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2"); + $client->succeed("imap-mark-ham >&2"); + $server->waitUntilSucceeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2"); + }; + subtest "no warnings or errors", sub { $server->fail("journalctl -u postfix | grep -i error >&2"); $server->fail("journalctl -u postfix | grep -i warning >&2"); From 88e292c5b7a7cf675fba418cb2f6c4421496863b Mon Sep 17 00:00:00 2001 From: Brian Olsen Date: Thu, 17 May 2018 02:01:49 +0200 Subject: [PATCH 11/11] postfix: Support setting options for policyd-spf --- default.nix | 12 ++++++++++++ mail-server/postfix.nix | 14 +++----------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/default.nix b/default.nix index 376d47a..0134541 100644 --- a/default.nix +++ b/default.nix @@ -454,6 +454,18 @@ in ''; }; + policydSPFExtraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 + ''; + description = '' + Extra configuration options for policyd-spf. This can be use to among + other things skip spf checking for some IP addresses. + ''; + }; + monitoring = { enable = mkEnableOption "monitoring via monit"; diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 01006ef..4a00e39 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -98,17 +98,9 @@ let (lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock") ++ [ "unix:/run/rspamd/rspamd-milter.sock" ]; - policyd-spf = pkgs.writeText "policyd-spf.conf" ('' - TestOnly = 1 - - HELO_reject = Fail - Mail_From_reject = Fail - - PermError_reject = False - TempError_Defer = False - - skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1 - '' + (lib.optionalString cfg.debug '' + policyd-spf = pkgs.writeText "policyd-spf.conf" ( + cfg.policydSPFExtraConfig + + (lib.optionalString cfg.debug '' debugLevel = 4 '')); in