Compare commits

...

40 commits

Author SHA1 Message Date
a98a93cf22 ci: deploy upstream on changes
All checks were successful
Build / deploy (push) Successful in 8s
2024-08-09 20:55:49 +01:00
806a4cfd21 test: Checking if virtual aliases are functional.
Relates to https://gitlab.skynet.ie/compsoc1/skynet/nixos/-/issues/22

test: Remove the account type limiatation
2024-07-21 13:12:53 +01:00
Sandro Jäckel
059b50b2e7
Allow setting userAttrs to empty string
This allows overwriting the default values for user_attrs to be empty
which is required when using virtual mailboxes with ldap accounts
that have posixAccount attributes set. When user_attrs is empty string
those are ignored then.
2024-07-16 11:15:14 +02:00
Isabel
290a995de5
refactor: policyd-spf -> spf-engine 2024-06-18 09:03:27 +01:00
isabel
54cbacb6eb
chore: remove flake utils 2024-06-14 21:52:49 +01:00
Antoine Eiche
29916981e7 Release 24.05 2024-06-11 07:36:43 +02:00
RoastedCheese
0d51a32e47 acme: test acmeCertificateName if module is enabled 2024-06-04 15:31:28 +00:00
Martin Weinelt
ed80b589d3
postfix: remove deprecated smtpd_tls_eecdh_grade
Causes a warning that suggests to just leave it at its default.
2024-06-03 12:34:43 +02:00
Matthew Leach
46a0829aa8 acme: Add new option acmeCertificateName
Allow the user to specify the name of the ACME configuration that the
mailserver should use. This allows users that request certificates that
aren't the FQDN of the mailserver, for example a wildcard certificate.
2024-05-31 09:53:32 +01:00
jopejoe1
41059fc548 docs: use settings instead of config in radicale 2024-05-03 09:14:16 +02:00
Sandro Jäckel
ef4756bcfc Quote ldap password
Otherwise special characters like # do not work
2024-04-28 10:02:48 +00:00
Sandro
9f6635a035 Drop default acmeRoot 2024-04-13 12:42:45 +00:00
Antoine Eiche
79c8cfcd58 Remove the support of 23.05 and 23.11
This is because SNM now supports the new sieve nixpkgs interface,
which is not backward compatible with previous releases.
2024-03-14 21:51:05 +01:00
Gaetan Lepage
799fe34c12 Update nixpkgs 2024-03-14 21:51:05 +01:00
Gaetan Lepage
d507bd9c95 dovecot: no longer need to copy sieve scripts 2024-03-14 21:50:46 +01:00
Raito Bezarius
fe6d325397 dovecot: support new sieve API in nixpkgs
Since https://github.com/NixOS/nixpkgs/pull/275031 things have became more structured
when it comes to the sieve plugin.

Relies on https://github.com/NixOS/nixpkgs/pull/281001 for full
features.
2024-03-09 23:23:17 +01:00
Christian Theune
572c1b4d69 rspamd: fix duplicate and syntactically wrong header settings
Fixes #280
2024-03-08 14:52:52 +01:00
Sleepful
9e36323ae3 Update roundcube example configuration: smtp_server is deprecated
Related issue on GH: https://github.com/roundcube/roundcubemail/issues/8756
2024-01-31 17:08:06 -06:00
Antoine Eiche
e47f3719f1 Release 23.11 2024-01-25 22:52:54 +01:00
Antoine Eiche
b5023b36a1 postfix: exclude $mynetwork from smtpd_forbid_bare_newline 2023-12-27 09:46:26 +01:00
Alvar Penning
3f526c08e8
postfix: SMTP Smuggling Protection
Enable Postfix SMTP Smuggling protection, introduced in Postfix 3.8.4,
which is, currently, only available within the nixpkgs' master branch.

- https://github.com/NixOS/nixpkgs/pull/276104
- https://github.com/NixOS/nixpkgs/pull/276264

For information about SMTP Smuggling:

