diff --git a/tests/clamav/bytecode.cvd b/tests/clamav/bytecode.cvd new file mode 100644 index 0000000..c0f3ab6 --- /dev/null +++ b/tests/clamav/bytecode.cvd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6800da4e3740b611e4f8a8e835be4a867abf8009af502e5bbf038d3ad162fa8 +size 187426 diff --git a/tests/clamav/daily.cvd b/tests/clamav/daily.cvd new file mode 100644 index 0000000..309e02a --- /dev/null +++ b/tests/clamav/daily.cvd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da96006e191412806bac1a2cef5d48ed0ec1b46efa150cf0aa6c27e734f4c4f8 +size 49476126 diff --git a/tests/clamav/freshclam.conf b/tests/clamav/freshclam.conf new file mode 100644 index 0000000..3d9ca5f --- /dev/null +++ b/tests/clamav/freshclam.conf @@ -0,0 +1 @@ +DatabaseMirror database.clamav.net diff --git a/tests/clamav/hashes.json b/tests/clamav/hashes.json new file mode 100644 index 0000000..54895d3 --- /dev/null +++ b/tests/clamav/hashes.json @@ -0,0 +1,5 @@ +{ + "bytecode.cvd": "a6800da4e3740b611e4f8a8e835be4a867abf8009af502e5bbf038d3ad162fa8", + "daily.cvd": "da96006e191412806bac1a2cef5d48ed0ec1b46efa150cf0aa6c27e734f4c4f8", + "main.cvd": "081884225087021e718599e8458ff6c9ee3cdebed8775dd8e445fc7b589d88a6" +} diff --git a/tests/clamav/main.cvd b/tests/clamav/main.cvd new file mode 100644 index 0000000..50b4ac5 --- /dev/null +++ b/tests/clamav/main.cvd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:081884225087021e718599e8458ff6c9ee3cdebed8775dd8e445fc7b589d88a6 +size 117892267 diff --git a/tests/clamav/update-clamav-database.sh b/tests/clamav/update-clamav-database.sh new file mode 100755 index 0000000..91f1ce1 --- /dev/null +++ b/tests/clamav/update-clamav-database.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -e + +cd "$(dirname "${0}")" + +rm ./*.cvd hashes.json || : + +freshclam --datadir=. --config-file=freshclam.conf +(for i in ./*.cvd; + do echo '{}' | + jq --arg path "$(basename "${i}")" \ + --arg sha256sum "$(sha256sum "${i}" | awk '{ print $1; }')" \ + '.[$path] = $sha256sum'; done) | + jq -s add > hashes.json diff --git a/tests/extern.nix b/tests/extern.nix new file mode 100644 index 0000000..301b0ff --- /dev/null +++ b/tests/extern.nix @@ -0,0 +1,414 @@ +# 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, ... }: + { + imports = [ + ../default.nix + ./lib/config.nix + ]; + + services.rsyslogd = { + enable = true; + defaultConfig = '' + *.* /dev/console + ''; + }; + + + mailserver = { + enable = true; + debug = true; + fqdn = "mail.example.com"; + domains = [ "example.com" "example2.com" ]; + rewriteMessageId = true; + dkimKeyBits = 1535; + + loginAccounts = { + "user1@example.com" = { + hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; + aliases = [ "postmaster@example.com" ]; + catchAll = [ "example.com" ]; + }; + "user2@example.com" = { + hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; + aliases = [ "chuck@example.com" ]; + }; + "user@example2.com" = { + hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; + }; + "lowquota@example.com" = { + hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0"; + quota = "1B"; + }; + }; + + extraVirtualAliases = { + "single-alias@example.com" = "user1@example.com"; + "multi-alias@example.com" = [ "user1@example.com" "user2@example.com" ]; + }; + + enableImap = true; + enableImapSsl = true; + }; + }; + 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}' "$@" + ''; + check-mail-id = pkgs.writeScriptBin "check-mail-id" '' + #!${pkgs.stdenv.shell} + 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 { + imports = [ + ./lib/config.nix + ]; + environment.systemPackages = with pkgs; [ + fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham + ]; + 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/.fetchmailRcLowQuota" = { + text = '' + poll ${serverIP} with proto IMAP + user 'lowquota@example.com' there with password 'user2' is 'root' here + mda procmail + ''; + mode = "0700"; + }; + "root/.procmailrc" = { + text = "DEFAULT=$HOME/mail"; + }; + "root/.msmtprc" = { + text = '' + account test + host ${serverIP} + port 587 + from user2@example.com + user user2@example.com + password user2 + + account test2 + host ${serverIP} + port 587 + from user@example2.com + user user@example2.com + password user2 + + account test3 + host ${serverIP} + port 587 + from chuck@example.com + user user2@example.com + password user2 + + account test4 + host ${serverIP} + port 587 + from postmaster@example.com + user user1@example.com + password user1 + + account test5 + host ${serverIP} + port 587 + from single-alias@example.com + user user1@example.com + password user1 + ''; + }; + "root/email1".text = '' + Message-ID: <12345qwerty@host.local.network> + From: User2 + To: User1 + Cc: + Bcc: + Subject: This is a test Email from user2 to user1 + Reply-To: + + Hello User1, + + how are you doing today? + ''; + "root/email2".text = '' + Message-ID: <232323abc@host.local.network> + 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 + ''; + "root/email3".text = '' + Message-ID: + From: Postmaster + To: Chuck + Cc: + Bcc: + Subject: This is a test Email from postmaster\@example.com to chuck + Reply-To: + + Hello Chuck, + + I think I may have misconfigured the mail server + XOXO Postmaster + ''; + "root/email4".text = '' + Message-ID: + From: Single Alias + To: User1 + Cc: + Bcc: + Subject: This is a test Email from single-alias\@example.com to user1 + Reply-To: + + Hello User1, + + how are you doing today? + + XOXO User1 aka Single Alias + ''; + "root/email5".text = '' + Message-ID: <789asdf@host.local.network> + From: User2 + To: Multi Alias + Cc: + Bcc: + Subject: This is a test Email from user2\@example.com to multi-alias + Reply-To: + + Hello Multi Alias, + + how are we doing today? + + XOXO User1 + ''; + }; + }; + }; + + testScript = { nodes, ... }: + '' + startAll; + + $server->waitForUnit("multi-user.target"); + $client->waitForUnit("multi-user.target"); + + # TODO put this blocking into the systemd units? + $server->waitUntilSucceeds("timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ \$? -eq 124 ]"); + + $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"); + + subtest "imap retrieving mail", sub { + # fetchmail returns EXIT_CODE 1 when no new mail + $client->succeed("fetchmail -v || [ \$? -eq 1 ] >&2"); + }; + + subtest "submission port send mail", 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" ]'); + }; + + subtest "imap retrieving mail 2", sub { + $client->execute("rm ~/mail/*"); + # fetchmail returns EXIT_CODE 0 when it retrieves mail + $client->succeed("fetchmail -v >&2"); + }; + + subtest "remove sensitive information on submission port", sub { + $client->succeed("cat ~/mail/* >&2"); + ## make sure our IP is _not_ in the email header + $client->fail("grep-ip ~/mail/*"); + $client->succeed("check-mail-id ~/mail/*"); + }; + + subtest "have correct fqdn as sender", sub { + $client->succeed("grep 'Received: from mail.example.com' ~/mail/*"); + }; + + subtest "dkim has user-specified size", sub { + $server->succeed("openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit)'"); + }; + + subtest "dkim singing, multiple domains", sub { + $client->execute("rm ~/mail/*"); + # send email from user2 to user1 + $client->succeed("msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email2 >&2"); + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + # fetchmail returns EXIT_CODE 0 when it retrieves mail + $client->succeed("fetchmail -v"); + $client->succeed("cat ~/mail/* >&2"); + # make sure it is dkim signed + $client->succeed("grep DKIM ~/mail/*"); + }; + + subtest "aliases", sub { + $client->execute("rm ~/mail/*"); + # send email from chuck to postmaster + $client->succeed("msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster\@example.com < /etc/root/email2 >&2"); + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + # fetchmail returns EXIT_CODE 0 when it retrieves mail + $client->succeed("fetchmail -v"); + }; + + subtest "catchAlls", sub { + $client->execute("rm ~/mail/*"); + # send email from chuck to non exsitent account + $client->succeed("msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol\@example.com < /etc/root/email2 >&2"); + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + # fetchmail returns EXIT_CODE 0 when it retrieves mail + $client->succeed("fetchmail -v"); + + $client->execute("rm ~/mail/*"); + # send email from user1 to chuck + $client->succeed("msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck\@example.com < /etc/root/email2 >&2"); + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + # fetchmail returns EXIT_CODE 1 when no new mail + # if this succeeds, it means that user1 recieved the mail that was intended for chuck. + $client->fail("fetchmail -v"); + }; + + subtest "extraVirtualAliases", sub { + $client->execute("rm ~/mail/*"); + # send email from single-alias to user1 + $client->succeed("msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email4 >&2"); + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + # fetchmail returns EXIT_CODE 0 when it retrieves mail + $client->succeed("fetchmail -v"); + + $client->execute("rm ~/mail/*"); + # send email from user1 to multi-alias (user{1,2}@example.com) + $client->succeed("msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias\@example.com < /etc/root/email5 >&2"); + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + # fetchmail returns EXIT_CODE 0 when it retrieves mail + $client->succeed("fetchmail -v"); + }; + + subtest "quota", sub { + $client->execute("rm ~/mail/*"); + $client->execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc"); + + $client->succeed("msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota\@example.com < /etc/root/email2 >&2"); + $server->waitUntilFails('[ "$(postqueue -p)" != "Mail queue is empty" ]'); + # fetchmail returns EXIT_CODE 0 when it retrieves mail + $client->fail("fetchmail -v"); + + }; + + 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"); + $server->fail("journalctl -u dovecot2 | grep -i error >&2"); + $server->fail("journalctl -u dovecot2 | grep -i warning >&2"); + }; + + ''; +} diff --git a/tests/intern.nix b/tests/intern.nix new file mode 100644 index 0000000..5abf379 --- /dev/null +++ b/tests/intern.nix @@ -0,0 +1,53 @@ +# 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 { + + machine = + { config, pkgs, ... }: + { + imports = [ + ./../default.nix + ./lib/config.nix + ]; + + mailserver = { + enable = true; + fqdn = "mail.example.com"; + domains = [ "example.com" ]; + + loginAccounts = { + "user1@example.com" = { + hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/"; + }; + }; + + vmailGroupName = "vmail"; + vmailUID = 5000; + }; + }; + + testScript = + '' + $machine->start; + $machine->waitForUnit("multi-user.target"); + + subtest "vmail gid is set correctly", sub { + $machine->succeed("getent group vmail | grep 5000"); + }; + + ''; +} diff --git a/tests/lib/config.nix b/tests/lib/config.nix new file mode 100644 index 0000000..b247c66 --- /dev/null +++ b/tests/lib/config.nix @@ -0,0 +1,11 @@ +{ + security.dhparams.defaultBitSize = 16; # really low for quicker tests + + # For slow non-kvm tests. + # nixos/modules/testing/test-instrumentation.nix also sets this. I don't know if there's a better way than etc to override theirs. + environment.etc."systemd/system.conf.d/bigdefaulttimeout.conf".text = '' + [Manager] + # Allow extremely slow start (default for test-VMs is 5 minutes) + DefaultTimeoutStartSec=15min + ''; +} diff --git a/tests/lib/pkgs.nokvm.nix b/tests/lib/pkgs.nokvm.nix new file mode 100644 index 0000000..fa13fde --- /dev/null +++ b/tests/lib/pkgs.nokvm.nix @@ -0,0 +1,31 @@ +let + pkgs = (import { system = builtins.currentSystem; config = {}; }); + patchedMachinePM = pkgs.writeTextFile { + name = "Machine.pm.patched-to-wait-longer-for-vm"; + text = builtins.replaceStrings ["alarm 600;"] ["alarm 1200;"] (builtins.readFile (+"/nixos/lib/test-driver/Machine.pm")); + }; +in +(pkgs // { + qemu_test = with pkgs; stdenv.mkDerivation { + name = "qemu_test_no_kvm"; + buildInputs = [ coreutils qemu_test ]; + inherit qemu_test; + inherit coreutils; + builder = builtins.toFile "builder.sh" '' + PATH=$coreutils/bin:$PATH + mkdir -p $out/bin + cp $qemu_test/bin/* $out/bin/ + ln -sf $out/bin/qemu-system-${stdenv.hostPlatform.qemuArch} $out/bin/qemu-kvm + ''; + }; + stdenv = pkgs.stdenv // { + mkDerivation = args: (pkgs.stdenv.mkDerivation (args // ( + pkgs.lib.optionalAttrs (args.name == "nixos-test-driver") { + installPhase = args.installPhase + '' + rm $libDir/Machine.pm + cp ${patchedMachinePM} $libDir/Machine.pm + ''; + } + ))); + }; +}) diff --git a/tests/minimal.nix b/tests/minimal.nix new file mode 100644 index 0000000..7327f55 --- /dev/null +++ b/tests/minimal.nix @@ -0,0 +1,31 @@ +# 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 { + + machine = + { config, pkgs, ... }: + { + imports = [ + ./../default.nix + ]; + }; + + testScript = + '' + $machine->waitForUnit("multi-user.target"); + ''; +} diff --git a/update.sh b/update.sh new file mode 100755 index 0000000..ff38a9c --- /dev/null +++ b/update.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +sed -i -e "s/v[0-9]\+\.[0-9]\+\.[0-9]\+/$1/g" README.md