Merge branch 'cleanup' into 'master'

Rename loginAccounts and group storage related settings

See merge request simple-nixos-mailserver/nixos-mailserver!501
This commit is contained in:
Martin Weinelt
2026-03-24 22:56:11 +00:00
21 changed files with 250 additions and 198 deletions
+151 -83
View File
@@ -25,7 +25,7 @@ let
inherit (lib)
literalExpression
literalMD
mkDefault
mkChangedOptionModule
mkEnableOption
mkOption
mkOptionType
@@ -137,7 +137,7 @@ in
description = "Message size limit enforced by Postfix.";
};
loginAccounts = mkOption {
accounts = mkOption {
type = types.attrsOf (
types.submodule (
{ name, ... }:
@@ -145,8 +145,13 @@ in
options = {
name = mkOption {
type = types.str;
default = name;
example = "user1@example.com";
description = "Username";
readOnly = true;
internal = true;
description = ''
The login username for this account.
'';
};
hashedPassword = mkOption {
@@ -154,24 +159,33 @@ in
default = null;
example = "$y$j9T$vfGrwkAaXCjCEWtVNMQck1$383uIXQmn2z0hnmVAA8kwFQmjNj78.nYbvWeyNLIaP1";
description = ''
The user's hashed password. Use `mkpasswd` as follows
The hashed login password for this account.
Use `mkpasswd` to create password hashes:
```
nix-shell -p mkpasswd --run 'mkpasswd -s'
```
Warning: this is stored in plaintext in the Nix store!
Use {option}`mailserver.loginAccounts.<name>.hashedPasswordFile` instead.
:::{note}
This is a convenience option, when your threat model allows
storing hashed secrets in the world-readable Nix store.
Passing the hash through
{option}`mailserver.accounts.<name>.hashedPasswordFile`
allows relying on filesystem discretionary access control as
another security boundary.
:::
'';
};
hashedPasswordFile = mkOption {
type = with types; nullOr path;
default = null;
example = "/run/keys/user1-passwordhash";
example = "/run/keys/user1-pw-hash";
description = ''
A file containing the user's hashed password. Use `mkpasswd` as follows
The hashed login password for this account read from a file.
Use `mkpasswd to create password hashes:
```
nix-shell -p mkpasswd --run 'mkpasswd -s'
```
@@ -185,9 +199,13 @@ in
inStore = false;
});
default = null;
example = "/run/keys/user1-password";
example = "/run/keys/user1-pw";
description = ''
A file containing the user's plain text password. The value will be hashed at runtime.
The plaintext login password for this account read from a file.
:::{note}
The password is hashed before it is passed on to Dovecot.
:::
'';
};
@@ -199,9 +217,13 @@ in
];
default = [ ];
description = ''
A list of aliases of this login account.
Note: Use list entries like "@example.com" to create a catchAll
that allows sending from all email addresses in these domain.
List of additional mail addresses (aliases) that get routed to this account.
:::{admonition} Catch-all with sending permissions
:class: tip
Configure `@example.com` to create a catch-all for this domain
that also allows sending from all addresses.
:::
'';
};
@@ -210,7 +232,7 @@ in
example = [ ''/^tom\..*@domain\.com$/'' ];
default = [ ];
description = ''
Same as {option}`mailserver.loginAccounts.<name>.aliases` but
Same as {option}`mailserver.accounts.<name>.aliases` but
using PCRE (Perl compatible regex).
'';
};
@@ -224,7 +246,12 @@ in
default = [ ];
description = ''
For which domains should this account act as a catch all?
Note: Does not allow sending from all addresses of these domains.
:::{warning}
Does not allow sending from all addresses of these domains.
Use {option}`mailserver.accounts.<name>.aliases` if that
is required.
:::
'';
};
@@ -266,9 +293,10 @@ in
default = false;
description = ''
Specifies if the account should be a send-only account.
Emails sent to send-only accounts will be rejected from
unauthorized senders with the `sendOnlyRejectMessage`
stating the reason.
Emails sent to send-only accounts will
be rejected with the reason configured in
{option}`mailserver.accounts.<name>.sendOnlyRejectMessage`.
'';
};
@@ -276,33 +304,38 @@ in
type = types.str;
default = "This account cannot receive emails.";
description = ''
The message that will be returned to the sender when an email is
sent to a send-only account. Only used if the account is marked
as send-only.
The message returned to the sender for a send-only account.
See {option}`mailserver.accounts.<name>.sendOnly`.
'';
};
};
config.name = mkDefault name;
}
)
);
example = {
user1 = {
hashedPassword = "$y$j9T$y6eZ1o.IvVNfdGMAsUEvh1$6K/llP52uw2iDh4iSwtAn54/JYy7FzCcoCHmjmx00H5";
};
user2 = {
hashedPassword = "$y$j9T$hZ.ubq0M897Hw.znxnGG9.$14EJBoOwbwKeWt.W4vpnBPEBZC9mYz4fWI9kOCLoZf4";
};
};
example = lib.literalExpression ''
{
user1 = {
# This password hash leaks into the Nix store
hashedPassword = "$y$j9T$y6eZ1o.IvVNfdGMAsUEvh1$6K/llP52uw2iDh4iSwtAn54/JYy7FzCcoCHmjmx00H5";
};
user2 = {
# Hashed password passed as a file
hashedPasswordFile = "/run/keys/user2-pw-hash";
};
user3 = {
# Plaintext password file
passwordFile = "/run/keys/user3-pw";
};
}
'';
description = ''
The login account of the domain. Every account is mapped to a unix user,
e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as
follows
Attribute set of mail accounts.
```
nix-shell -p mkpasswd --run 'mkpasswd -s'
```
Each entry defines a mailbox and login credentials, where the attribute
name is used as the login username and optionally routed mail address.
Use `mkpasswd` to generate password hashes.
'';
default = { };
};
@@ -652,13 +685,13 @@ in
aliases = mkOption {
type =
let
loginAccount = mkOptionType {
account = mkOptionType {
name = "Login Account";
check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts);
check = account: builtins.elem account (builtins.attrNames cfg.accounts);
};
in
with types;
attrsOf (either loginAccount (nonEmptyListOf loginAccount));
attrsOf (either account (nonEmptyListOf account));
example = {
"postmaster@example.com" = "user1@example.com";
"abuse@example.com" = "user1@example.com";
@@ -751,53 +784,80 @@ in
default = [ ];
};
vmailUID = mkOption {
type = types.int;
default = 5000;
description = ''
The unix UID of the virtual mail user. Be mindful that if this is
changed, you will need to manually adjust the permissions of
`mailDirectory`.
'';
};
storage = {
path = mkOption {
type = types.path;
default = "/var/vmail";
description = ''
Path on disk where mail home directories are stored.
'';
};
vmailUserName = mkOption {
type = types.str;
default = "virtualMail";
description = ''
The user name and group name of the user that owns the directory where all
the mail is stored.
'';
};
directoryLayout = mkOption {
type = types.enum [
"fs"
"maildir++"
];
default = "maildir++";
description = ''
Sets whether dovecot should organize mail in subdirectories:
vmailGroupName = mkOption {
type = types.str;
default = "virtualMail";
description = ''
The user name and group name of the user that owns the directory where all
the mail is stored.
'';
};
- /var/vmail/example.com/user/.folder.subfolder/ (Maildir++ layout)
- /var/vmail/example.com/user/folder/subfolder/ (FS layout)
mailDirectory = mkOption {
type = types.path;
default = "/var/vmail";
description = ''
Where to store the mail.
'';
};
See <https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#directory-layout>
See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details.
'';
};
useFsLayout = mkOption {
type = types.bool;
default = false;
description = ''
Sets whether dovecot should organize mail in subdirectories:
uid = mkOption {
type = types.ints.positive;
default = 5000;
description = ''
The user id assigned to the vmail user.
- /var/vmail/example.com/user/.folder.subfolder/ (default layout)
- /var/vmail/example.com/user/folder/subfolder/ (FS layout)
This user owns the mail storage files and directories and is used by
services accessing the mail store.
See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details.
'';
:::{warning}
If you change this value you also need to manually adjust the
permissions of your :option:`mailserver.storage.path`.
:::
'';
};
owner = mkOption {
type = types.str;
default = "virtualMail";
description = ''
The name of the user that owns the :option:`mailserver.storage.path`.
'';
};
gid = mkOption {
type = types.ints.positive;
default = 5000;
description = ''
The group id of the primary group of the vmail user.
This group owns the mail storage directories. Access can be delegated
to other users via group membership.
:::{warning}
If you change this value you also need to manually adjust the
permissions of your :option:`mailserver.storage.path`.
:::
'';
};
group = mkOption {
type = types.str;
default = "virtualMail";
description = ''
The primary group name of the user that owns the
:option:`mailserver.storage.path`.
'';
};
};
useUTF8FolderNames = mkOption {
@@ -1481,8 +1541,8 @@ in
locations = mkOption {
type = types.listOf types.path;
default = [ cfg.mailDirectory ];
defaultText = literalExpression "[ config.mailserver.mailDirectory ]";
default = [ cfg.storage.path ];
defaultText = literalExpression "[ config.mailserver.storage.path ]";
description = "The locations that are to be backed up by borg.";
};
@@ -1682,5 +1742,13 @@ in
[ "mailserver" "ldap" "attributes" "mail" ]
)
(mkRenamedOptionModule [ "mailserver" "extraVirtualAliases" ] [ "mailserver" "aliases" ])
(mkRenamedOptionModule [ "mailserver" "loginAccounts" ] [ "mailserver" "accounts" ])
(mkRenamedOptionModule [ "mailserver" "vmailUID" ] [ "mailserver" "storage" "uid" ])
(mkRenamedOptionModule [ "mailserver" "vmailUserName" ] [ "mailserver" "storage" "owner" ])
(mkRenamedOptionModule [ "mailserver" "vmailGroupName" ] [ "mailserver" "storage" "group" ])
(mkRenamedOptionModule [ "mailserver" "mailDirectory" ] [ "mailserver" "storage" "path" ])
(mkChangedOptionModule [ "mailserver" "useFSLayout" ] [ "mailserver" "storage" "directoryLayout" ] (
config: if config.mailserver.useFSLayout then "fs" else "maildir++"
))
];
}
+7 -8
View File
@@ -5,14 +5,13 @@ First off you should have a backup of your ``configuration.nix`` file
where you have the server config (but that is already in a git
repository right?)
Next you need to backup ``/var/vmail`` or whatever you have specified
for the option ``mailDirectory``. This is where all the mails reside.
Good options are a cron job with ``rsync`` or ``scp``. But really
anything works, as it is simply a folder with plenty of files in it. If
your backup solution does not preserve the owner of the files dont
forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them
back (or whatever you specified as ``vmailUserName``, and
``vmailGroupName``).
Next you need to backup ``/var/vmail`` or whatever you have specified for the
option :option:`mailserver.storage.path`. This is where all the mails reside.
Good options are a cron job with ``rsync`` or ``scp``. But really anything
works, as it is simply a folder with plenty of files in it. If your backup
solution does not preserve the owner of the files dont forget to ``chown`` them
to ``virtualMail:virtualMail`` if you copy them back (or whatever you specified
as :option:`mailserver.storage.owner`, and :option:`mailserver.storage.group`).
If you enabled ``enableManageSieve`` then you also may want to backup
``/var/sieve`` or whatever you have specified as ``sieveDirectory``.
+1 -1
View File
@@ -13,7 +13,7 @@ domain ``example.com`` and send mails with any address of this domain:
.. code:: nix
mailserver.loginAccounts = {
mailserver.accounts = {
"user@example.com" = {
aliases = [ "@example.com" ];
};
+3 -3
View File
@@ -40,18 +40,18 @@ best practices to mailserver management.
systemctl stop dovecot.service
3. Create a backup or snapshot of your :option:`mailserver.mailDirectory`, so
3. Create a backup or snapshot of your :option:`mailserver.storage.path`, so
you can restore should anything go wrong.
4. Run the migration script and pass the required arguments to enable LDAP lookups:
The script should be run under the user who owns the :option:`mailserver.mailDirectory`.
The script should be run under the user who owns the :option:`mailserver.storage.path`.
If run as root it will automatically switch into the appropriate user by itself.
The script will not modify your data unless called with ``--execute``.
The migration script finds all Dovecot home directories in
``/var/vmail/ldap/`` (or any other :option:`mailserver.mailDirectory`),
``/var/vmail/ldap/`` (or any other :option:`mailserver.storage.path`),
for example that of bob at ``/var/vmail/ldap/bob@example.com``.
It then takes ``bob@example.com`` and queries the LDAP server for
``mail=bob@example.com`` to retrieve the UUID attribute. Finally
+1 -1
View File
@@ -12,7 +12,7 @@ let
mapAttrsToList
;
mailAccounts = config.mailserver.loginAccounts;
mailAccounts = config.mailserver.accounts;
htpasswd = pkgs.writeText "radicale.users" (
concatStrings (flip mapAttrsToList mailAccounts (mail: user: "${mail}+:${user.hashedPassword}\n"))
);
+1 -1
View File
@@ -10,7 +10,7 @@ Limitations
Radicale since the 3.x release (introduced in NixOS 20.09) does not support
traditional crypt() password hashes any longer. To establish access for
existing :option:`mailserver.loginAccounts`, the hashing method used
existing :option:`mailserver.accounts`, the hashing method used
for ``hashedPassword`` needs to be compatible with one of the available
`htpasswd_encryption`_ methods. Such hashes can for example be created using
+1 -1
View File
@@ -21,7 +21,7 @@ NixOS 26.05
is an alternative to hashed passwords that integrates well with workflows
established by `agenix`_/`sops-nix`_ that instead rely on encryption. This
option prevents files from leaking in to the Nix store.
See :option:`mailserver.loginAccounts.<name>.passwordFile`.
See :option:`mailserver.accounts.<name>.passwordFile`.
- LDAP setups require a migration of Dovecot home directories to
`UUID based home directories`_. The exact UUID attribute can be customized
through :option:`mailserver.ldap.attributes.uuid`.
+1 -1
View File
@@ -38,7 +38,7 @@
# A list of all login accounts. To create the password hashes, use
# nix-shell -p mkpasswd --run 'mkpasswd -s'
loginAccounts = {
accounts = {
"user1@example.com" = {
# Reads the password hash from a file on the server
hashedPasswordFile = "/a/file/containing/a/hashed/password";
+13 -15
View File
@@ -98,22 +98,20 @@ in
) config.mailserver.dkim.domains
)
)
++
lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail")
[
{
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2;
message = ''
Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`.
Remediation:
- Stop the `dovecot.service`
- Move `/var/vmail/ldap` below your `mailserver.mailDirectory`
- Increase the `stateVersion` to 2.
++ lib.optionals (config.mailserver.ldap.enable && config.mailserver.storage.path != "/var/vmail") [
{
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2;
message = ''
Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.storage.path`.
Remediation:
- Stop the `dovecot.service`
- Move `/var/vmail/ldap` below your `mailserver.storage.path`
- Increase the `stateVersion` to 2.
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-ldap-home-directory-migration for more information.
'';
}
]
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-ldap-home-directory-migration for more information.
'';
}
]
++ [
{
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 3;
+4 -4
View File
@@ -51,12 +51,12 @@ rec {
builtins.toString (mkHashFile name value.hashedPassword)
else
value.passwordFile
) cfg.loginAccounts;
) cfg.accounts;
# Collect accounts with plain text passwords that require hashing
accountsWithPlaintextPasswordFiles = lib.filter (
name: cfg.loginAccounts.${name}.passwordFile != null
) (builtins.attrNames cfg.loginAccounts);
accountsWithPlaintextPasswordFiles = lib.filter (name: cfg.accounts.${name}.passwordFile != null) (
builtins.attrNames cfg.accounts
);
# Appends the LDAP bind password to files to avoid writing this
# password into the Nix store.
+13 -13
View File
@@ -50,7 +50,7 @@ let
}) attrs
);
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
maildirLayoutAppendix = lib.optionalString (cfg.storage.directoryLayout == "fs") ":LAYOUT=fs";
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
# https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory
@@ -79,7 +79,7 @@ let
scope = ${mkLdapSearchScope cfg.ldap.scope}
user_attrs = \
${ldapUuidAttribute}=${ldapUuidAttribute}, \
=home=${cfg.mailDirectory}/ldap/%{ldap:${ldapUuidAttribute}}, \
=home=${cfg.storage.path}/ldap/%{ldap:${ldapUuidAttribute}}, \
=mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${
lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{ldap:${ldapUuidAttribute}}"
}
@@ -115,7 +115,7 @@ let
umask 077
for f in ${
builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts)
builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.accounts)
}; do
if [ ! -f "$f" ]; then
echo "Expected password hash file $f does not exist!"
@@ -131,7 +131,7 @@ let
"${name}:${"$(sed -n '1{p;p;q}' ${passwordFiles."${name}"} | ${lib.getExe' pkgs.dovecot "doveadm"} pw)"}::::::"
else
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts
) cfg.accounts
)}
EOF
@@ -141,7 +141,7 @@ let
name: value:
"${name}:::::::"
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
) cfg.loginAccounts
) cfg.accounts
)}
EOF
'';
@@ -228,8 +228,8 @@ in
enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl;
enablePAM = false;
enableQuota = true;
mailGroup = cfg.vmailGroupName;
mailUser = cfg.vmailUserName;
mailGroup = cfg.storage.group;
mailUser = cfg.storage.owner;
mailLocation = dovecotMaildir;
sslServerCert = x509CertificateFile;
sslServerKey = x509PrivateKeyFile;
@@ -371,7 +371,7 @@ in
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
}
mail_access_groups = ${cfg.vmailGroupName}
mail_access_groups = ${cfg.storage.group}
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7
ssl = required
@@ -431,9 +431,9 @@ in
driver = passwd-file
args = ${userdbFile}
default_fields = \
home=${cfg.mailDirectory}/%{domain}/%{username} \
uid=${builtins.toString cfg.vmailUID} \
gid=${builtins.toString cfg.vmailUID}
home=${cfg.storage.path}/%{domain}/%{username} \
uid=${builtins.toString cfg.storage.uid} \
gid=${builtins.toString cfg.storage.uid}
}
${lib.optionalString cfg.ldap.enable ''
@@ -446,8 +446,8 @@ in
driver = ldap
args = ${ldapConfFile}
override_fields = \
uid=${toString cfg.vmailUID} \
gid=${toString cfg.vmailUID}
uid=${toString cfg.storage.uid} \
gid=${toString cfg.storage.uid}
}
''}
+4 -4
View File
@@ -51,7 +51,7 @@ let
to = name;
in
map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name)
) cfg.loginAccounts
) cfg.accounts
)
);
regex_valiases_postfix = mergeLookupTables (
@@ -62,7 +62,7 @@ let
to = name;
in
map (from: { "${from}" = to; }) value.aliasesRegexp
) cfg.loginAccounts
) cfg.accounts
)
);
@@ -75,7 +75,7 @@ let
to = name;
in
map (from: { "@${from}" = to; }) value.catchAll
) cfg.loginAccounts
) cfg.accounts
)
);
@@ -127,7 +127,7 @@ let
# denied_recipients_postfix :: [ String ]
denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") (
lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)
lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.accounts)
);
denied_recipients_file = builtins.toFile "denied_recipients" (
lib.concatStringsSep "\n" denied_recipients_postfix
+1 -1
View File
@@ -61,7 +61,7 @@ in
retain hourly ${toString cfg.backup.retain.hourly}
retain daily ${toString cfg.backup.retain.daily}
retain weekly ${toString cfg.backup.retain.weekly}
backup ${cfg.mailDirectory}/ localhost/
backup ${cfg.storage.path}/ localhost/
'';
};
};
+2 -2
View File
@@ -46,7 +46,7 @@ in
preStart =
let
directories = lib.strings.escapeShellArgs (
[ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
[ cfg.storage.path ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
);
in
''
@@ -55,7 +55,7 @@ in
# Prevent world-readable paths, even temporarily.
umask 007
mkdir -p ${directories}
chgrp "${cfg.vmailGroupName}" ${directories}
chgrp "${cfg.storage.group}" ${directories}
chmod 02770 ${directories}
'';
};
+30 -47
View File
@@ -16,32 +16,13 @@
{
config,
options,
pkgs,
lib,
...
}:
with (import ./common.nix {
inherit
config
options
lib
pkgs
;
});
with config.mailserver;
let
vmail_user = {
name = vmailUserName;
isSystemUser = true;
uid = vmailUID;
home = mailDirectory;
createHome = true;
group = vmailGroupName;
};
cfg = config.mailserver;
virtualMailUsersActivationScript =
pkgs.writeScript "activate-virtual-mail-users"
@@ -55,10 +36,10 @@ let
umask 007
# Create directory to store user sieve scripts if it doesn't exist
if (! test -d "${sieveDirectory}"); then
mkdir "${sieveDirectory}"
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}"
chmod 770 "${sieveDirectory}"
if (! test -d "${cfg.sieveDirectory}"); then
mkdir "${cfg.sieveDirectory}"
chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}"
chmod 770 "${cfg.sieveDirectory}"
fi
# Copy user's sieve script to the correct location (if it exists). If it
@@ -67,30 +48,30 @@ let
{ name, sieveScript }:
if lib.isString sieveScript then
''
if (! test -d "${sieveDirectory}/${name}"); then
mkdir -p "${sieveDirectory}/${name}"
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
chmod 770 "${sieveDirectory}/${name}"
if (! test -d "${cfg.sieveDirectory}/${name}"); then
mkdir -p "${cfg.sieveDirectory}/${name}"
chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}/${name}"
chmod 770 "${cfg.sieveDirectory}/${name}"
fi
cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
cat << 'EOF' > "${cfg.sieveDirectory}/${name}/default.sieve"
${sieveScript}
EOF
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
chown "${cfg.storage.owner}:${cfg.storage.group}" "${cfg.sieveDirectory}/${name}/default.sieve"
''
else
''
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
rm "${sieveDirectory}/${name}/default.sieve"
if (test -f "${cfg.sieveDirectory}/${name}/default.sieve"); then
rm "${cfg.sieveDirectory}/${name}/default.sieve"
fi
if (test -f "${sieveDirectory}/${name}.svbin"); then
rm "${sieveDirectory}/${name}/default.svbin"
if (test -f "${cfg.sieveDirectory}/${name}.svbin"); then
rm "${cfg.sieveDirectory}/${name}/default.svbin"
fi
''
) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))}
) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues cfg.accounts))}
'';
in
{
config = lib.mkIf enable {
config = lib.mkIf cfg.enable {
# assert that all accounts provide a password
assertions = map (acct: {
assertion =
@@ -102,27 +83,29 @@ in
]
) == 1;
message = "Login account ${acct.name} must provide exactly one of password file, hashed password, or hashed password file";
}) (lib.attrValues loginAccounts);
}) (lib.attrValues cfg.accounts);
# warn for accounts that specify both password and file
warnings =
map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
(
lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) (
lib.attrValues loginAccounts
lib.attrValues cfg.accounts
)
);
# set the vmail gid to a specific value
users.groups = {
"${vmailGroupName}" = {
gid = vmailUID;
};
users.groups.${cfg.storage.group} = {
inherit (cfg.storage) gid;
};
# define all users
users.users = {
"${vmail_user.name}" = lib.mkForce vmail_user;
users.users.${cfg.storage.owner} = lib.mkForce {
inherit (cfg.storage)
group
uid
;
name = cfg.storage.owner;
isSystemUser = true;
home = cfg.storage.path;
createHome = true;
};
systemd.services.activate-virtual-mail-users = {
+2 -1
View File
@@ -25,8 +25,9 @@ f = open(sys.argv[1])
options = json.load(f)
groups = [
"mailserver.loginAccounts",
"mailserver.accounts",
"mailserver.x509",
"mailserver.storage",
"mailserver.dkim",
"mailserver.srs",
"mailserver.dmarcReporting",
+1 -1
View File
@@ -77,7 +77,7 @@
];
virusScanning = true;
loginAccounts = {
accounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [ "postmaster@example.com" ];
+1 -1
View File
@@ -66,7 +66,7 @@
};
dmarcReporting.enable = true;
loginAccounts = {
accounts = {
"user1@example.com" = {
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = [ "postmaster@example.com" ];
+7 -4
View File
@@ -87,7 +87,7 @@ in
];
localDnsResolver = false;
loginAccounts = {
accounts = {
"user1@example.com" = {
hashedPasswordFile = hashedPasswordFile;
};
@@ -109,8 +109,11 @@ in
"user2@example.com" = "user1@example.com";
};
vmailGroupName = "vmail";
vmailUID = 5000;
storage = {
gid = 5000;
group = "vmail";
};
indexDir = "/var/lib/dovecot/indices";
enableImap = false;
@@ -218,7 +221,7 @@ in
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.mailserver.mailDirectory}/example.com/user1")
machine.succeed("doveadm user -f home user1@example.com | grep ${nodes.machine.mailserver.storage.path}/example.com/user1")
machine.succeed("doveadm user -f mail user1@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/example.com/user1'")
with subtest("mail to send only accounts is rejected"):
+5 -5
View File
@@ -125,7 +125,7 @@ in
fqdn = "mail.example.com";
domains = [ "example.com" ];
localDnsResolver = false;
mailDirectory = "/var/lib/dovecot/vmail";
storage.path = "/var/lib/dovecot/vmail";
indexDir = "/var/lib/dovecot/indices";
aliases = {
@@ -133,7 +133,7 @@ in
"frank@example.com" = "mallory@example.com";
};
loginAccounts = {
accounts = {
# Colliding local account takes precedence over LDAP account with
# same address.
"carol@example.com" = {
@@ -214,10 +214,10 @@ in
machine.succeed("doveadm user -u alice")
machine.log(machine.succeed("doveadm user -u bob"))
machine.succeed("doveadm user -f uid bob@example.com | grep ${toString nodes.machine.mailserver.vmailUID}")
machine.succeed("doveadm user -f gid bob@example.com | grep ${toString nodes.machine.mailserver.vmailUID}")
machine.succeed("doveadm user -f uid bob@example.com | grep ${toString nodes.machine.mailserver.storage.uid}")
machine.succeed("doveadm user -f gid bob@example.com | grep ${toString nodes.machine.mailserver.storage.uid}")
machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.mailDirectory}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092")
machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.storage.path}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092")
machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/ldap/f3b4e8ea-087f-42cc-95f0-cbfd99386092'")
with subtest("Files containing secrets are only readable by root"):
+1 -1
View File
@@ -35,7 +35,7 @@ let
fqdn = "mail.${domain}";
domains = [ domain ];
localDnsResolver = false;
loginAccounts = {
accounts = {
"user@${domain}" = {
hashedPasswordFile = hashPassword "password";
};