- https://www.postfix.org/smtp-smuggling.html
- https://www.postfix.org/postconf.5.html#smtpd_forbid_bare_newline
2023-12-23 20:15:16 +01:00
Lafiel
008d78cc21
dovecot: add support store mailbox names on disk using UTF-8 2023-11-16 01:02:33 +03:00
Jean-Baptiste Giraudeau
84783b661e
Add tests for regex (PCRE) aliases 2023-09-28 16:13:00 +02:00
Jean-Baptiste Giraudeau
93221e4b25
Add support for regex (PCRE) aliases. 2023-09-05 14:58:10 +02:00
Naïm Favier
c63f6e7b05
docs: fix link 2023-07-21 23:55:54 +02:00
Bjørn Forsman
a3b03d1b5a Use umask for race-free permission setting
Without using umask there's a small time window where paths are world
readable. That is a bad idea to do for secret files (e.g. the dovecot
code path).
2023-07-17 18:22:16 +02:00
Antoine Eiche
69a4b7ad67 ldap: add an entry in the doc 2023-07-11 19:31:20 +00:00
Antoine Eiche
71b4c62d85 dovecot: fix a typo on userAttrs 2023-07-11 19:31:20 +00:00
Antoine Eiche
6775502be3 ldap: set assertions to forbid ldap and loginAccounts simultaneously 2023-07-11 19:31:20 +00:00
Antoine Eiche
7695c856f1 ldap: improve the documentation 2023-07-11 19:31:20 +00:00
Antoine Eiche
fb3210b932 ldap: do not write password to the Nix store 2023-07-11 19:31:20 +00:00
Antoine Eiche
33554e57ce Make the ldap test working
- The smtp/imap user name is now user@domain.tld
- Make the test_lookup function much more robust: it was now getting
  the correct file from the store.
2023-07-11 19:31:20 +00:00
Martin Weinelt
8b03ae5701 Create LDAP test
Sets up a declaratively configured OpenLDAP instance with users alice
and bob. They each own one email address,

First we test that postfix can communicate with LDAP and do the expected
lookups using the defined maps.

Then we use doveadm to make sure it can look up the two accounts.

Next we check the binding between account and mail address, by logging
in as alice and trying to send from bob@example.com, which alice is not
allowed to do. We expect postfix to reject the sender address here.

Finally we check mail delivery between alice and bob. Alice tries to
send a mail from alice@example.com to bob@example.com and bob then
checks whether it arrived in their mailbox.
2023-07-11 19:31:20 +00:00
Martin Weinelt
42e245b069 scripts/mail-check: allow passing the smtp username
Will be prefered over the from address when specified.
2023-07-11 19:31:20 +00:00
Martin Weinelt
08f077c5ca Add support for LDAP users
Allow configuring lookups for users and their mail addresses from an
LDAP directory. The LDAP username will be used as an accountname as
opposed to the email address used as the `loginName` for declarative
accounts. Mailbox for LDAP users will be stored below
`/var/vmail/ldap/<account>`.

Configuring domains is out of scope, since domains require further
configuration within the NixOS mailserver construct to set up all
related services accordingly.

Aliases can already be configured using `mailserver.forwards` but could
be supported using LDAP at a later point.
2023-07-11 19:31:20 +00:00
Nigel Bray
d460e9ff62 Fix and improve the setup guide 2023-07-05 21:53:56 +02:00
Florian Klink
0c1801b489 dovecot: add dovecot_pigeonhole to system packages
`sieve-test` can be used to test sieve scripts.

It's annoying to nix-shell it in, because it reads the dovecot global
config and might stumble over incompatible .so files (as has happened
to me).

Simply providing it in $PATH is easier.
2023-06-29 20:54:57 +02:00
Antoine Eiche
24128c3052 Release 23.05 2023-06-22 21:31:07 +02:00
Antoine Eiche
c4ec122aac readme: remove the announcement public key
Current maintainer no longer has it.
2023-06-11 17:10:19 +02:00
Antoine Eiche
131c48de9b Preserve the compatibility with nixos-22.11 2023-06-11 17:10:14 +02:00
26 changed files with 778 additions and 198 deletions

View file

@ -0,0 +1,17 @@
name: Build
on:
push:
branches:
- 'master'
jobs:
# deploy it upstream
deploy:
runs-on: docker
steps:
- name: "Deploy to Skynet"
uses: https://forgejo.skynet.ie/Skynet/actions-deploy-to-skynet@v2
with:
input: 'simple-nixos-mailserver'
token: ${{ secrets.API_TOKEN_FORGEJO }}

