4 Commits

Author SHA1 Message Date
Jakub Skokan
478da2c67b Allow TLSv1 for compatibility with older devices 2023-06-01 16:53:35 +02:00
Antoine Eiche
acc7791ee9 readme: remove the announcement public key
Current maintainer no longer has it.
2023-05-30 23:37:31 +02:00
Antoine Eiche
758fa8f9bc Release 23.05 2023-05-30 23:37:31 +02:00
Antoine Eiche
811389e31b Preserve the compatibility with nixos-23.05 2023-05-30 23:37:31 +02:00
27 changed files with 202 additions and 779 deletions

View File

@@ -32,8 +32,8 @@ let
desc = prJobsets // {
"master" = mkFlakeJobset "master";
"nixos-23.11" = mkFlakeJobset "nixos-23.11";
"nixos-24.05" = mkFlakeJobset "nixos-24.05";
"nixos-22.11" = mkFlakeJobset "nixos-22.11";
"nixos-23.05" = mkFlakeJobset "nixos-23.05";
};
log = {

View File

@@ -8,14 +8,14 @@
For each NixOS release, we publish a branch. You then have to use the
SNM branch corresponding to your NixOS version.
* For NixOS 24.05
- Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05)
* For NixOS 23.11
- Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/release-notes.html#nixos-23-11)
* For NixOS 23.05
- Use the [SNM branch `nixos-23.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.05)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/release-notes.html#nixos-23-05)
* For NixOS 22.11
- Use the [SNM branch `nixos-22.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.11)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/release-notes.html#nixos-22-11)
* For NixOS unstable
- Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
@@ -71,11 +71,46 @@ can stay up to date with bug fixes and updates.
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
- 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
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).
Check out the [Complete Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
## Development

View File

@@ -111,15 +111,6 @@ 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 {
type = with types; listOf (enum cfg.domains);
example = ["example.com" "example2.com"];
@@ -207,157 +198,6 @@ in
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.nullOr 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 {
type = types.nullOr types.str;
default = null;
@@ -574,14 +414,6 @@ 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 {
type = types.str;
default = ".";
@@ -622,7 +454,7 @@ in
certificateScheme = let
schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ];
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))}\"'."
translate = i: warn "setting mailserver.certificateScheme by number is deprecated, please use names instead"
(builtins.elemAt schemes (i - 1));
in mkOption {
type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes);
@@ -675,19 +507,6 @@ in
'';
};
acmeCertificateName = mkOption {
type = types.str;
default = cfg.fqdn;
example = "example.com";
description = ''
({option}`mailserver.certificateScheme` == `acme`)
When the `acme` `certificateScheme` is selected, you can use this option
to override the default certificate name. This is useful if you've
generated a wildcard certificate, for example.
'';
};
enableImap = mkOption {
type = types.bool;
default = true;
@@ -968,21 +787,6 @@ 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 {
type = types.str;
default = cfg.fqdn;
@@ -1297,7 +1101,6 @@ in
};
imports = [
./mail-server/assertions.nix
./mail-server/borgbackup.nix
./mail-server/debug.nix
./mail-server/rsnapshot.nix

View File

@@ -24,13 +24,12 @@ have to be used. These can still be generated using `mkpasswd -m bcrypt`.
in {
services.radicale = {
enable = true;
settings = {
auth = {
type = "htpasswd";
htpasswd_filename = "${htpasswd}";
htpasswd_encryption = "bcrypt";
};
};
config = ''
[auth]
type = htpasswd
htpasswd_filename = ${htpasswd}
htpasswd_encryption = bcrypt
'';
};
services.nginx = {

View File

@@ -20,7 +20,7 @@ servers may require more work.
extraConfig = ''
# starttls needed for authentication, so the fqdn required to match
# the certificate
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
$config['smtp_server'] = "tls://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';

View File

@@ -4,14 +4,13 @@ Autodiscovery
`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:
================= ==== ==== ======== ====== ==== =================
================ ==== ==== ======== ====== ==== =================
Record TTL Type Priority Weight Port Value
================= ==== ==== ======== ====== ==== =================
================ ==== ==== ======== ====== ==== =================
_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.
_imaps._tcp 3600 SRV 5 0 993 mail.example.com.
================= ==== ==== ======== ====== ==== =================
================ ==== ==== ======== ====== ==== =================
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.

View File

@@ -30,7 +30,6 @@ Welcome to NixOS Mailserver's documentation!
fts
flakes
autodiscovery
ldap
Indices and tables
==================

View File

@@ -1,14 +0,0 @@
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``.

View File

@@ -1,28 +1,10 @@
Release Notes
=============
NixOS 24.05
-----------
- Add new option ``acmeCertificateName`` which can be used to support
wildcard certificates
NixOS 23.11
-----------
- Add basic support for LDAP users
- Add support for regex (PCRE) aliases
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
-----------
- 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>`__)
NixOS 22.05

View File

@@ -24,14 +24,17 @@ You can run the training in a root shell as follows:
.. code:: bash
# Path to the controller socket
export RSOCK="/var/run/rspamd/worker-controller.sock"
# Learn the Junk folder as spam
rspamc learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
rspamc -h $RSOCK learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
# Learn the INBOX as ham
rspamc learn_ham /var/vmail/$DOMAIN/$USER/cur/
rspamc -h $RSOCK learn_ham /var/vmail/$DOMAIN/$USER/cur/
# Check that training was successful
rspamc stat | grep learned
rspamc -h $RSOCK stat | grep learned
Tune symbol weight
~~~~~~~~~~~~~~~~~~

View File

@@ -48,19 +48,18 @@ Setup the server
~~~~~~~~~~~~~~~~
The following describes a server setup that is fairly complete. Even
though there are more possible options (see the `NixOS Mailserver
options documentation <options.html>`_), these should be the most
common ones.
though there are more possible options (see the ``default.nix`` file),
these should be the most common ones.
.. code:: nix
{ config, pkgs, ... }: {
{ config, pkgs, ... }:
{
imports = [
(builtins.fetchTarball {
# Pick a release version you are interested in and set its hash, e.g.
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz";
# 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
# Pick a commit from the branch you are interested in
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz";
# And set its hash
sha256 = "0000000000000000000000000000000000000000000000000000";
})
];
@@ -84,8 +83,6 @@ common ones.
# down nginx and opens port 80.
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

56
flake.lock generated
View File

@@ -19,11 +19,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"lastModified": 1668681692,
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"rev": "009399224d5e398d03b22badca40a37ac85412a1",
"type": "github"
},
"original": {
@@ -34,11 +34,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1717602782,
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
"lastModified": 1670751203,
"narHash": "sha256-XdoH1v3shKDGlrwjgrNX/EN8s3c+kQV7xY6cLCE8vcI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
"rev": "64e0bf055f9d25928c31fb12924e59ff8ce71e60",
"type": "github"
},
"original": {
@@ -47,18 +47,33 @@
"type": "indirect"
}
},
"nixpkgs-24_05": {
"nixpkgs-22_11": {
"locked": {
"lastModified": 1717144377,
"narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=",
"lastModified": 1669558522,
"narHash": "sha256-yqxn+wOiPqe6cxzOo4leeJOp1bXE/fjPEi/3F/bBHv8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "805a384895c696f802a9bf5bf4720f37385df547",
"rev": "ce5fe99df1f15a09a91a86be9738d68fadfbad82",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-24.05",
"ref": "nixos-22.11",
"type": "indirect"
}
},
"nixpkgs-23_05": {
"locked": {
"lastModified": 1684782344,
"narHash": "sha256-SHN8hPYYSX0thDrMLMWPWYulK3YFgASOrCsIL3AJ78g=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "8966c43feba2c701ed624302b6a935f97bcbdf88",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-23.05",
"type": "indirect"
}
},
@@ -67,7 +82,24 @@
"blobs": "blobs",
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs",
"nixpkgs-24_05": "nixpkgs-24_05"
"nixpkgs-22_11": "nixpkgs-22_11",
"nixpkgs-23_05": "nixpkgs-23_05",
"utils": "utils"
}
},
"utils": {
"locked": {
"lastModified": 1605370193,
"narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5021eac20303a61fafe17224c087f5519baed54d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
}
},

View File

@@ -6,15 +6,17 @@
url = "github:edolstra/flake-compat";
flake = false;
};
utils.url = "github:numtide/flake-utils";
nixpkgs.url = "flake:nixpkgs/nixos-unstable";
nixpkgs-24_05.url = "flake:nixpkgs/nixos-24.05";
nixpkgs-22_11.url = "flake:nixpkgs/nixos-22.11";
nixpkgs-23_05.url = "flake:nixpkgs/nixos-23.05";
blobs = {
url = "gitlab:simple-nixos-mailserver/blobs";
flake = false;
};
};
outputs = { self, blobs, nixpkgs, nixpkgs-24_05, ... }: let
outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_11, nixpkgs-23_05, ... }: let
lib = nixpkgs.lib;
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
@@ -24,8 +26,8 @@
pkgs = nixpkgs.legacyPackages.${system};
}
{
name = "24.05";
pkgs = nixpkgs-24_05.legacyPackages.${system};
name = "23.05";
pkgs = nixpkgs-22_11.legacyPackages.${system};
}
];
testNames = [
@@ -33,7 +35,6 @@
"external"
"clamav"
"multiple"
"ldap"
];
genTest = testName: release: {
"name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
@@ -89,7 +90,6 @@
sphinx
sphinx_rtd_theme
myst-parser
linkify-it-py
])
)];
buildPhase = ''

