From 62ea8a7e00b6e8c37e29b3ecc3b9e9744e8ebfba Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Mon, 2 Jun 2025 04:30:45 +0200 Subject: [PATCH 1/3] dovecot: migrate to dedicated homedir and separate maildir paths Per the dovecot documentation[0] we were previously running with an unsupported home directory configuration, because we shared them among all virtual users at /var/vmail. After resolving this by creating per user home directories at /var/vmail/%{domain}/%{user} this now also overlaps with the location of the Maildir, which is not recommended. As a result we now need to migrate our Maildirs into /var/vmail/%{domain}/%{user}/mail, for which a small shell script is provided as part of this change. The script is included in the documentation because we cannot provide it in time for users, because they might already be seeing the relevant assertion and there is no safe waiting period that would allow us to skip shipping it like that. [0] https://doc.dovecot.org/2.3/configuration_manual/mail_location/ --- docs/migrations.rst | 69 ++++++++++ docs/setup-guide.rst | 2 +- mail-server/assertions.nix | 10 ++ mail-server/dovecot.nix | 7 +- migrations/nixos-mailserver-migration-03.py | 132 ++++++++++++++++++++ 5 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 migrations/nixos-mailserver-migration-03.py diff --git a/docs/migrations.rst b/docs/migrations.rst index daef17e..6c186b0 100644 --- a/docs/migrations.rst +++ b/docs/migrations.rst @@ -13,6 +13,75 @@ to your setup. NixOS 25.11 ----------- +#3 Dovecot mail directory migration +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The way the Dovecot home directory for login accounts were previously set up +resulted in shared home directories for all those users. This is not a +supported Dovecot configuration. + +To resolve this we migrated the home directory into the individual +`domain/localpart` subdirectory below the `mailserver.mailDirectory`. + +But since this now overlaps with the location of the Maildir, it must be +migrated into the `mail/` directory below the home directory. +And while the LDAP home directory is not affected we use this migration to +keep the Maildir configurations of LDAP users in sync with those of local +accounts. + +This is a big step forward, since we can now more cleanly colocate other +data directories, like sieve in the home directory, which in turn simplifies +backups. + +This migration is required for every configuration. + +For remediating this issue the following steps are required: + +1. Copy the `migration script `_ script to your mailserver + and make it executable: + + .. code-block:: bash + + wget https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/raw/master/migrations/nixos-mailserver-migration-03.py + chmod +x nixos-mailserver-migration-03.py + +2. Stop the ``dovecot2.service``. + + .. code-block:: bash + + systemctl stop dovecot2.service + +3. Create a backup or snapshot of your ``mailserver.mailDirectory``, so you can restore + should anything go wrong. + +4. Run the migration script under your virtual mail user with the following arguments: + + - ``--layout default`` unless ``useFSLayout`` is enabled, then ``--layout folder`` + - The value of ``mailserver.mailDirectory``, which defaults to ``/var/vmail`` + + The script will not modify your data unless called with ``--execute``. + + Example: + + .. code-block:: bash + + sudo -u virtualMail ./nixos-mailserver-migration-03.py --layout default /var/vmail + +5. Review the commands. They should be + + - create a ``mail`` directory for each accounnt, + - move maildir contents from the parent directory into it, + - suggest removal of files that do not belong to the maildir + + - their removal is not mandatory and the script **will not** remove them when called with ``--execute`` + - review these items carefully if you want to remove them yourself + + - remove obsolete files from the old home directory location + +6. Rerun the command with ``--execute`` or run the commands manually. + +7. Update the ``mailserver.stateVersion`` to ``3``. + #2 Dovecot LDAP home directory migration ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/setup-guide.rst b/docs/setup-guide.rst index 6c07a1e..4312373 100644 --- a/docs/setup-guide.rst +++ b/docs/setup-guide.rst @@ -72,7 +72,7 @@ common ones. mailserver = { enable = true; - stateVersion = 2; + stateVersion = 3; fqdn = "mail.example.com"; domains = [ "example.com" ]; diff --git a/mail-server/assertions.nix b/mail-server/assertions.nix index 8e8ce05..929f592 100644 --- a/mail-server/assertions.nix +++ b/mail-server/assertions.nix @@ -38,6 +38,16 @@ ''; } ] + ++ [ + { + assertion = config.mailserver.stateVersion >= 3; + message = '' + Issue: The dovecot mail location for all users has changed and need to be migrated. + + Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration for the required remediation steps. + ''; + } + ] ++ lib.optionals (config.mailserver.certificateScheme != "acme") [ { assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index da9f569..a487be9 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -45,9 +45,10 @@ let maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8"; - # maildir in format "/${domain}/${user}" + # https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory + # Mail directory below the home directory dovecotMaildir = - "maildir:${cfg.mailDirectory}/%{domain}/%{username}${maildirLayoutAppendix}${maildirUTF8FolderNames}" + "maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}" + (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}"); postfixCfg = config.services.postfix; @@ -386,7 +387,7 @@ in userdb { driver = passwd-file args = ${userdbFile} - default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} + default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}/%{domain}/%{username} } ${lib.optionalString cfg.ldap.enable '' diff --git a/migrations/nixos-mailserver-migration-03.py b/migrations/nixos-mailserver-migration-03.py new file mode 100644 index 0000000..3bf782a --- /dev/null +++ b/migrations/nixos-mailserver-migration-03.py @@ -0,0 +1,132 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i python3 -p python3 + +import argparse +import os +import shutil +import sys +from enum import Enum +from pathlib import Path +from pwd import getpwnam + + +class FolderLayout(Enum): + Default = 1 + Folder = 2 + + +def check_user(vmail_root: Path): + owner = vmail_root.owner() + owner_uid = getpwnam(owner).pw_uid + + if os.geteuid() == owner_uid: + return + + try: + print( + f"Trying to switch effective user id to {owner_uid} ({owner})", + file=sys.stderr, + ) + os.seteuid(owner_uid) + return + except PermissionError: + print( + f"Failed switching to virtual mail user. Please run this script under it, for example by using `sudo -u {owner}`)", + file=sys.stderr, + ) + sys.exit(1) + + +def is_maildir_related(path: Path, layout: FolderLayout) -> bool: + if not path.is_dir(): + return False + if path.name in ["cur", "new", "tmp"]: + return True + if layout is FolderLayout.Default and path.name.startswith("."): + return True + if layout is FolderLayout.Folder: + return True + + return False + + +def mkdir(dst: Path, dry_run: bool = True): + print(f'mkdir "{dst}"') + if not dry_run: + # u+rwx, setgid + dst.mkdir(mode=0o2700) + + +def move(src: Path, dst: Path, dry_run: bool = True): + print(f'mv "{src}" "{dst}"') + if not dry_run: + src.rename(dst) + + +def delete(dst: Path, dry_run: bool = True): + if not dst.exists(): + return + + if dst.is_dir(): + print(f'rm --recursive "{dst}"') + if not dry_run: + shutil.rmtree(dst) + else: + print(f'rm "{dst}"') + if not dry_run: + dst.unlink() + + +def main(vmail_root: Path, layout: FolderLayout, dry_run: bool = True): + maildirs = {path.parent for path in vmail_root.glob("*/*/cur")} + maybe_delete = [] + + # The old maildir will be the new home directory + for homedir in maildirs: + maildir = homedir / "mail" + mkdir(maildir, dry_run) + + for path in homedir.iterdir(): + if is_maildir_related(path, layout): + move(path, maildir / path.name, dry_run) + else: + maybe_delete.append(path) + + # Files that are part of the previous home directory, but now obsolete + for path in [ + vmail_root / ".dovecot.lda-dupes", + vmail_root / ".dovecot.lda-dupes.locks", + ]: + delete(path, dry_run) + + # The remaining files are likely obsolete, but should still be checked with care + for path in maybe_delete: + print(f"# rm {str(path)}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description=""" + NixOS Mailserver Migration #3: Dovecot mail directory migration + (https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration) + """ + ) + parser.add_argument( + "vmail_root", type=Path, help="Path to the `mailserver.mailDirectory`" + ) + parser.add_argument( + "--layout", + choices=["default", "folder"], + required=True, + help="Folder layout: 'default' unless `mailserver.useFsLayout` was enabled, then'folder'", + ) + parser.add_argument( + "--execute", action="store_true", help="Actually perform changes" + ) + + args = parser.parse_args() + + layout = FolderLayout.Default if args.layout == "default" else FolderLayout.Folder + + check_user(args.vmail_root) + main(args.vmail_root, layout, not args.execute) From f25495cabfe2041811f1146de55560c626c2423a Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 20 Jun 2025 03:24:28 +0200 Subject: [PATCH 2/3] dovecot: fix custom index dir configuration for ldap users --- mail-server/dovecot.nix | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index a487be9..cdd46e7 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -387,7 +387,10 @@ in userdb { driver = passwd-file args = ${userdbFile} - default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}/%{domain}/%{username} + default_fields = \ + home=${cfg.mailDirectory}/%{domain}/%{username} \ + uid=${builtins.toString cfg.vmailUID} \ + gid=${builtins.toString cfg.vmailUID} } ${lib.optionalString cfg.ldap.enable '' @@ -399,7 +402,14 @@ in userdb { driver = ldap args = ${ldapConfFile} - default_fields = home=${cfg.mailDirectory}/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID} + default_fields = \ + home=${cfg.mailDirectory}/ldap/%{user} \ + uid=${toString cfg.vmailUID} \ + gid=${toString cfg.vmailUID} \ + mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${ + lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{user}" + } + } ''} From 3c1cff431ca97a97ead1a81ab154967081c82206 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Fri, 20 Jun 2025 04:56:09 +0200 Subject: [PATCH 3/3] tests: test for the expected maildir and index dir locations These are not ideal yet, but we should make them a fixture, so that we are always aware what they are for the different supported setups. --- tests/internal.nix | 213 ++++++++++++++++++++++++--------------------- tests/ldap.nix | 194 +++++++++++++++++++++-------------------- 2 files changed, 214 insertions(+), 193 deletions(-) diff --git a/tests/internal.nix b/tests/internal.nix index af552c3..93441b0 100644 --- a/tests/internal.nix +++ b/tests/internal.nix @@ -100,117 +100,128 @@ in vmailGroupName = "vmail"; vmailUID = 5000; + indexDir = "/var/lib/dovecot/indices"; enableImap = false; }; }; }; - testScript = '' - machine.start() - machine.wait_for_unit("multi-user.target") + testScript = + { + nodes, + ... + }: + '' + machine.start() + machine.wait_for_unit("multi-user.target") - # Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205 - with subtest("mail forwarded can are locally kept"): - # A mail sent to user2@example.com is in the user1@example.com mailbox - machine.succeed( - " ".join( - [ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--imap-host localhost", - "--imap-username user1@example.com", - "--from-addr user1@example.com", - "--to-addr user2@example.com", - "--src-password-file ${passwordFile}", - "--dst-password-file ${passwordFile}", - "--ignore-dkim-spf", - ] - ) - ) - # A mail sent to user2@example.com is in the user2@example.com mailbox - machine.succeed( - " ".join( - [ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--imap-host localhost", - "--imap-username user2@example.com", - "--from-addr user1@example.com", - "--to-addr user2@example.com", - "--src-password-file ${passwordFile}", - "--dst-password-file ${passwordFile}", - "--ignore-dkim-spf", - ] - ) - ) + # Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205 + with subtest("mail forwarded can are locally kept"): + # A mail sent to user2@example.com is in the user1@example.com mailbox + machine.succeed( + " ".join( + [ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--imap-host localhost", + "--imap-username user1@example.com", + "--from-addr user1@example.com", + "--to-addr user2@example.com", + "--src-password-file ${passwordFile}", + "--dst-password-file ${passwordFile}", + "--ignore-dkim-spf", + ] + ) + ) + # A mail sent to user2@example.com is in the user2@example.com mailbox + machine.succeed( + " ".join( + [ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--imap-host localhost", + "--imap-username user2@example.com", + "--from-addr user1@example.com", + "--to-addr user2@example.com", + "--src-password-file ${passwordFile}", + "--dst-password-file ${passwordFile}", + "--ignore-dkim-spf", + ] + ) + ) - with subtest("regex email alias are received"): - # A mail sent to user2-regex-alias@domain.com is in the user2@example.com mailbox - machine.succeed( - " ".join( - [ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--imap-host localhost", - "--imap-username user2@example.com", - "--from-addr user1@example.com", - "--to-addr user2-regex-alias@domain.com", - "--src-password-file ${passwordFile}", - "--dst-password-file ${passwordFile}", - "--ignore-dkim-spf", - ] - ) - ) + with subtest("regex email alias are received"): + # A mail sent to user2-regex-alias@domain.com is in the user2@example.com mailbox + machine.succeed( + " ".join( + [ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--imap-host localhost", + "--imap-username user2@example.com", + "--from-addr user1@example.com", + "--to-addr user2-regex-alias@domain.com", + "--src-password-file ${passwordFile}", + "--dst-password-file ${passwordFile}", + "--ignore-dkim-spf", + ] + ) + ) - with subtest("user can send from regex email alias"): - # A mail sent from user2-regex-alias@domain.com, using user2@example.com credentials is received - machine.succeed( - " ".join( - [ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--imap-host localhost", - "--smtp-username user2@example.com", - "--from-addr user2-regex-alias@domain.com", - "--to-addr user1@example.com", - "--src-password-file ${passwordFile}", - "--dst-password-file ${passwordFile}", - "--ignore-dkim-spf", - ] - ) - ) + with subtest("user can send from regex email alias"): + # A mail sent from user2-regex-alias@domain.com, using user2@example.com credentials is received + machine.succeed( + " ".join( + [ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--imap-host localhost", + "--smtp-username user2@example.com", + "--from-addr user2-regex-alias@domain.com", + "--to-addr user1@example.com", + "--src-password-file ${passwordFile}", + "--dst-password-file ${passwordFile}", + "--ignore-dkim-spf", + ] + ) + ) - with subtest("vmail gid is set correctly"): - machine.succeed("getent group vmail | grep 5000") + with subtest("vmail gid is set correctly"): + machine.succeed("getent group vmail | grep 5000") - with subtest("mail to send only accounts is rejected"): - machine.wait_for_open_port(25) - # TODO put this blocking into the systemd units - machine.wait_until_succeeds( - "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" - ) - machine.succeed( - "cat ${sendMail} | nc localhost 25 | grep -q '554 5.5.0 Error'" - ) + with subtest("Check dovecot maildir and index locations"): + # If these paths change we need a migration + machine.succeed("doveadm user -f home user1@example.com | grep ${nodes.machine.config.mailserver.mailDirectory}/example.com/user1") + machine.succeed("doveadm user -f mail user1@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.config.mailserver.indexDir}/example.com/user1'") - with subtest("rspamd controller serves web ui"): - machine.succeed( - "set +o pipefail; curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q ''" - ) + with subtest("mail to send only accounts is rejected"): + machine.wait_for_open_port(25) + # TODO put this blocking into the systemd units + machine.wait_until_succeeds( + "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" + ) + machine.succeed( + "cat ${sendMail} | nc localhost 25 | grep -q '554 5.5.0 Error'" + ) - with subtest("imap port 143 is closed and imaps is serving SSL"): - machine.wait_for_closed_port(143) - machine.wait_for_open_port(993) - machine.succeed( - "echo | openssl s_client -connect localhost:993 | grep 'New, TLS'" - ) - ''; + with subtest("rspamd controller serves web ui"): + machine.succeed( + "set +o pipefail; curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q ''" + ) + + with subtest("imap port 143 is closed and imaps is serving SSL"): + machine.wait_for_closed_port(143) + machine.wait_for_open_port(993) + machine.succeed( + "echo | openssl s_client -connect localhost:993 | grep 'New, TLS'" + ) + ''; } diff --git a/tests/ldap.nix b/tests/ldap.nix index 1c92572..0948357 100644 --- a/tests/ldap.nix +++ b/tests/ldap.nix @@ -90,6 +90,7 @@ in fqdn = "mail.example.com"; domains = [ "example.com" ]; localDnsResolver = false; + indexDir = "/var/lib/dovecot/indices"; ldap = { enable = true; @@ -115,107 +116,116 @@ in }; }; }; - testScript = '' - import sys - import re + testScript = + { + nodes, + ... + }: + '' + import sys + import re - machine.start() - machine.wait_for_unit("multi-user.target") + machine.start() + machine.wait_for_unit("multi-user.target") - # This function retrieves the ldap table file from a postconf - # command. - # A key lookup is achived and the returned value is compared - # to the expected value. - def test_lookup(postconf_cmdline, key, expected): - conf = machine.succeed(postconf_cmdline).rstrip() - ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1) - value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip() - try: - assert value == expected - except AssertionError: - print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr) - raise + # This function retrieves the ldap table file from a postconf + # command. + # A key lookup is achived and the returned value is compared + # to the expected value. + def test_lookup(postconf_cmdline, key, expected): + conf = machine.succeed(postconf_cmdline).rstrip() + ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1) + value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip() + try: + assert value == expected + except AssertionError: + print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr) + raise - with subtest("Test postmap lookups"): - test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com") - test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com") + with subtest("Test postmap lookups"): + test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com") + test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com") - test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com") - test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com") + test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com") + test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com") - with subtest("Test doveadm lookups"): - machine.succeed("doveadm user -u alice@example.com") - machine.succeed("doveadm user -u bob@example.com") + with subtest("Test doveadm lookups"): + machine.succeed("doveadm user -u alice@example.com") + machine.succeed("doveadm user -u bob@example.com") - with subtest("Files containing secrets are only readable by root"): - machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'") - machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'") + with subtest("Files containing secrets are only readable by root"): + machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'") + machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'") - with subtest("Test account/mail address binding"): - machine.fail(" ".join([ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--smtp-username alice@example.com", - "--imap-host localhost", - "--imap-username bob@example.com", - "--from-addr bob@example.com", - "--to-addr aliceb@example.com", - "--src-password-file <(echo '${alicePassword}')", - "--dst-password-file <(echo '${bobPassword}')", - "--ignore-dkim-spf" - ])) - machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'") + with subtest("Test account/mail address binding"): + machine.fail(" ".join([ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--smtp-username alice@example.com", + "--imap-host localhost", + "--imap-username bob@example.com", + "--from-addr bob@example.com", + "--to-addr aliceb@example.com", + "--src-password-file <(echo '${alicePassword}')", + "--dst-password-file <(echo '${bobPassword}')", + "--ignore-dkim-spf" + ])) + machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'") - with subtest("Test mail delivery"): - machine.succeed(" ".join([ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--smtp-username alice@example.com", - "--imap-host localhost", - "--imap-username bob@example.com", - "--from-addr alice@example.com", - "--to-addr bob@example.com", - "--src-password-file <(echo '${alicePassword}')", - "--dst-password-file <(echo '${bobPassword}')", - "--ignore-dkim-spf" - ])) + with subtest("Test mail delivery"): + machine.succeed(" ".join([ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--smtp-username alice@example.com", + "--imap-host localhost", + "--imap-username bob@example.com", + "--from-addr alice@example.com", + "--to-addr bob@example.com", + "--src-password-file <(echo '${alicePassword}')", + "--dst-password-file <(echo '${bobPassword}')", + "--ignore-dkim-spf" + ])) - with subtest("Test mail forwarding works"): - machine.succeed(" ".join([ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--smtp-username alice@example.com", - "--imap-host localhost", - "--imap-username bob@example.com", - "--from-addr alice@example.com", - "--to-addr bob_fw@example.com", - "--src-password-file <(echo '${alicePassword}')", - "--dst-password-file <(echo '${bobPassword}')", - "--ignore-dkim-spf" - ])) + with subtest("Test mail forwarding works"): + machine.succeed(" ".join([ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--smtp-username alice@example.com", + "--imap-host localhost", + "--imap-username bob@example.com", + "--from-addr alice@example.com", + "--to-addr bob_fw@example.com", + "--src-password-file <(echo '${alicePassword}')", + "--dst-password-file <(echo '${bobPassword}')", + "--ignore-dkim-spf" + ])) - with subtest("Test cannot send mail from forwarded address"): - machine.fail(" ".join([ - "mail-check send-and-read", - "--smtp-port 587", - "--smtp-starttls", - "--smtp-host localhost", - "--smtp-username bob@example.com", - "--imap-host localhost", - "--imap-username alice@example.com", - "--from-addr bob_fw@example.com", - "--to-addr alice@example.com", - "--src-password-file <(echo '${bobPassword}')", - "--dst-password-file <(echo '${alicePassword}')", - "--ignore-dkim-spf" - ])) - machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'") + with subtest("Test cannot send mail from forwarded address"): + machine.fail(" ".join([ + "mail-check send-and-read", + "--smtp-port 587", + "--smtp-starttls", + "--smtp-host localhost", + "--smtp-username bob@example.com", + "--imap-host localhost", + "--imap-username alice@example.com", + "--from-addr bob_fw@example.com", + "--to-addr alice@example.com", + "--src-password-file <(echo '${bobPassword}')", + "--dst-password-file <(echo '${alicePassword}')", + "--ignore-dkim-spf" + ])) + machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'") - ''; + with subtest("Check dovecot mail and index locations"): + # If these paths change we need a migration + machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.config.mailserver.mailDirectory}/ldap/bob@example.com") + machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.config.mailserver.indexDir}/ldap/bob@example.com'") + ''; }