View file

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

View file

@ -8,26 +8,21 @@
For each NixOS release, we publish a branch. You then have to use the
SNM branch corresponding to your NixOS version.
* 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 22.05
- Use the [SNM branch `nixos-22.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.05)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/release-notes.html#nixos-22-05)
* 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 unstable
- Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm)
This is a very low volume list where new releases of SNM are announced, so you
can stay up to date with bug fixes and updates. All announcements are signed by
the gpg key with fingerprint
```
D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
```
can stay up to date with bug fixes and updates.
## Features
@ -76,71 +71,15 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
- 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 [Complete Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
## How to Backup
Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
Checkout the [Complete Backup Guide](https://nixos-mailserver.readthedocs.io/en/latest/backup-guide.html). Backups are easy with `SNM`.
For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html).
## Development
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) wiki page.
## Release notes
### nixos-20.03
- Rspamd is upgraded to 2.0 which deprecates the SQLite Bayes
backend. We then moved to the Redis backend (the default since
Rspamd 2.0). If you don't want to relearn the Redis backend from the
scratch, we could manually run
rspamadm statconvert --spam-db /var/lib/rspamd/bayes.spam.sqlite --ham-db /var/lib/rspamd/bayes.ham.sqlite -h 127.0.0.1:6379 --symbol-ham BAYES_HAM --symbol-spam BAYES_SPAM
See the [Rspamd migration
notes](https://rspamd.com/doc/migration.html#migration-to-rspamd-20)
and [this SNM Merge
Request](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/164)
for details.
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page.
## Contributors
See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master)
@ -155,6 +94,4 @@ See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mails
* Logo made with [Logomakr.com](https://logomakr.com)
[logo]: docs/logo.png

View file

@ -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 {
type = with types; listOf (enum cfg.domains);
example = ["example.com" "example2.com"];
@ -198,6 +207,157 @@ 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;
@ -304,7 +464,6 @@ in
type = let
loginAccount = mkOptionType {
name = "Login Account";
check = (account: builtins.elem account (builtins.attrNames cfg.loginAccounts));
};
in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount));
example = {
@ -414,6 +573,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 {
type = types.str;
default = ".";
@ -507,6 +674,19 @@ 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;
@ -787,6 +967,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 {
type = types.str;
default = cfg.fqdn;
@ -1101,6 +1296,7 @@ in
};
imports = [
./mail-server/assertions.nix
./mail-server/borgbackup.nix
./mail-server/debug.nix
./mail-server/rsnapshot.nix

View file

@ -24,12 +24,13 @@ have to be used. These can still be generated using `mkpasswd -m bcrypt`.
in {
services.radicale = {
enable = true;
config = ''
[auth]
type = htpasswd
htpasswd_filename = ${htpasswd}
htpasswd_encryption = bcrypt
'';
settings = {
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_server'] = "tls://${config.mailserver.fqdn}";
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';

View file

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

14
docs/ldap.rst Normal file
View 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``.

View file

@ -1,10 +1,28 @@
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

@ -48,18 +48,19 @@ Setup the server
~~~~~~~~~~~~~~~~
The following describes a server setup that is fairly complete. Even
though there are more possible options (see the ``default.nix`` file),
these should be the most common ones.
though there are more possible options (see the `NixOS Mailserver
options documentation <options.html>`_), these should be the most
common ones.
.. code:: nix
{ config, pkgs, ... }:
{
{ config, pkgs, ... }: {
imports = [
(builtins.fetchTarball {
# 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
# 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
sha256 = "0000000000000000000000000000000000000000000000000000";
})
];
@ -72,17 +73,19 @@ these should be the most common ones.
# A list of all login accounts. To create the password hashes, use
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
loginAccounts = {
"user1@example.com" = {
hashedPasswordFile = "/a/file/containing/a/hashed/password";
aliases = ["postmaster@example.com"];
};
"user2@example.com" = { ... };
"user1@example.com" = {
hashedPasswordFile = "/a/file/containing/a/hashed/password";
aliases = ["postmaster@example.com"];
};
"user2@example.com" = { ... };
};
# Use Let's Encrypt certificates. Note that this needs to set up a stripped
# 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

View file

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

View file

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

View file

@ -0,0 +1,18 @@
{ 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.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.fqdn}.directory}/fullchain.pem"
then "${config.security.acme.certs.${cfg.acmeCertificateName}.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.fqdn}.directory}/key.pem"
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
else throw "unknown certificate scheme";
passwordFiles = let
@ -45,4 +45,26 @@ 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,16 +22,18 @@ let
cfg = config.mailserver;
passwdDir = "/run/dovecot2";
passdbFile = "${passwdDir}/passdb";
passwdFile = "${passwdDir}/passwd";
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}"
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}"
+ (lib.optionalString (cfg.indexDir != null)
":INDEX=${cfg.indexDir}/%d/%n"
);
@ -58,6 +60,42 @@ 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}
@ -68,6 +106,9 @@ 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!"
@ -75,7 +116,7 @@ let
fi
done
cat <<EOF > ${passdbFile}
cat <<EOF > ${passwdFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts)}
@ -89,9 +130,6 @@ 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);
@ -99,6 +137,12 @@ 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 {
@ -109,6 +153,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 = {
enable = true;
enableImap = enableImap || enableImapSsl;
@ -125,8 +176,18 @@ in
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
protocols = lib.optional cfg.enableManageSieve "sieve";
sieveScripts = {
after = builtins.toFile "spam.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" ''
require "fileinto";
if header :is "X-Spam" "Yes" {
@ -134,8 +195,29 @@ 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 = ''
@ -220,7 +302,7 @@ in
passdb {
driver = passwd-file
args = ${passdbFile}
args = ${passwdFile}
}
userdb {
@ -229,6 +311,19 @@ 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
@ -244,28 +339,6 @@ 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
@ -294,17 +367,10 @@ in
systemd.services.dovecot2 = {
preStart = ''
${genPasswdScript}
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'
'';
'' + (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) {
description = "Optimize dovecot indices for fts_xapian";

View file

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

View file

@ -33,6 +33,11 @@ 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
@ -65,6 +70,10 @@ 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}")
@ -94,6 +103,7 @@ 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.
@ -123,6 +133,7 @@ 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 =
{
@ -133,21 +144,73 @@ 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";
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_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;
@ -170,7 +233,16 @@ in
virtual_gid_maps = "static:5000";
virtual_mailbox_base = mailDirectory;
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";
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
lmtp_destination_recipient_limit = "1";
@ -202,9 +274,6 @@ 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";
@ -237,6 +306,9 @@ 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;
@ -253,7 +325,7 @@ in
privileged = true;
chroot = false;
command = "spawn";
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
args = [ "user=nobody" "argv=${pkgs.spf-engine}/bin/policyd-spf" "${policyd-spf}"];
};
"submission-header-cleanup" = {
type = "unix";

View file

@ -30,7 +30,7 @@ in
inherit debug;
locals = {
"milter_headers.conf" = { text = ''
extended_spam_headers = yes;
extended_spam_headers = true;
''; };
"redis.conf" = { text = ''
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
@ -69,14 +69,6 @@ in
''; };
};
overrides = {
"milter_headers.conf" = {
text = ''
extended_spam_headers = true;
'';
};
};
workers.rspamd_proxy = {
type = "rspamd_proxy";
bindSockets = [{

View file

@ -64,6 +64,8 @@ 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,6 +34,9 @@ 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,6 +27,7 @@ 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, 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))
message = "\n".join([
"From: {from_addr}",
@ -30,7 +30,7 @@ def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, star
if starttls:
smtp.starttls()
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)
return
@ -141,6 +141,7 @@ 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,
@ -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-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,7 +501,6 @@ 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")
@ -509,7 +508,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 -i warning >&2"
"journalctl -u dovecot2 |grep -v 'Expunged message reappeared, giving a new UID'| grep -v 'FTS Xapian: Box is empty' | grep -i warning >&2"
)
'';
}

View file

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

183
tests/ldap.nix Normal file
View 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"
]))
'';
}

View file

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