View File

@@ -1,22 +0,0 @@
{ 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";
}
] ++ lib.optionals (config.mailserver.enable && config.mailserver.certificateScheme != "acme") [
{
assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn;
message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName";
}
];
}

View File

@@ -26,7 +26,7 @@ in
else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem"
else throw "unknown certificate scheme";
# key :: PATH
@@ -35,7 +35,7 @@ in
else if cfg.certificateScheme == "selfsigned"
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem"
else throw "unknown certificate scheme";
passwordFiles = let
@@ -45,26 +45,4 @@ in
if value.hashedPasswordFile == null then
builtins.toString (mkHashFile name value.hashedPassword)
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, suffix ? "", 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}
echo -n '${suffix}' >> ${destination}
chmod 600 ${destination}
'';
}

View File

@@ -22,18 +22,16 @@ let
cfg = config.mailserver;
passwdDir = "/run/dovecot2";
passwdFile = "${passwdDir}/passwd";
passdbFile = "${passwdDir}/passdb";
userdbFile = "${passwdDir}/userdb";
# This file contains the ldap bind password
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
bool2int = x: if x then "1" else "0";
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
# maildir in format "/${domain}/${user}"
dovecotMaildir =
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}"
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"
+ (lib.optionalString (cfg.indexDir != null)
":INDEX=${cfg.indexDir}/%d/%n"
);
@@ -60,42 +58,6 @@ 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 != null) ''
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 = "'';
suffix = ''"'';
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapConfFile;
};
genPasswdScript = pkgs.writeScript "generate-password-file" ''
#!${pkgs.stdenv.shell}
@@ -106,9 +68,6 @@ let
chmod 755 "${passwdDir}"
fi
# Prevent world-readable password files, even temporarily.
umask 077
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
if [ ! -f "$f" ]; then
echo "Expected password hash file $f does not exist!"
@@ -116,7 +75,7 @@ let
fi
done
cat <<EOF > ${passwdFile}
cat <<EOF > ${passdbFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts)}
@@ -130,6 +89,9 @@ let
else "")
) cfg.loginAccounts)}
EOF
chmod 600 ${passdbFile}
chmod 600 ${userdbFile}
'';
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
@@ -137,12 +99,6 @@ let
# The assertion garantees there is exactly one Junk mailbox.
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
{
config = with cfg; lib.mkIf enable {
@@ -153,13 +109,6 @@ 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 = {
enable = true;
enableImap = enableImap || enableImapSsl;
@@ -176,18 +125,8 @@ in
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
protocols = lib.optional cfg.enableManageSieve "sieve";
pluginSettings = {
sieve = "file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve";
sieve_default = "file:${cfg.sieveDirectory}/%u/default.sieve";
sieve_default_name = "default";
};
sieve = {
extensions = [
"fileinto"
];
scripts.after = builtins.toFile "spam.sieve" ''
sieveScripts = {
after = builtins.toFile "spam.sieve" ''
require "fileinto";
if header :is "X-Spam" "Yes" {
@@ -195,29 +134,8 @@ in
stop;
}
'';
pipeBins = map lib.getExe [
(pkgs.writeShellScriptBin "sa-learn-ham.sh"
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
(pkgs.writeShellScriptBin "sa-learn-spam.sh"
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
];
};
imapsieve.mailbox = [
{
name = junkMailboxName;
causes = [ "COPY" "APPEND" ];
before = ./dovecot/imap_sieve/report-spam.sieve;
}
{
name = "*";
from = junkMailboxName;
causes = [ "COPY" ];
before = ./dovecot/imap_sieve/report-ham.sieve;
}
];
mailboxes = cfg.mailboxes;
extraConfig = ''
@@ -302,7 +220,7 @@ in
passdb {
driver = passwd-file
args = ${passwdFile}
args = ${passdbFile}
}
userdb {
@@ -311,19 +229,6 @@ in
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 {
unix_listener auth {
mode = 0660
@@ -339,6 +244,28 @@ in
inbox = yes
}
plugin {
sieve_plugins = sieve_imapsieve sieve_extprograms
sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve
sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve
sieve_default_name = default
# From elsewhere to Spam folder
imapsieve_mailbox1_name = ${junkMailboxName}
imapsieve_mailbox1_causes = COPY,APPEND
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
# From Spam folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = ${junkMailboxName}
imapsieve_mailbox2_causes = COPY
imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
}
${lib.optionalString cfg.fullTextSearch.enable ''
plugin {
plugin = fts fts_xapian
@@ -367,10 +294,17 @@ in
systemd.services.dovecot2 = {
preStart = ''
${genPasswdScript}
'' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
rm -rf '${stateDir}/imap_sieve'
mkdir '${stateDir}/imap_sieve'
cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
for k in "${stateDir}/imap_sieve"/*.sieve ; do
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
done
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
'';
};
systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]);
systemd.services.postfix.restartTriggers = [ genPasswdScript ];
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
description = "Optimize dovecot indices for fts_xapian";

View File

@@ -17,10 +17,11 @@
{ config, pkgs, lib, ... }:
with (import ./common.nix { inherit config lib pkgs; });
with (import ./common.nix { inherit config; });
let
cfg = config.mailserver;
acmeRoot = "/var/lib/acme/acme-challenge";
in
{
config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) {
@@ -31,10 +32,11 @@ in
serverAliases = cfg.certificateDomains;
forceSSL = true;
enableACME = true;
acmeRoot = acmeRoot;
};
};
security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
security.acme.certs."${cfg.fqdn}".reloadServices = [
"postfix.service"
"dovecot2.service"
];

View File

@@ -33,11 +33,6 @@ let
let to = name;
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
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 = mergeLookupTables (lib.flatten (lib.mapAttrsToList
@@ -70,10 +65,6 @@ let
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
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 = (map
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
@@ -103,7 +94,6 @@ let
# every alias is owned (uniquely) by its user.
# The user's own address is already in 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" (''
# Removes sensitive headers from mails handed in via the submission port.
@@ -133,7 +123,6 @@ let
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
submissionOptions =
{
@@ -144,73 +133,21 @@ let
smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = "$myhostname";
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
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_login_maps = "hash:/etc/postfix/vaccounts";
smtpd_sender_restrictions = "reject_sender_login_mismatch";
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
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
{
config = with cfg; lib.mkIf enable {
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
preStart = ''
${appendPwdInVirtualMailboxMap}
${appendPwdInSenderLoginMap}
'';
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
};
services.postfix = {
enable = true;
hostname = "${sendingFqdn}";
networksStyle = "host";
mapFiles."valias" = valiases_file;
mapFiles."regex_valias" = regex_valiases_file;
mapFiles."vaccounts" = vaccounts_file;
mapFiles."regex_vaccounts" = regex_vaccounts_file;
mapFiles."denied_recipients" = denied_recipients_file;
mapFiles."reject_senders" = reject_senders_file;
mapFiles."reject_recipients" = reject_recipients_file;
@@ -233,16 +170,7 @@ in
virtual_gid_maps = "static:5000";
virtual_mailbox_base = mailDirectory;
virtual_mailbox_domains = vhosts_file;
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_mailbox_maps = mappedFile "valias";
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
lmtp_destination_recipient_limit = "1";
@@ -274,6 +202,9 @@ in
# Submission by mail clients is handled in submissionOptions
smtpd_tls_security_level = "may";
# strong might suffice and is computationally less expensive
smtpd_tls_eecdh_grade = "ultra";
# Disable obselete protocols
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, TLSv1, !SSLv2, !SSLv3";
smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, TLSv1, !SSLv2, !SSLv3";
@@ -306,9 +237,6 @@ in
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}";
# Fix for https://www.postfix.org/smtp-smuggling.html
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;
smtpd_forbid_bare_newline_exclusions = "$mynetworks";
};
submissionOptions = submissionOptions;
@@ -325,7 +253,7 @@ in
privileged = true;
chroot = false;
command = "spawn";
args = [ "user=nobody" "argv=${pkgs.spf-engine}/bin/policyd-spf" "${policyd-spf}"];
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
};
"submission-header-cleanup" = {
type = "unix";

View File

@@ -25,21 +25,12 @@ let
in
{
config = with cfg; lib.mkIf enable {
environment.systemPackages = lib.mkBefore [
(pkgs.runCommand "rspamc-wrapped" {
nativeBuildInputs = with pkgs; [ makeWrapper ];
}''
makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \
--add-flags "-h /var/run/rspamd/worker-controller.sock"
'')
];
services.rspamd = {
enable = true;
inherit debug;
locals = {
"milter_headers.conf" = { text = ''
extended_spam_headers = true;
extended_spam_headers = yes;
''; };
"redis.conf" = { text = ''
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
@@ -78,6 +69,14 @@ in
''; };
};
overrides = {
"milter_headers.conf" = {
text = ''
extended_spam_headers = true;
'';
};
};
workers.rspamd_proxy = {
type = "rspamd_proxy";
bindSockets = [{

View File

@@ -64,8 +64,6 @@ in
in ''
# Create mail directory and set permissions. See
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
# Prevent world-readable paths, even temporarily.
umask 007
mkdir -p ${directories}
chgrp "${vmailGroupName}" ${directories}
chmod 02770 ${directories}

View File

@@ -34,9 +34,6 @@ let
set -euo pipefail
# Prevent world-readable paths, even temporarily.
umask 007
# Create directory to store user sieve scripts if it doesn't exist
if (! test -d "${sieveDirectory}"); then
mkdir "${sieveDirectory}"

View File

@@ -27,7 +27,6 @@ groups = ["mailserver.loginAccounts",
"mailserver.dmarcReporting",
"mailserver.fullTextSearch",
"mailserver.redis",
"mailserver.ldap",
"mailserver.monitoring",
"mailserver.backup",
"mailserver.borgbackup"]

View File

@@ -9,7 +9,7 @@ import time
RETRY = 100
def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls):
def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, starttls):
print("Sending mail with subject '{}'".format(subject))
message = "\n".join([
"From: {from_addr}",
@@ -30,7 +30,7 @@ def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr
if starttls:
smtp.starttls()
if from_pwd is not None:
smtp.login(smtp_username or from_addr, from_pwd)
smtp.login(from_addr, from_pwd)
smtp.sendmail(from_addr, [to_addr], message)
return
@@ -141,7 +141,6 @@ def send_and_read(args):
_send_mail(smtp_host=args.smtp_host,
smtp_port=args.smtp_port,
smtp_username=args.smtp_username,
from_addr=args.from_addr,
from_pwd=src_pwd,
to_addr=args.to_addr,
@@ -172,7 +171,6 @@ 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-port', type=str, default=25)
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('--imap-host', required=True, type=str)
parser_send_and_read.add_argument('--imap-port', type=str, default=993)

View File

@@ -501,6 +501,7 @@ pkgs.nixosTest {
with subtest("dmarc reporting"):
server.systemctl("start rspamd-dmarc-reporter.service")
server.wait_until_succeeds("journalctl -eu rspamd-dmarc-reporter.service -o cat | grep -q 'No reports for '")
with subtest("no warnings or errors"):
server.fail("journalctl -u postfix | grep -i error >&2")
@@ -508,7 +509,7 @@ pkgs.nixosTest {
server.fail("journalctl -u dovecot2 | grep -i error >&2")
# harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html
server.fail(
"journalctl -u dovecot2 |grep -v 'Expunged message reappeared, giving a new UID'| grep -v 'FTS Xapian: Box is empty' | grep -i warning >&2"
"journalctl -u dovecot2 |grep -v 'Expunged message reappeared, giving a new UID'| grep -i warning >&2"
)
'';
}

View File

@@ -55,7 +55,7 @@ pkgs.nixosTest {
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "domain.com" ];
domains = [ "example.com" ];
localDnsResolver = false;
loginAccounts = {
@@ -64,7 +64,6 @@ pkgs.nixosTest {
};
"user2@example.com" = {
hashedPasswordFile = hashedPasswordFile;
aliasesRegexp = [''/^user2.*@domain\.com$/''];
};
"send-only@example.com" = {
hashedPasswordFile = hashPassword "send-only";
@@ -127,46 +126,6 @@ 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"):
machine.succeed("getent group vmail | grep 5000")
@@ -177,7 +136,7 @@ pkgs.nixosTest {
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
)
machine.succeed(
"cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q '554 5.5.0 Error'"
"cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q 'This account cannot receive emails'"
)
with subtest("rspamd controller serves web ui"):

View File

@@ -1,183 +0,0 @@
{ 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"
]))
'';
}

View File

@@ -30,7 +30,7 @@ let
};
services.dnsmasq = {
enable = true;
# Fixme: once nixos-22.11 has been removed, could be replaced by
# Fixme: once nixos-23.05 hhas been removed, could be replaced by
# settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
extraConfig = ''
mx-host=domain1.com,domain1,10