Compare commits
24 Commits
havefun-23
...
havefun-23
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
863c892223 | ||
|
|
b5023b36a1 | ||
|
|
3f526c08e8 | ||
|
|
008d78cc21 | ||
|
|
84783b661e | ||
|
|
93221e4b25 | ||
|
|
c63f6e7b05 | ||
|
|
a3b03d1b5a | ||
|
|
69a4b7ad67 | ||
|
|
71b4c62d85 | ||
|
|
6775502be3 | ||
|
|
7695c856f1 | ||
|
|
fb3210b932 | ||
|
|
33554e57ce | ||
|
|
8b03ae5701 | ||
|
|
42e245b069 | ||
|
|
08f077c5ca | ||
|
|
d460e9ff62 | ||
|
|
0c1801b489 | ||
|
|
24128c3052 | ||
|
|
c4ec122aac | ||
|
|
131c48de9b | ||
|
|
290d00f6db | ||
|
|
7e09d8f537 |
43
README.md
43
README.md
@@ -71,46 +71,11 @@ can stay up to date with bug fixes and updates.
|
|||||||
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
|
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
|
||||||
- Join the Libera Chat IRC channel `#nixos-mailserver`
|
- Join the Libera Chat IRC channel `#nixos-mailserver`
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{ config, pkgs, ... }:
|
|
||||||
let release = "nixos-21.11";
|
|
||||||
in {
|
|
||||||
imports = [
|
|
||||||
(builtins.fetchTarball {
|
|
||||||
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz";
|
|
||||||
# This hash needs to be updated
|
|
||||||
sha256 = "0000000000000000000000000000000000000000000000000000";
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [ "example.com" "example2.com" ];
|
|
||||||
loginAccounts = {
|
|
||||||
"user1@example.com" = {
|
|
||||||
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' > /hashed/password/file/location
|
|
||||||
hashedPasswordFile = "/hashed/password/file/location";
|
|
||||||
|
|
||||||
aliases = [
|
|
||||||
"info@example.com"
|
|
||||||
"postmaster@example.com"
|
|
||||||
"postmaster@example2.com"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
For a complete list of options, see `default.nix`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## How to Set Up a 10/10 Mail Server Guide
|
## How to Set Up a 10/10 Mail Server Guide
|
||||||
Check out the [Complete Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
|
|
||||||
|
Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
|
||||||
|
|
||||||
|
For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
|||||||
186
default.nix
186
default.nix
@@ -111,6 +111,15 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
aliasesRegexp = mkOption {
|
||||||
|
type = with types; listOf types.str;
|
||||||
|
example = [''/^tom\..*@domain\.com$/''];
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
catchAll = mkOption {
|
catchAll = mkOption {
|
||||||
type = with types; listOf (enum cfg.domains);
|
type = with types; listOf (enum cfg.domains);
|
||||||
example = ["example.com" "example2.com"];
|
example = ["example.com" "example2.com"];
|
||||||
@@ -198,6 +207,157 @@ in
|
|||||||
default = {};
|
default = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ldap = {
|
||||||
|
enable = mkEnableOption "LDAP support";
|
||||||
|
|
||||||
|
uris = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
example = literalExpression ''
|
||||||
|
[
|
||||||
|
"ldaps://ldap1.example.com"
|
||||||
|
"ldaps://ldap2.example.com"
|
||||||
|
]
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
URIs where your LDAP server can be reached
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
startTls = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether to enable StartTLS upon connection to the server.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
tlsCAFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||||
|
defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
|
||||||
|
description = ''
|
||||||
|
Certifificate trust anchors used to verify the LDAP server certificate.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
bind = {
|
||||||
|
dn = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = "cn=mail,ou=accounts,dc=example,dc=com";
|
||||||
|
description = ''
|
||||||
|
Distinguished name used by the mail server to do lookups
|
||||||
|
against the LDAP servers.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = "/run/my-secret";
|
||||||
|
description = ''
|
||||||
|
A file containing the password required to authenticate against the LDAP servers.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
searchBase = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = "ou=people,ou=accounts,dc=example,dc=com";
|
||||||
|
description = ''
|
||||||
|
Base DN at below which to search for users accounts.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
searchScope = mkOption {
|
||||||
|
type = types.enum [ "sub" "base" "one" ];
|
||||||
|
default = "sub";
|
||||||
|
description = ''
|
||||||
|
Search scope below which users accounts are looked for.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
dovecot = {
|
||||||
|
userAttrs = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "";
|
||||||
|
description = ''
|
||||||
|
LDAP attributes to be retrieved during userdb lookups.
|
||||||
|
|
||||||
|
See the users_attrs reference at
|
||||||
|
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-attrs
|
||||||
|
in the Dovecot manual.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
userFilter = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mail=%u";
|
||||||
|
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
|
||||||
|
description = ''
|
||||||
|
Filter for user lookups in Dovecot.
|
||||||
|
|
||||||
|
See the user_filter reference at
|
||||||
|
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-filter
|
||||||
|
in the Dovecot manual.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
passAttrs = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "userPassword=password";
|
||||||
|
description = ''
|
||||||
|
LDAP attributes to be retrieved during passdb lookups.
|
||||||
|
|
||||||
|
See the pass_attrs reference at
|
||||||
|
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-attrs
|
||||||
|
in the Dovecot manual.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
passFilter = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = "mail=%u";
|
||||||
|
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
|
||||||
|
description = ''
|
||||||
|
Filter for password lookups in Dovecot.
|
||||||
|
|
||||||
|
See the pass_filter reference for
|
||||||
|
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-filter
|
||||||
|
in the Dovecot manual.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
postfix = {
|
||||||
|
filter = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mail=%s";
|
||||||
|
example = "(&(objectClass=inetOrgPerson)(mail=%s))";
|
||||||
|
description = ''
|
||||||
|
LDAP filter used to search for an account by mail, where
|
||||||
|
`%s` is a substitute for the address in
|
||||||
|
question.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
uidAttribute = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mail";
|
||||||
|
example = "uid";
|
||||||
|
description = ''
|
||||||
|
The LDAP attribute referencing the account name for a user.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
mailAttribute = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mail";
|
||||||
|
description = ''
|
||||||
|
The LDAP attribute holding mail addresses for a user.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
indexDir = mkOption {
|
indexDir = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
@@ -414,6 +574,14 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useUTF8FolderNames = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Store mailbox names on disk using UTF-8 instead of modified UTF-7 (mUTF-7).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
hierarchySeparator = mkOption {
|
hierarchySeparator = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = ".";
|
default = ".";
|
||||||
@@ -454,7 +622,7 @@ in
|
|||||||
|
|
||||||
certificateScheme = let
|
certificateScheme = let
|
||||||
schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ];
|
schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ];
|
||||||
translate = i: warn "setting mailserver.certificateScheme by number is deprecated, please use names instead"
|
translate = i: warn "Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${(builtins.elemAt schemes (i - 1))}\"'."
|
||||||
(builtins.elemAt schemes (i - 1));
|
(builtins.elemAt schemes (i - 1));
|
||||||
in mkOption {
|
in mkOption {
|
||||||
type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes);
|
type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes);
|
||||||
@@ -787,6 +955,21 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
smtpdForbidBareNewline = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
With "smtpd_forbid_bare_newline = yes", the Postfix SMTP server
|
||||||
|
disconnects a remote SMTP client that sends a line ending in a 'bare
|
||||||
|
newline'.
|
||||||
|
|
||||||
|
This feature was added in Postfix 3.8.4 against SMTP Smuggling and will
|
||||||
|
default to "yes" in Postfix 3.9.
|
||||||
|
|
||||||
|
https://www.postfix.org/smtp-smuggling.html
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
sendingFqdn = mkOption {
|
sendingFqdn = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = cfg.fqdn;
|
default = cfg.fqdn;
|
||||||
@@ -1101,6 +1284,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
imports = [
|
imports = [
|
||||||
|
./mail-server/assertions.nix
|
||||||
./mail-server/borgbackup.nix
|
./mail-server/borgbackup.nix
|
||||||
./mail-server/debug.nix
|
./mail-server/debug.nix
|
||||||
./mail-server/rsnapshot.nix
|
./mail-server/rsnapshot.nix
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ Autodiscovery
|
|||||||
`RFC6186 <https://www.rfc-editor.org/rfc/rfc6186>`_ allows supporting email clients to automatically discover SMTP / IMAP addresses
|
`RFC6186 <https://www.rfc-editor.org/rfc/rfc6186>`_ allows supporting email clients to automatically discover SMTP / IMAP addresses
|
||||||
of the mailserver. For that, the following records are required:
|
of the mailserver. For that, the following records are required:
|
||||||
|
|
||||||
================ ==== ==== ======== ====== ==== =================
|
================= ==== ==== ======== ====== ==== =================
|
||||||
Record TTL Type Priority Weight Port Value
|
Record TTL Type Priority Weight Port Value
|
||||||
================ ==== ==== ======== ====== ==== =================
|
================= ==== ==== ======== ====== ==== =================
|
||||||
_submission._tcp 3600 SRV 5 0 587 mail.example.com.
|
_submission._tcp 3600 SRV 5 0 587 mail.example.com.
|
||||||
|
_submissions._tcp 3600 SRV 5 0 465 mail.example.com.
|
||||||
_imap._tcp 3600 SRV 5 0 143 mail.example.com.
|
_imap._tcp 3600 SRV 5 0 143 mail.example.com.
|
||||||
_imaps._tcp 3600 SRV 5 0 993 mail.example.com.
|
_imaps._tcp 3600 SRV 5 0 993 mail.example.com.
|
||||||
================ ==== ==== ======== ====== ==== =================
|
================= ==== ==== ======== ====== ==== =================
|
||||||
|
|
||||||
Please note that only a few MUAs currently implement this. For vendor-specific
|
Please note that only a few MUAs currently implement this. For vendor-specific
|
||||||
discovery mechanisms `automx <https://github.com/rseichter/automx2>`_ can be used instead.
|
discovery mechanisms `automx <https://github.com/rseichter/automx2>`_ can be used instead.
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ Welcome to NixOS Mailserver's documentation!
|
|||||||
fts
|
fts
|
||||||
flakes
|
flakes
|
||||||
autodiscovery
|
autodiscovery
|
||||||
|
ldap
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|||||||
14
docs/ldap.rst
Normal file
14
docs/ldap.rst
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
LDAP Support
|
||||||
|
============
|
||||||
|
|
||||||
|
It is possible to manage mail user accounts with LDAP rather than with
|
||||||
|
the option `loginAccounts <options.html#mailserver-loginaccounts>`_.
|
||||||
|
|
||||||
|
All related LDAP options are described in the `LDAP options section
|
||||||
|
<options.html#mailserver-ldap>`_ and the `LDAP test
|
||||||
|
<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/tests/ldap.nix>`_
|
||||||
|
provides a getting started example.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
The LDAP support can not be enabled if some accounts are also defined with ``mailserver.loginAccounts``.
|
||||||
|
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
Release Notes
|
Release Notes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
|
||||||
|
NixOS 23.05
|
||||||
|
-----------
|
||||||
|
|
||||||
|
- Existing ACME certificates can be reused without configuring NGINX
|
||||||
|
- Certificate scheme is no longer a number, but a meaningful string instead
|
||||||
|
|
||||||
NixOS 22.11
|
NixOS 22.11
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
- Allow Rspamd to send dmarc reporting
|
- Allow Rspamd to send DMARC reporting
|
||||||
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/244>`__)
|
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/244>`__)
|
||||||
|
|
||||||
NixOS 22.05
|
NixOS 22.05
|
||||||
|
|||||||
@@ -48,18 +48,19 @@ Setup the server
|
|||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The following describes a server setup that is fairly complete. Even
|
The following describes a server setup that is fairly complete. Even
|
||||||
though there are more possible options (see the ``default.nix`` file),
|
though there are more possible options (see the `NixOS Mailserver
|
||||||
these should be the most common ones.
|
options documentation <options.html>`_), these should be the most
|
||||||
|
common ones.
|
||||||
|
|
||||||
.. code:: nix
|
.. code:: nix
|
||||||
|
|
||||||
{ config, pkgs, ... }:
|
{ config, pkgs, ... }: {
|
||||||
{
|
|
||||||
imports = [
|
imports = [
|
||||||
(builtins.fetchTarball {
|
(builtins.fetchTarball {
|
||||||
# Pick a commit from the branch you are interested in
|
# Pick a release version you are interested in and set its hash, e.g.
|
||||||
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz";
|
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz";
|
||||||
# And set its hash
|
# To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command:
|
||||||
|
# release="nixos-23.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
|
||||||
sha256 = "0000000000000000000000000000000000000000000000000000";
|
sha256 = "0000000000000000000000000000000000000000000000000000";
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
@@ -83,6 +84,8 @@ these should be the most common ones.
|
|||||||
# down nginx and opens port 80.
|
# down nginx and opens port 80.
|
||||||
certificateScheme = "acme-nginx";
|
certificateScheme = "acme-nginx";
|
||||||
};
|
};
|
||||||
|
security.acme.acceptTerms = true;
|
||||||
|
security.acme.defaults.email = "security@example.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
After a ``nixos-rebuild switch`` your server should be running all
|
After a ``nixos-rebuild switch`` your server should be running all
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "23.05";
|
name = "23.05";
|
||||||
pkgs = nixpkgs-22_11.legacyPackages.${system};
|
pkgs = nixpkgs-23_05.legacyPackages.${system};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
testNames = [
|
testNames = [
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
"external"
|
"external"
|
||||||
"clamav"
|
"clamav"
|
||||||
"multiple"
|
"multiple"
|
||||||
|
"ldap"
|
||||||
];
|
];
|
||||||
genTest = testName: release: {
|
genTest = testName: release: {
|
||||||
"name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
|
"name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
|
||||||
|
|||||||
17
mail-server/assertions.nix
Normal file
17
mail-server/assertions.nix
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
{
|
||||||
|
assertions = lib.optionals config.mailserver.ldap.enable [
|
||||||
|
{
|
||||||
|
assertion = config.mailserver.loginAccounts == {};
|
||||||
|
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = config.mailserver.extraVirtualAliases == {};
|
||||||
|
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = config.mailserver.forwards == {};
|
||||||
|
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.forwards";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -45,4 +45,25 @@ in
|
|||||||
if value.hashedPasswordFile == null then
|
if value.hashedPasswordFile == null then
|
||||||
builtins.toString (mkHashFile name value.hashedPassword)
|
builtins.toString (mkHashFile name value.hashedPassword)
|
||||||
else value.hashedPasswordFile) cfg.loginAccounts;
|
else value.hashedPasswordFile) cfg.loginAccounts;
|
||||||
|
|
||||||
|
# Appends the LDAP bind password to files to avoid writing this
|
||||||
|
# password into the Nix store.
|
||||||
|
appendLdapBindPwd = {
|
||||||
|
name, file, prefix, passwordFile, destination
|
||||||
|
}: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
||||||
|
#!${pkgs.stdenv.shell}
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
baseDir=$(dirname ${destination})
|
||||||
|
if (! test -d "$baseDir"); then
|
||||||
|
mkdir -p $baseDir
|
||||||
|
chmod 755 $baseDir
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat ${file} > ${destination}
|
||||||
|
echo -n "${prefix}" >> ${destination}
|
||||||
|
cat ${passwordFile} >> ${destination}
|
||||||
|
chmod 600 ${destination}
|
||||||
|
'';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,16 +22,18 @@ let
|
|||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
passwdDir = "/run/dovecot2";
|
passwdDir = "/run/dovecot2";
|
||||||
passdbFile = "${passwdDir}/passdb";
|
passwdFile = "${passwdDir}/passwd";
|
||||||
userdbFile = "${passwdDir}/userdb";
|
userdbFile = "${passwdDir}/userdb";
|
||||||
|
# This file contains the ldap bind password
|
||||||
|
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
|
||||||
bool2int = x: if x then "1" else "0";
|
bool2int = x: if x then "1" else "0";
|
||||||
|
|
||||||
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
||||||
|
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
|
||||||
|
|
||||||
# maildir in format "/${domain}/${user}"
|
# maildir in format "/${domain}/${user}"
|
||||||
dovecotMaildir =
|
dovecotMaildir =
|
||||||
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"
|
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}"
|
||||||
+ (lib.optionalString (cfg.indexDir != null)
|
+ (lib.optionalString (cfg.indexDir != null)
|
||||||
":INDEX=${cfg.indexDir}/%d/%n"
|
":INDEX=${cfg.indexDir}/%d/%n"
|
||||||
);
|
);
|
||||||
@@ -58,6 +60,41 @@ let
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
ldapConfig = pkgs.writeTextFile {
|
||||||
|
name = "dovecot-ldap.conf.ext.template";
|
||||||
|
text = ''
|
||||||
|
ldap_version = 3
|
||||||
|
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
|
||||||
|
${lib.optionalString cfg.ldap.startTls ''
|
||||||
|
tls = yes
|
||||||
|
''}
|
||||||
|
tls_require_cert = hard
|
||||||
|
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
||||||
|
dn = ${cfg.ldap.bind.dn}
|
||||||
|
sasl_bind = no
|
||||||
|
auth_bind = yes
|
||||||
|
base = ${cfg.ldap.searchBase}
|
||||||
|
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
|
||||||
|
${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") ''
|
||||||
|
user_attrs = ${cfg.ldap.dovecot.userAttrs}
|
||||||
|
''}
|
||||||
|
user_filter = ${cfg.ldap.dovecot.userFilter}
|
||||||
|
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
|
||||||
|
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
|
||||||
|
''}
|
||||||
|
pass_filter = ${cfg.ldap.dovecot.passFilter}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
setPwdInLdapConfFile = appendLdapBindPwd {
|
||||||
|
name = "ldap-conf-file";
|
||||||
|
file = ldapConfig;
|
||||||
|
prefix = "dnpass = ";
|
||||||
|
passwordFile = cfg.ldap.bind.passwordFile;
|
||||||
|
destination = ldapConfFile;
|
||||||
|
};
|
||||||
|
|
||||||
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
|
|
||||||
@@ -68,6 +105,9 @@ let
|
|||||||
chmod 755 "${passwdDir}"
|
chmod 755 "${passwdDir}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Prevent world-readable password files, even temporarily.
|
||||||
|
umask 077
|
||||||
|
|
||||||
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
|
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
|
||||||
if [ ! -f "$f" ]; then
|
if [ ! -f "$f" ]; then
|
||||||
echo "Expected password hash file $f does not exist!"
|
echo "Expected password hash file $f does not exist!"
|
||||||
@@ -75,7 +115,7 @@ let
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
cat <<EOF > ${passdbFile}
|
cat <<EOF > ${passwdFile}
|
||||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
||||||
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
||||||
) cfg.loginAccounts)}
|
) cfg.loginAccounts)}
|
||||||
@@ -89,9 +129,6 @@ let
|
|||||||
else "")
|
else "")
|
||||||
) cfg.loginAccounts)}
|
) cfg.loginAccounts)}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 600 ${passdbFile}
|
|
||||||
chmod 600 ${userdbFile}
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
|
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
|
||||||
@@ -99,6 +136,12 @@ let
|
|||||||
# The assertion garantees there is exactly one Junk mailbox.
|
# The assertion garantees there is exactly one Junk mailbox.
|
||||||
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
|
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
|
||||||
|
|
||||||
|
mkLdapSearchScope = scope: (
|
||||||
|
if scope == "sub" then "subtree"
|
||||||
|
else if scope == "one" then "onelevel"
|
||||||
|
else scope
|
||||||
|
);
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
@@ -109,6 +152,13 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
|
||||||
|
# the global config and tries to open shared libraries configured in there,
|
||||||
|
# which are usually not compatible.
|
||||||
|
environment.systemPackages = [
|
||||||
|
pkgs.dovecot_pigeonhole
|
||||||
|
];
|
||||||
|
|
||||||
services.dovecot2 = {
|
services.dovecot2 = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableImap = enableImap || enableImapSsl;
|
enableImap = enableImap || enableImapSsl;
|
||||||
@@ -220,7 +270,7 @@ in
|
|||||||
|
|
||||||
passdb {
|
passdb {
|
||||||
driver = passwd-file
|
driver = passwd-file
|
||||||
args = ${passdbFile}
|
args = ${passwdFile}
|
||||||
}
|
}
|
||||||
|
|
||||||
userdb {
|
userdb {
|
||||||
@@ -229,6 +279,19 @@ in
|
|||||||
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}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
${lib.optionalString cfg.ldap.enable ''
|
||||||
|
passdb {
|
||||||
|
driver = ldap
|
||||||
|
args = ${ldapConfFile}
|
||||||
|
}
|
||||||
|
|
||||||
|
userdb {
|
||||||
|
driver = ldap
|
||||||
|
args = ${ldapConfFile}
|
||||||
|
default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID}
|
||||||
|
}
|
||||||
|
''}
|
||||||
|
|
||||||
service auth {
|
service auth {
|
||||||
unix_listener auth {
|
unix_listener auth {
|
||||||
mode = 0660
|
mode = 0660
|
||||||
@@ -301,10 +364,10 @@ in
|
|||||||
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
|
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
|
||||||
done
|
done
|
||||||
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
|
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
|
||||||
'';
|
'' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
|
systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]);
|
||||||
|
|
||||||
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
|
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
|
||||||
description = "Optimize dovecot indices for fts_xapian";
|
description = "Optimize dovecot indices for fts_xapian";
|
||||||
|
|||||||
@@ -33,6 +33,11 @@ let
|
|||||||
let to = name;
|
let to = name;
|
||||||
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
|
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
|
||||||
cfg.loginAccounts));
|
cfg.loginAccounts));
|
||||||
|
regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||||
|
(name: value:
|
||||||
|
let to = name;
|
||||||
|
in map (from: {"${from}" = to;}) value.aliasesRegexp)
|
||||||
|
cfg.loginAccounts));
|
||||||
|
|
||||||
# catchAllPostfix :: Map String [String]
|
# catchAllPostfix :: Map String [String]
|
||||||
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||||
@@ -65,6 +70,10 @@ let
|
|||||||
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
|
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
|
||||||
in builtins.toFile "valias" content;
|
in builtins.toFile "valias" content;
|
||||||
|
|
||||||
|
regex_valiases_file = let
|
||||||
|
content = lookupTableToString regex_valiases_postfix;
|
||||||
|
in builtins.toFile "regex_valias" content;
|
||||||
|
|
||||||
# denied_recipients_postfix :: [ String ]
|
# denied_recipients_postfix :: [ String ]
|
||||||
denied_recipients_postfix = (map
|
denied_recipients_postfix = (map
|
||||||
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
|
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
|
||||||
@@ -94,6 +103,7 @@ let
|
|||||||
# every alias is owned (uniquely) by its user.
|
# every alias is owned (uniquely) by its user.
|
||||||
# The user's own address is already in all_valiases_postfix.
|
# The user's own address is already in all_valiases_postfix.
|
||||||
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
||||||
|
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (lookupTableToString regex_valiases_postfix);
|
||||||
|
|
||||||
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
|
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
|
||||||
# Removes sensitive headers from mails handed in via the submission port.
|
# Removes sensitive headers from mails handed in via the submission port.
|
||||||
@@ -123,6 +133,7 @@ let
|
|||||||
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
|
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
|
||||||
|
|
||||||
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
||||||
|
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
|
||||||
|
|
||||||
submissionOptions =
|
submissionOptions =
|
||||||
{
|
{
|
||||||
@@ -133,21 +144,73 @@ let
|
|||||||
smtpd_sasl_security_options = "noanonymous";
|
smtpd_sasl_security_options = "noanonymous";
|
||||||
smtpd_sasl_local_domain = "$myhostname";
|
smtpd_sasl_local_domain = "$myhostname";
|
||||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||||
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts";
|
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${lib.optionalString (regex_valiases_postfix != {}) ",pcre:/etc/postfix/regex_vaccounts"}";
|
||||||
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
||||||
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||||
cleanup_service_name = "submission-header-cleanup";
|
cleanup_service_name = "submission-header-cleanup";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
commonLdapConfig = ''
|
||||||
|
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
|
||||||
|
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
|
||||||
|
version = 3
|
||||||
|
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
||||||
|
tls_require_cert = yes
|
||||||
|
|
||||||
|
search_base = ${cfg.ldap.searchBase}
|
||||||
|
scope = ${cfg.ldap.searchScope}
|
||||||
|
|
||||||
|
bind = yes
|
||||||
|
bind_dn = ${cfg.ldap.bind.dn}
|
||||||
|
'';
|
||||||
|
|
||||||
|
ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" ''
|
||||||
|
${commonLdapConfig}
|
||||||
|
query_filter = ${cfg.ldap.postfix.filter}
|
||||||
|
result_attribute = ${cfg.ldap.postfix.mailAttribute}
|
||||||
|
'';
|
||||||
|
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
|
||||||
|
appendPwdInSenderLoginMap = appendLdapBindPwd {
|
||||||
|
name = "ldap-sender-login-map";
|
||||||
|
file = ldapSenderLoginMap;
|
||||||
|
prefix = "bind_pw = ";
|
||||||
|
passwordFile = cfg.ldap.bind.passwordFile;
|
||||||
|
destination = ldapSenderLoginMapFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" ''
|
||||||
|
${commonLdapConfig}
|
||||||
|
query_filter = ${cfg.ldap.postfix.filter}
|
||||||
|
result_attribute = ${cfg.ldap.postfix.uidAttribute}
|
||||||
|
'';
|
||||||
|
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
|
||||||
|
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
|
||||||
|
name = "ldap-virtual-mailbox-map";
|
||||||
|
file = ldapVirtualMailboxMap;
|
||||||
|
prefix = "bind_pw = ";
|
||||||
|
passwordFile = cfg.ldap.bind.passwordFile;
|
||||||
|
destination = ldapVirtualMailboxMapFile;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
|
|
||||||
|
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
|
||||||
|
preStart = ''
|
||||||
|
${appendPwdInVirtualMailboxMap}
|
||||||
|
${appendPwdInSenderLoginMap}
|
||||||
|
'';
|
||||||
|
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
|
||||||
|
};
|
||||||
|
|
||||||
services.postfix = {
|
services.postfix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hostname = "${sendingFqdn}";
|
hostname = "${sendingFqdn}";
|
||||||
networksStyle = "host";
|
networksStyle = "host";
|
||||||
mapFiles."valias" = valiases_file;
|
mapFiles."valias" = valiases_file;
|
||||||
|
mapFiles."regex_valias" = regex_valiases_file;
|
||||||
mapFiles."vaccounts" = vaccounts_file;
|
mapFiles."vaccounts" = vaccounts_file;
|
||||||
|
mapFiles."regex_vaccounts" = regex_vaccounts_file;
|
||||||
mapFiles."denied_recipients" = denied_recipients_file;
|
mapFiles."denied_recipients" = denied_recipients_file;
|
||||||
mapFiles."reject_senders" = reject_senders_file;
|
mapFiles."reject_senders" = reject_senders_file;
|
||||||
mapFiles."reject_recipients" = reject_recipients_file;
|
mapFiles."reject_recipients" = reject_recipients_file;
|
||||||
@@ -170,7 +233,16 @@ in
|
|||||||
virtual_gid_maps = "static:5000";
|
virtual_gid_maps = "static:5000";
|
||||||
virtual_mailbox_base = mailDirectory;
|
virtual_mailbox_base = mailDirectory;
|
||||||
virtual_mailbox_domains = vhosts_file;
|
virtual_mailbox_domains = vhosts_file;
|
||||||
virtual_mailbox_maps = mappedFile "valias";
|
virtual_mailbox_maps = [
|
||||||
|
(mappedFile "valias")
|
||||||
|
] ++ lib.optionals (cfg.ldap.enable) [
|
||||||
|
"ldap:${ldapVirtualMailboxMapFile}"
|
||||||
|
] ++ lib.optionals (regex_valiases_postfix != {}) [
|
||||||
|
(mappedRegexFile "regex_valias")
|
||||||
|
];
|
||||||
|
virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [
|
||||||
|
(mappedRegexFile "regex_valias")
|
||||||
|
]);
|
||||||
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
||||||
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
|
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
|
||||||
lmtp_destination_recipient_limit = "1";
|
lmtp_destination_recipient_limit = "1";
|
||||||
@@ -237,6 +309,9 @@ in
|
|||||||
milter_protocol = "6";
|
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}";
|
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
||||||
|
|
||||||
|
# Fix for https://www.postfix.org/smtp-smuggling.html
|
||||||
|
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;
|
||||||
|
smtpd_forbid_bare_newline_exclusions = "$mynetworks";
|
||||||
};
|
};
|
||||||
|
|
||||||
submissionOptions = submissionOptions;
|
submissionOptions = submissionOptions;
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ in
|
|||||||
in ''
|
in ''
|
||||||
# Create mail directory and set permissions. See
|
# Create mail directory and set permissions. See
|
||||||
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
||||||
|
# Prevent world-readable paths, even temporarily.
|
||||||
|
umask 007
|
||||||
mkdir -p ${directories}
|
mkdir -p ${directories}
|
||||||
chgrp "${vmailGroupName}" ${directories}
|
chgrp "${vmailGroupName}" ${directories}
|
||||||
chmod 02770 ${directories}
|
chmod 02770 ${directories}
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ let
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Prevent world-readable paths, even temporarily.
|
||||||
|
umask 007
|
||||||
|
|
||||||
# Create directory to store user sieve scripts if it doesn't exist
|
# Create directory to store user sieve scripts if it doesn't exist
|
||||||
if (! test -d "${sieveDirectory}"); then
|
if (! test -d "${sieveDirectory}"); then
|
||||||
mkdir "${sieveDirectory}"
|
mkdir "${sieveDirectory}"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ groups = ["mailserver.loginAccounts",
|
|||||||
"mailserver.dmarcReporting",
|
"mailserver.dmarcReporting",
|
||||||
"mailserver.fullTextSearch",
|
"mailserver.fullTextSearch",
|
||||||
"mailserver.redis",
|
"mailserver.redis",
|
||||||
|
"mailserver.ldap",
|
||||||
"mailserver.monitoring",
|
"mailserver.monitoring",
|
||||||
"mailserver.backup",
|
"mailserver.backup",
|
||||||
"mailserver.borgbackup"]
|
"mailserver.borgbackup"]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import time
|
|||||||
|
|
||||||
RETRY = 100
|
RETRY = 100
|
||||||
|
|
||||||
def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, starttls):
|
def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls):
|
||||||
print("Sending mail with subject '{}'".format(subject))
|
print("Sending mail with subject '{}'".format(subject))
|
||||||
message = "\n".join([
|
message = "\n".join([
|
||||||
"From: {from_addr}",
|
"From: {from_addr}",
|
||||||
@@ -30,7 +30,7 @@ def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, star
|
|||||||
if starttls:
|
if starttls:
|
||||||
smtp.starttls()
|
smtp.starttls()
|
||||||
if from_pwd is not None:
|
if from_pwd is not None:
|
||||||
smtp.login(from_addr, from_pwd)
|
smtp.login(smtp_username or from_addr, from_pwd)
|
||||||
|
|
||||||
smtp.sendmail(from_addr, [to_addr], message)
|
smtp.sendmail(from_addr, [to_addr], message)
|
||||||
return
|
return
|
||||||
@@ -141,6 +141,7 @@ def send_and_read(args):
|
|||||||
|
|
||||||
_send_mail(smtp_host=args.smtp_host,
|
_send_mail(smtp_host=args.smtp_host,
|
||||||
smtp_port=args.smtp_port,
|
smtp_port=args.smtp_port,
|
||||||
|
smtp_username=args.smtp_username,
|
||||||
from_addr=args.from_addr,
|
from_addr=args.from_addr,
|
||||||
from_pwd=src_pwd,
|
from_pwd=src_pwd,
|
||||||
to_addr=args.to_addr,
|
to_addr=args.to_addr,
|
||||||
@@ -171,6 +172,7 @@ parser_send_and_read = subparsers.add_parser('send-and-read', description="Send
|
|||||||
parser_send_and_read.add_argument('--smtp-host', type=str)
|
parser_send_and_read.add_argument('--smtp-host', type=str)
|
||||||
parser_send_and_read.add_argument('--smtp-port', type=str, default=25)
|
parser_send_and_read.add_argument('--smtp-port', type=str, default=25)
|
||||||
parser_send_and_read.add_argument('--smtp-starttls', action='store_true')
|
parser_send_and_read.add_argument('--smtp-starttls', action='store_true')
|
||||||
|
parser_send_and_read.add_argument('--smtp-username', type=str, default='', help="username used for smtp login. If not specified, the from-addr value is used")
|
||||||
parser_send_and_read.add_argument('--from-addr', type=str)
|
parser_send_and_read.add_argument('--from-addr', type=str)
|
||||||
parser_send_and_read.add_argument('--imap-host', required=True, type=str)
|
parser_send_and_read.add_argument('--imap-host', required=True, type=str)
|
||||||
parser_send_and_read.add_argument('--imap-port', type=str, default=993)
|
parser_send_and_read.add_argument('--imap-port', type=str, default=993)
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ pkgs.nixosTest {
|
|||||||
mailserver = {
|
mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
fqdn = "mail.example.com";
|
fqdn = "mail.example.com";
|
||||||
domains = [ "example.com" ];
|
domains = [ "example.com" "domain.com" ];
|
||||||
localDnsResolver = false;
|
localDnsResolver = false;
|
||||||
|
|
||||||
loginAccounts = {
|
loginAccounts = {
|
||||||
@@ -64,6 +64,7 @@ pkgs.nixosTest {
|
|||||||
};
|
};
|
||||||
"user2@example.com" = {
|
"user2@example.com" = {
|
||||||
hashedPasswordFile = hashedPasswordFile;
|
hashedPasswordFile = hashedPasswordFile;
|
||||||
|
aliasesRegexp = [''/^user2.*@domain\.com$/''];
|
||||||
};
|
};
|
||||||
"send-only@example.com" = {
|
"send-only@example.com" = {
|
||||||
hashedPasswordFile = hashPassword "send-only";
|
hashedPasswordFile = hashPassword "send-only";
|
||||||
@@ -126,6 +127,46 @@ pkgs.nixosTest {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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("vmail gid is set correctly"):
|
with subtest("vmail gid is set correctly"):
|
||||||
machine.succeed("getent group vmail | grep 5000")
|
machine.succeed("getent group vmail | grep 5000")
|
||||||
|
|
||||||
|
|||||||
183
tests/ldap.nix
Normal file
183
tests/ldap.nix
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> {}
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
bindPassword = "unsafegibberish";
|
||||||
|
alicePassword = "testalice";
|
||||||
|
bobPassword = "testbob";
|
||||||
|
in
|
||||||
|
pkgs.nixosTest {
|
||||||
|
name = "ldap";
|
||||||
|
nodes = {
|
||||||
|
machine = { config, pkgs, ... }: {
|
||||||
|
imports = [
|
||||||
|
./../default.nix
|
||||||
|
./lib/config.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
virtualisation.memorySize = 1024;
|
||||||
|
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
permitRootLogin = "yes";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = [
|
||||||
|
(pkgs.writeScriptBin "mail-check" ''
|
||||||
|
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||||
|
'')];
|
||||||
|
|
||||||
|
environment.etc.bind-password.text = bindPassword;
|
||||||
|
|
||||||
|
services.openldap = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
children = {
|
||||||
|
"cn=schema".includes = [
|
||||||
|
"${pkgs.openldap}/etc/schema/core.ldif"
|
||||||
|
"${pkgs.openldap}/etc/schema/cosine.ldif"
|
||||||
|
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
|
||||||
|
"${pkgs.openldap}/etc/schema/nis.ldif"
|
||||||
|
];
|
||||||
|
"olcDatabase={1}mdb" = {
|
||||||
|
attrs = {
|
||||||
|
objectClass = [
|
||||||
|
"olcDatabaseConfig"
|
||||||
|
"olcMdbConfig"
|
||||||
|
];
|
||||||
|
olcDatabase = "{1}mdb";
|
||||||
|
olcDbDirectory = "/var/lib/openldap/example";
|
||||||
|
olcSuffix = "dc=example";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
declarativeContents."dc=example" = ''
|
||||||
|
dn: dc=example
|
||||||
|
objectClass: domain
|
||||||
|
dc: example
|
||||||
|
|
||||||
|
dn: cn=mail,dc=example
|
||||||
|
objectClass: organizationalRole
|
||||||
|
objectClass: simpleSecurityObject
|
||||||
|
objectClass: top
|
||||||
|
cn: mail
|
||||||
|
userPassword: ${bindPassword}
|
||||||
|
|
||||||
|
dn: ou=users,dc=example
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: users
|
||||||
|
|
||||||
|
dn: cn=alice,ou=users,dc=example
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
cn: alice
|
||||||
|
sn: Foo
|
||||||
|
mail: alice@example.com
|
||||||
|
userPassword: ${alicePassword}
|
||||||
|
|
||||||
|
dn: cn=bob,ou=users,dc=example
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
cn: bob
|
||||||
|
sn: Bar
|
||||||
|
mail: bob@example.com
|
||||||
|
userPassword: ${bobPassword}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.example.com";
|
||||||
|
domains = [ "example.com" ];
|
||||||
|
localDnsResolver = false;
|
||||||
|
|
||||||
|
ldap = {
|
||||||
|
enable = true;
|
||||||
|
uris = [
|
||||||
|
"ldap://"
|
||||||
|
];
|
||||||
|
bind = {
|
||||||
|
dn = "cn=mail,dc=example";
|
||||||
|
passwordFile = "/etc/bind-password";
|
||||||
|
};
|
||||||
|
searchBase = "ou=users,dc=example";
|
||||||
|
searchScope = "sub";
|
||||||
|
};
|
||||||
|
|
||||||
|
vmailGroupName = "vmail";
|
||||||
|
vmailUID = 5000;
|
||||||
|
|
||||||
|
enableImap = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
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("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"
|
||||||
|
]))
|
||||||
|
'';
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ let
|
|||||||
};
|
};
|
||||||
services.dnsmasq = {
|
services.dnsmasq = {
|
||||||
enable = true;
|
enable = true;
|
||||||
# Fixme: once nixos-23.05 hhas been removed, could be replaced by
|
# Fixme: once nixos-22.11 has been removed, could be replaced by
|
||||||
# settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
|
# settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
mx-host=domain1.com,domain1,10
|
mx-host=domain1.com,domain1,10
|
||||||
|
|||||||
Reference in New Issue
Block a user