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/default.nix b/default.nix
index 36d0848..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";
@@ -733,8 +745,9 @@ 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/rspamd.nix
./mail-server/nginx.nix
./mail-server/kresd.nix
./mail-server/post-upgrade-check.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 4294a2d..2be417d 100644
--- a/mail-server/dovecot.nix
+++ b/mail-server/dovecot.nix
@@ -23,11 +23,30 @@ 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}";
+
+ 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 {
@@ -69,6 +88,7 @@ in
protocol imap {
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
+ mail_plugins = $mail_plugins imap_sieve
}
protocol pop3 {
@@ -77,15 +97,15 @@ 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
''}
service lmtp {
- unix_listener /var/lib/postfix/queue/private/dovecot-lmtp {
- group = postfix
+ unix_listener dovecot-lmtp {
+ group = ${postfixCfg.group}
mode = 0600
- user = postfix # TODO: < make variable
+ user = ${postfixCfg.user}
}
}
@@ -104,10 +124,10 @@ in
}
service auth {
- unix_listener /var/lib/postfix/queue/private/auth {
+ unix_listener auth {
mode = 0660
- user = postfix # TODO: < make variable
- group = postfix # TODO: < make variable
+ user = ${postfixCfg.user}
+ group = ${postfixCfg.group}
}
}
@@ -119,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/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/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 b145122..4a00e39 100644
--- a/mail-server/postfix.nix
+++ b/mail-server/postfix.nix
@@ -90,6 +90,19 @@ let
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
'');
+
+ inetSocket = addr: port: "inet:[${toString port}@${addr}]";
+ unixSocket = sock: "unix:${sock}";
+
+ smtpdMilters =
+ (lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
+ ++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
+
+ policyd-spf = pkgs.writeText "policyd-spf.conf" (
+ cfg.policydSPFExtraConfig
+ + (lib.optionalString cfg.debug ''
+ debugLevel = 4
+ ''));
in
{
config = with cfg; lib.mkIf enable {
@@ -121,16 +134,21 @@ 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
- # 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
@@ -151,6 +169,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 =
@@ -158,7 +181,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";
@@ -167,11 +190,23 @@ 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 = {
+ "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;
+ chroot = false;
+ maxproc = 0;
+ command = "cleanup";
+ args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
+ };
+ };
};
};
}
diff --git a/mail-server/rmilter.nix b/mail-server/rmilter.nix
deleted file mode 100644
index ed2d019..0000000
--- a/mail-server/rmilter.nix
+++ /dev/null
@@ -1,74 +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 = /var/run/clamav/clamd.ctl;
- };
- ''
- 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 "";
-in
-{
- config = with cfg; lib.mkIf enable {
- services.rspamd = {
- enable = true;
- };
-
- services.rmilter = {
- inherit debug;
- enable = true;
- postfix.enable = true;
- rspamd = {
- enable = true;
- extraConfig = "extended_spam_headers = yes;";
- };
- extraConfig =
- ''
- use_redis = true;
- max_size = 20M;
-
- ${clamav}
-
- ${dkim}
- '';
- };
- };
-}
-
diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix
new file mode 100644
index 0000000..950ae56
--- /dev/null
+++ b/mail-server/rspamd.nix
@@ -0,0 +1,88 @@
+# 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
+ }
+ '';
+ };
+ 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");
+ 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 1a3f8b9..a4a9285 100644
--- a/mail-server/systemd.nix
+++ b/mail-server/systemd.nix
@@ -19,99 +19,83 @@
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
- '';
+ lib.optionalString (lib.versionAtLeast (lib.getVersion pkgs.dovecot) "2.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
+ '';
- 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}"
- '';
+ 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}
+ 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;
+ };
+ };
- ${let
- dovecotVersion = builtins.fromJSON
- (builtins.readFile (pkgs.callPackage ./dovecot-version.nix {}));
- in lib.optionalString
- (dovecotVersion.major == 2 && dovecotVersion.minor >= 3)
- createDhParameterFile}
+ # 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}
'';
};
- # Create dkim certificates
- systemd.services.rmilter = {
- requires = [ "rmilter.socket" ];
- after = [ "rmilter.socket" ];
- preStart =
- ''
- ${create_dkim_cert}
- '';
+ # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
+ systemd.services.postfix = {
+ after = [ "dovecot2.service" "mailserver-certificates.target" ]
+ ++ (lib.optional cfg.dkimSigning "opendkim.service");
+ wants = [ "mailserver-certificates.target" ];
+ requires = [ "dovecot2.service" ]
+ ++ (lib.optional cfg.dkimSigning "opendkim.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");
+ };
+
+ '';
+}
diff --git a/tests/extern.nix b/tests/extern.nix
index b552552..7ccd9ca 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;
@@ -56,6 +64,7 @@ import {
};
enableImap = true;
+ enableImapSsl = true;
};
};
client = { nodes, config, pkgs, ... }: let
@@ -71,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" = {
@@ -87,7 +150,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 +280,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");
@@ -317,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");