Compare commits

..

1 commit

Author SHA1 Message Date
Antoine Eiche
0a2619f0ca docs: some minor changes 2023-05-23 20:54:17 +00:00
30 changed files with 243 additions and 828 deletions

View file

@ -1,17 +0,0 @@
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 // { desc = prJobsets // {
"master" = mkFlakeJobset "master"; "master" = mkFlakeJobset "master";
"nixos-23.11" = mkFlakeJobset "nixos-23.11"; "nixos-22.05" = mkFlakeJobset "nixos-22.05";
"nixos-24.05" = mkFlakeJobset "nixos-24.05"; "nixos-22.11" = mkFlakeJobset "nixos-22.11";
}; };
log = { log = {

View file

@ -8,21 +8,26 @@
For each NixOS release, we publish a branch. You then have to use the For each NixOS release, we publish a branch. You then have to use the
SNM branch corresponding to your NixOS version. SNM branch corresponding to your NixOS version.
* For NixOS 24.05 * For NixOS 22.11
- Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05) - 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-24.05/) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/release-notes.html#nixos-22-11)
* For NixOS 23.11 * For NixOS 22.05
- Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11) - 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-23.11/) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/release-notes.html#nixos-23-11) - [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.05/release-notes.html#nixos-22-05)
* For NixOS unstable * For NixOS unstable
- Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master) - Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/) - [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm) [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 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. 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
```
## Features ## Features
@ -71,15 +76,71 @@ can stay up to date with bug fixes and updates.
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/) - Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
- Join the Libera Chat IRC channel `#nixos-mailserver` - Join the Libera Chat IRC channel `#nixos-mailserver`
### Quick Start
```nix
{ config, pkgs, ... }:
let release = "nixos-21.11";
in {
imports = [
(builtins.fetchTarball {
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz";
# This hash needs to be updated
sha256 = "0000000000000000000000000000000000000000000000000000";
})
];
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
loginAccounts = {
"user1@example.com" = {
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' > /hashed/password/file/location
hashedPasswordFile = "/hashed/password/file/location";
aliases = [
"info@example.com"
"postmaster@example.com"
"postmaster@example2.com"
];
};
};
};
}
```
For a complete list of options, see `default.nix`.
## How to Set Up a 10/10 Mail Server Guide ## How to Set Up a 10/10 Mail Server Guide
Check out the [Complete Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation. ## How to Backup
For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html). Checkout the [Complete Backup Guide](https://nixos-mailserver.readthedocs.io/en/latest/backup-guide.html). Backups are easy with `SNM`.
## Development ## Development
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page. 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.
## Contributors ## Contributors
See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master) See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/graphs/master)
@ -94,4 +155,6 @@ See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mails
* Logo made with [Logomakr.com](https://logomakr.com) * Logo made with [Logomakr.com](https://logomakr.com)
[logo]: docs/logo.png [logo]: docs/logo.png

View file

@ -48,11 +48,7 @@ in
type = types.listOf types.str; type = types.listOf types.str;
example = [ "imap.example.com" "pop3.example.com" ]; example = [ "imap.example.com" "pop3.example.com" ];
default = []; default = [];
description = '' description = "Secondary domains and subdomains for which it is necessary to generate a certificate.";
({option}`mailserver.certificateScheme` == `acme-nginx`)
Secondary domains and subdomains for which it is necessary to generate a certificate.
'';
}; };
messageSizeLimit = mkOption { messageSizeLimit = mkOption {
@ -111,15 +107,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 { catchAll = mkOption {
type = with types; listOf (enum cfg.domains); type = with types; listOf (enum cfg.domains);
example = ["example.com" "example2.com"]; example = ["example.com" "example2.com"];
@ -207,157 +194,6 @@ in
default = {}; default = {};
}; };
ldap = {
enable = mkEnableOption "LDAP support";
uris = mkOption {
type = types.listOf types.str;
example = literalExpression ''
[
"ldaps://ldap1.example.com"
"ldaps://ldap2.example.com"
]
'';
description = ''
URIs where your LDAP server can be reached
'';
};
startTls = mkOption {
type = types.bool;
default = false;
description = ''
Whether to enable StartTLS upon connection to the server.
'';
};
tlsCAFile = mkOption {
type = types.path;
default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
description = ''
Certifificate trust anchors used to verify the LDAP server certificate.
'';
};
bind = {
dn = mkOption {
type = types.str;
example = "cn=mail,ou=accounts,dc=example,dc=com";
description = ''
Distinguished name used by the mail server to do lookups
against the LDAP servers.
'';
};
passwordFile = mkOption {
type = types.str;
example = "/run/my-secret";
description = ''
A file containing the password required to authenticate against the LDAP servers.
'';
};
};
searchBase = mkOption {
type = types.str;
example = "ou=people,ou=accounts,dc=example,dc=com";
description = ''
Base DN at below which to search for users accounts.
'';
};
searchScope = mkOption {
type = types.enum [ "sub" "base" "one" ];
default = "sub";
description = ''
Search scope below which users accounts are looked for.
'';
};
dovecot = {
userAttrs = mkOption {
type = types.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 { indexDir = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = null; default = null;
@ -464,6 +300,7 @@ in
type = let type = let
loginAccount = mkOptionType { loginAccount = mkOptionType {
name = "Login Account"; name = "Login Account";
check = (account: builtins.elem account (builtins.attrNames cfg.loginAccounts));
}; };
in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount)); in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount));
example = { example = {
@ -573,14 +410,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 { hierarchySeparator = mkOption {
type = types.str; type = types.str;
default = "."; default = ".";
@ -619,26 +448,19 @@ in
}; };
}; };
certificateScheme = let certificateScheme = mkOption {
schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ]; type = types.enum [ 1 2 3 ];
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))}\"'." default = 2;
(builtins.elemAt schemes (i - 1));
in mkOption {
type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes);
default = "selfsigned";
description = '' description = ''
The scheme to use for managing TLS certificates: Certificate Files. There are three options for these.
1. `manual`: you specify locations via {option}`mailserver.certificateFile` and 1) You specify locations and manually copy certificates there.
{option}`mailserver.keyFile` and manually copy certificates there. 2) You let the server create new (self signed) certificates on the fly.
2. `selfsigned`: you let the server create new (self-signed) certificates on the fly. 3) You let the server create a certificate via `Let's Encrypt`. Note that
3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org) this implies that a stripped down webserver has to be started. This also
via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for implies that the FQDN must be set as an `A` record to point to the IP of
{option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly the server. In particular port 80 on the server will be opened. For details
configured to point to your server (see the [setup guide](setup-guide.rst) for more information). on how to set up the domain records, see the guide in the readme.
4. `acme`: you already have an ACME certificate set up (for example, you're already running a TLS-enabled
Nginx server on the FQDN). This is better than `manual` because the appropriate services will be reloaded
when the certificate is renewed.
''; '';
}; };
@ -646,9 +468,9 @@ in
type = types.path; type = types.path;
example = "/root/mail-server.crt"; example = "/root/mail-server.crt";
description = '' description = ''
({option}`mailserver.certificateScheme` == `manual`)
Location of the certificate. Location of the certificate.
This option is only used when the `certificateScheme` is `1`.
''; '';
}; };
@ -656,9 +478,9 @@ in
type = types.path; type = types.path;
example = "/root/mail-server.key"; example = "/root/mail-server.key";
description = '' description = ''
({option}`mailserver.certificateScheme` == `manual`)
Location of the key file. Location of the key file.
This option is only used when the `certificateScheme` is `1`.
''; '';
}; };
@ -666,24 +488,11 @@ in
type = types.path; type = types.path;
default = "/var/certs"; default = "/var/certs";
description = '' description = ''
({option}`mailserver.certificateScheme` == `selfsigned`) This is the folder where the certificate will be created. The name is
This is the folder where the self-signed certificate will be created. The name is
hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the hardcoded to "cert-DOMAIN.pem" and "key-DOMAIN.pem" and the
certificate is valid for 10 years. certificate is valid for 10 years.
'';
};
acmeCertificateName = mkOption { This option is only used when the `certificateScheme` is `2`.
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.
''; '';
}; };
@ -967,21 +776,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 { sendingFqdn = mkOption {
type = types.str; type = types.str;
default = cfg.fqdn; default = cfg.fqdn;
@ -1296,7 +1090,6 @@ in
}; };
imports = [ imports = [
./mail-server/assertions.nix
./mail-server/borgbackup.nix ./mail-server/borgbackup.nix
./mail-server/debug.nix ./mail-server/debug.nix
./mail-server/rsnapshot.nix ./mail-server/rsnapshot.nix

View file

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

View file

@ -20,7 +20,7 @@ servers may require more work.
extraConfig = '' extraConfig = ''
# starttls needed for authentication, so the fqdn required to match # starttls needed for authentication, so the fqdn required to match
# the certificate # the certificate
$config['smtp_host'] = "tls://${config.mailserver.fqdn}"; $config['smtp_server'] = "tls://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u"; $config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p"; $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 `RFC6186 <https://www.rfc-editor.org/rfc/rfc6186>`_ allows supporting email clients to automatically discover SMTP / IMAP addresses
of the mailserver. For that, the following records are required: of the mailserver. For that, the following records are required:
================= ==== ==== ======== ====== ==== ================= ================ ==== ==== ======== ====== ==== =================
Record TTL Type Priority Weight Port Value Record TTL Type Priority Weight Port Value
================= ==== ==== ======== ====== ==== ================= ================ ==== ==== ======== ====== ==== =================
_submission._tcp 3600 SRV 5 0 587 mail.example.com. _submission._tcp 3600 SRV 5 0 587 mail.example.com.
_submissions._tcp 3600 SRV 5 0 465 mail.example.com. _imap._tcp 3600 SRV 5 0 143 mail.example.com.
_imap._tcp 3600 SRV 5 0 143 mail.example.com. _imaps._tcp 3600 SRV 5 0 993 mail.example.com.
_imaps._tcp 3600 SRV 5 0 993 mail.example.com. ================ ==== ==== ======== ====== ==== =================
================= ==== ==== ======== ====== ==== =================
Please note that only a few MUAs currently implement this. For vendor-specific Please note that only a few MUAs currently implement this. For vendor-specific
discovery mechanisms `automx <https://github.com/rseichter/automx2>`_ can be used instead. discovery mechanisms `automx <https://github.com/rseichter/automx2>`_ can be used instead.

View file

@ -18,7 +18,7 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'NixOS Mailserver' project = 'NixOS Mailserver'
copyright = '2022, NixOS Mailserver Contributors' copyright = '2023, NixOS Mailserver Contributors'
author = 'NixOS Mailserver Contributors' author = 'NixOS Mailserver Contributors'

View file

@ -30,7 +30,6 @@ Welcome to NixOS Mailserver's documentation!
fts fts
flakes flakes
autodiscovery autodiscovery
ldap
Indices and tables 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 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 NixOS 22.11
----------- -----------
- Allow Rspamd to send DMARC reporting - Allow Rspamd to send dmarc reporting
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/244>`__) (`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/244>`__)
NixOS 22.05 NixOS 22.05

View file

@ -48,19 +48,18 @@ Setup the server
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
The following describes a server setup that is fairly complete. Even The following describes a server setup that is fairly complete. Even
though there are more possible options (see the `NixOS Mailserver though there are more possible options (see the ``default.nix`` file),
options documentation <options.html>`_), these should be the most these should be the most common ones.
common ones.
.. code:: nix .. code:: nix
{ config, pkgs, ... }: { { config, pkgs, ... }:
{
imports = [ imports = [
(builtins.fetchTarball { (builtins.fetchTarball {
# Pick a release version you are interested in and set its hash, e.g. # Pick a commit from the branch you are interested in
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz"; url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz";
# To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command: # And set its hash
# release="nixos-23.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
sha256 = "0000000000000000000000000000000000000000000000000000"; sha256 = "0000000000000000000000000000000000000000000000000000";
}) })
]; ];
@ -73,19 +72,17 @@ common ones.
# A list of all login accounts. To create the password hashes, use # A list of all login accounts. To create the password hashes, use
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' # nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
loginAccounts = { loginAccounts = {
"user1@example.com" = { "user1@example.com" = {
hashedPasswordFile = "/a/file/containing/a/hashed/password"; hashedPasswordFile = "/a/file/containing/a/hashed/password";
aliases = ["postmaster@example.com"]; aliases = ["postmaster@example.com"];
}; };
"user2@example.com" = { ... }; "user2@example.com" = { ... };
}; };
# Use Let's Encrypt certificates. Note that this needs to set up a stripped # Use Let's Encrypt certificates. Note that this needs to set up a stripped
# down nginx and opens port 80. # down nginx and opens port 80.
certificateScheme = "acme-nginx"; certificateScheme = 3;
}; };
security.acme.acceptTerms = true;
security.acme.defaults.email = "security@example.com";
} }
After a ``nixos-rebuild switch`` your server should be running all After a ``nixos-rebuild switch`` your server should be running all

View file

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

View file

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

View file

@ -1,18 +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.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

@ -21,22 +21,22 @@ let
in in
{ {
# cert :: PATH # cert :: PATH
certificatePath = if cfg.certificateScheme == "manual" certificatePath = if cfg.certificateScheme == 1
then cfg.certificateFile then cfg.certificateFile
else if cfg.certificateScheme == "selfsigned" else if cfg.certificateScheme == 2
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" else if cfg.certificateScheme == 3
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"; else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
# key :: PATH # key :: PATH
keyPath = if cfg.certificateScheme == "manual" keyPath = if cfg.certificateScheme == 1
then cfg.keyFile then cfg.keyFile
else if cfg.certificateScheme == "selfsigned" else if cfg.certificateScheme == 2
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" else if cfg.certificateScheme == 3
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"; else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
passwordFiles = let passwordFiles = let
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
@ -45,26 +45,4 @@ in
if value.hashedPasswordFile == null then if value.hashedPasswordFile == null then
builtins.toString (mkHashFile name value.hashedPassword) builtins.toString (mkHashFile name value.hashedPassword)
else value.hashedPasswordFile) cfg.loginAccounts; else value.hashedPasswordFile) cfg.loginAccounts;
# Appends the LDAP bind password to files to avoid writing this
# password into the Nix store.
appendLdapBindPwd = {
name, file, prefix, 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; cfg = config.mailserver;
passwdDir = "/run/dovecot2"; passwdDir = "/run/dovecot2";
passwdFile = "${passwdDir}/passwd"; passdbFile = "${passwdDir}/passdb";
userdbFile = "${passwdDir}/userdb"; userdbFile = "${passwdDir}/userdb";
# This file contains the ldap bind password
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
bool2int = x: if x then "1" else "0"; bool2int = x: if x then "1" else "0";
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
# maildir in format "/${domain}/${user}" # maildir in format "/${domain}/${user}"
dovecotMaildir = dovecotMaildir =
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}" "maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}"
+ (lib.optionalString (cfg.indexDir != null) + (lib.optionalString (cfg.indexDir != null)
":INDEX=${cfg.indexDir}/%d/%n" ":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" '' genPasswdScript = pkgs.writeScript "generate-password-file" ''
#!${pkgs.stdenv.shell} #!${pkgs.stdenv.shell}
@ -106,9 +68,6 @@ let
chmod 755 "${passwdDir}" chmod 755 "${passwdDir}"
fi fi
# Prevent world-readable password files, even temporarily.
umask 077
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
if [ ! -f "$f" ]; then if [ ! -f "$f" ]; then
echo "Expected password hash file $f does not exist!" echo "Expected password hash file $f does not exist!"
@ -116,7 +75,7 @@ let
fi fi
done done
cat <<EOF > ${passwdFile} cat <<EOF > ${passdbFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts)} ) cfg.loginAccounts)}
@ -130,6 +89,9 @@ let
else "") else "")
) cfg.loginAccounts)} ) cfg.loginAccounts)}
EOF EOF
chmod 600 ${passdbFile}
chmod 600 ${userdbFile}
''; '';
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes); junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
@ -137,12 +99,6 @@ let
# The assertion garantees there is exactly one Junk mailbox. # The assertion garantees there is exactly one Junk mailbox.
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else ""; junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
mkLdapSearchScope = scope: (
if scope == "sub" then "subtree"
else if scope == "one" then "onelevel"
else scope
);
in in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
@ -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 = { services.dovecot2 = {
enable = true; enable = true;
enableImap = enableImap || enableImapSsl; enableImap = enableImap || enableImapSsl;
@ -176,18 +125,8 @@ in
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ]; mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
protocols = lib.optional cfg.enableManageSieve "sieve"; protocols = lib.optional cfg.enableManageSieve "sieve";
pluginSettings = { sieveScripts = {
sieve = "file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve"; after = builtins.toFile "spam.sieve" ''
sieve_default = "file:${cfg.sieveDirectory}/%u/default.sieve";
sieve_default_name = "default";
};
sieve = {
extensions = [
"fileinto"
];
scripts.after = builtins.toFile "spam.sieve" ''
require "fileinto"; require "fileinto";
if header :is "X-Spam" "Yes" { if header :is "X-Spam" "Yes" {
@ -195,29 +134,8 @@ in
stop; 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; mailboxes = cfg.mailboxes;
extraConfig = '' extraConfig = ''
@ -302,7 +220,7 @@ in
passdb { passdb {
driver = passwd-file driver = passwd-file
args = ${passwdFile} args = ${passdbFile}
} }
userdb { userdb {
@ -311,19 +229,6 @@ in
default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory} default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}
} }
${lib.optionalString cfg.ldap.enable ''
passdb {
driver = ldap
args = ${ldapConfFile}
}
userdb {
driver = ldap
args = ${ldapConfFile}
default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID}
}
''}
service auth { service auth {
unix_listener auth { unix_listener auth {
mode = 0660 mode = 0660
@ -339,6 +244,28 @@ in
inbox = yes 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 '' ${lib.optionalString cfg.fullTextSearch.enable ''
plugin { plugin {
plugin = fts fts_xapian plugin = fts fts_xapian
@ -367,10 +294,17 @@ in
systemd.services.dovecot2 = { systemd.services.dovecot2 = {
preStart = '' preStart = ''
${genPasswdScript} ${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) { systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
description = "Optimize dovecot indices for fts_xapian"; description = "Optimize dovecot indices for fts_xapian";

View file

@ -23,6 +23,6 @@ in
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
dovecot opendkim openssh postfix rspamd dovecot opendkim openssh postfix rspamd
] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []); ] ++ (if certificateScheme == 2 then [ openssl ] else []);
}; };
} }

View file

@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, lib, ... }: { config, pkgs, lib, ... }:
let let
cfg = config.mailserver; cfg = config.mailserver;
@ -31,7 +31,7 @@ in
++ lib.optional enablePop3 110 ++ lib.optional enablePop3 110
++ lib.optional enablePop3Ssl 995 ++ lib.optional enablePop3Ssl 995
++ lib.optional enableManageSieve 4190 ++ lib.optional enableManageSieve 4190
++ lib.optional (certificateScheme == "acme-nginx") 80; ++ lib.optional (certificateScheme == 3) 80;
}; };
}; };
} }

View file

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

View file

@ -33,11 +33,6 @@ let
let to = name; let to = name;
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name)) in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
cfg.loginAccounts)); cfg.loginAccounts));
regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
(name: value:
let to = name;
in map (from: {"${from}" = to;}) value.aliasesRegexp)
cfg.loginAccounts));
# catchAllPostfix :: Map String [String] # catchAllPostfix :: Map String [String]
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
@ -70,10 +65,6 @@ let
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]); content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
in builtins.toFile "valias" content; in builtins.toFile "valias" content;
regex_valiases_file = let
content = lookupTableToString regex_valiases_postfix;
in builtins.toFile "regex_valias" content;
# denied_recipients_postfix :: [ String ] # denied_recipients_postfix :: [ String ]
denied_recipients_postfix = (map denied_recipients_postfix = (map
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
@ -103,7 +94,6 @@ let
# every alias is owned (uniquely) by its user. # every alias is owned (uniquely) by its user.
# The user's own address is already in all_valiases_postfix. # The user's own address is already in all_valiases_postfix.
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix); vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (lookupTableToString regex_valiases_postfix);
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ('' submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
# Removes sensitive headers from mails handed in via the submission port. # Removes sensitive headers from mails handed in via the submission port.
@ -133,7 +123,6 @@ let
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig; policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
submissionOptions = submissionOptions =
{ {
@ -144,73 +133,21 @@ let
smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = "$myhostname"; smtpd_sasl_local_domain = "$myhostname";
smtpd_client_restrictions = "permit_sasl_authenticated,reject"; smtpd_client_restrictions = "permit_sasl_authenticated,reject";
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${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_sender_restrictions = "reject_sender_login_mismatch";
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
cleanup_service_name = "submission-header-cleanup"; cleanup_service_name = "submission-header-cleanup";
}; };
commonLdapConfig = ''
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
version = 3
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
tls_require_cert = yes
search_base = ${cfg.ldap.searchBase}
scope = ${cfg.ldap.searchScope}
bind = yes
bind_dn = ${cfg.ldap.bind.dn}
'';
ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" ''
${commonLdapConfig}
query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.mailAttribute}
'';
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
appendPwdInSenderLoginMap = appendLdapBindPwd {
name = "ldap-sender-login-map";
file = ldapSenderLoginMap;
prefix = "bind_pw = ";
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapSenderLoginMapFile;
};
ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" ''
${commonLdapConfig}
query_filter = ${cfg.ldap.postfix.filter}
result_attribute = ${cfg.ldap.postfix.uidAttribute}
'';
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
name = "ldap-virtual-mailbox-map";
file = ldapVirtualMailboxMap;
prefix = "bind_pw = ";
passwordFile = cfg.ldap.bind.passwordFile;
destination = ldapVirtualMailboxMapFile;
};
in in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
preStart = ''
${appendPwdInVirtualMailboxMap}
${appendPwdInSenderLoginMap}
'';
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
};
services.postfix = { services.postfix = {
enable = true; enable = true;
hostname = "${sendingFqdn}"; hostname = "${sendingFqdn}";
networksStyle = "host"; networksStyle = "host";
mapFiles."valias" = valiases_file; mapFiles."valias" = valiases_file;
mapFiles."regex_valias" = regex_valiases_file;
mapFiles."vaccounts" = vaccounts_file; mapFiles."vaccounts" = vaccounts_file;
mapFiles."regex_vaccounts" = regex_vaccounts_file;
mapFiles."denied_recipients" = denied_recipients_file; mapFiles."denied_recipients" = denied_recipients_file;
mapFiles."reject_senders" = reject_senders_file; mapFiles."reject_senders" = reject_senders_file;
mapFiles."reject_recipients" = reject_recipients_file; mapFiles."reject_recipients" = reject_recipients_file;
@ -233,16 +170,7 @@ in
virtual_gid_maps = "static:5000"; virtual_gid_maps = "static:5000";
virtual_mailbox_base = mailDirectory; virtual_mailbox_base = mailDirectory;
virtual_mailbox_domains = vhosts_file; virtual_mailbox_domains = vhosts_file;
virtual_mailbox_maps = [ virtual_mailbox_maps = mappedFile "valias";
(mappedFile "valias")
] ++ lib.optionals (cfg.ldap.enable) [
"ldap:${ldapVirtualMailboxMapFile}"
] ++ lib.optionals (regex_valiases_postfix != {}) [
(mappedRegexFile "regex_valias")
];
virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [
(mappedRegexFile "regex_valias")
]);
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
lmtp_destination_recipient_limit = "1"; lmtp_destination_recipient_limit = "1";
@ -274,6 +202,9 @@ in
# Submission by mail clients is handled in submissionOptions # Submission by mail clients is handled in submissionOptions
smtpd_tls_security_level = "may"; smtpd_tls_security_level = "may";
# strong might suffice and is computationally less expensive
smtpd_tls_eecdh_grade = "ultra";
# Disable obselete protocols # Disable obselete protocols
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3"; smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
smtp_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_protocol = "6";
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}"; milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
# Fix for https://www.postfix.org/smtp-smuggling.html
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;
smtpd_forbid_bare_newline_exclusions = "$mynetworks";
}; };
submissionOptions = submissionOptions; submissionOptions = submissionOptions;
@ -325,7 +253,7 @@ in
privileged = true; privileged = true;
chroot = false; chroot = false;
command = "spawn"; 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" = { "submission-header-cleanup" = {
type = "unix"; type = "unix";

View file

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

View file

@ -19,9 +19,9 @@
let let
cfg = config.mailserver; cfg = config.mailserver;
certificatesDeps = certificatesDeps =
if cfg.certificateScheme == "manual" then if cfg.certificateScheme == 1 then
[] []
else if cfg.certificateScheme == "selfsigned" then else if cfg.certificateScheme == 2 then
[ "mailserver-selfsigned-certificate.service" ] [ "mailserver-selfsigned-certificate.service" ]
else else
[ "acme-finished-${cfg.fqdn}.target" ]; [ "acme-finished-${cfg.fqdn}.target" ];
@ -29,7 +29,7 @@ in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
# Create self signed certificate # Create self signed certificate
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") { systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == 2) {
after = [ "local-fs.target" ]; after = [ "local-fs.target" ];
script = '' script = ''
# Create certificates if they do not exist yet # Create certificates if they do not exist yet
@ -64,8 +64,6 @@ in
in '' in ''
# Create mail directory and set permissions. See # Create mail directory and set permissions. See
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>. # <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
# Prevent world-readable paths, even temporarily.
umask 007
mkdir -p ${directories} mkdir -p ${directories}
chgrp "${vmailGroupName}" ${directories} chgrp "${vmailGroupName}" ${directories}
chmod 02770 ${directories} chmod 02770 ${directories}

View file

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

View file

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

View file

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

View file

@ -501,6 +501,7 @@ pkgs.nixosTest {
with subtest("dmarc reporting"): with subtest("dmarc reporting"):
server.systemctl("start rspamd-dmarc-reporter.service") 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"): with subtest("no warnings or errors"):
server.fail("journalctl -u postfix | grep -i error >&2") server.fail("journalctl -u postfix | grep -i error >&2")
@ -508,7 +509,7 @@ pkgs.nixosTest {
server.fail("journalctl -u dovecot2 | grep -i error >&2") server.fail("journalctl -u dovecot2 | grep -i error >&2")
# harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html # harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html
server.fail( 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 = { mailserver = {
enable = true; enable = true;
fqdn = "mail.example.com"; fqdn = "mail.example.com";
domains = [ "example.com" "domain.com" ]; domains = [ "example.com" ];
localDnsResolver = false; localDnsResolver = false;
loginAccounts = { loginAccounts = {
@ -64,7 +64,6 @@ pkgs.nixosTest {
}; };
"user2@example.com" = { "user2@example.com" = {
hashedPasswordFile = hashedPasswordFile; hashedPasswordFile = hashedPasswordFile;
aliasesRegexp = [''/^user2.*@domain\.com$/''];
}; };
"send-only@example.com" = { "send-only@example.com" = {
hashedPasswordFile = hashPassword "send-only"; hashedPasswordFile = hashPassword "send-only";
@ -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"): with subtest("vmail gid is set correctly"):
machine.succeed("getent group vmail | grep 5000") 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 ]" "set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
) )
machine.succeed( 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"): 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,8 +30,6 @@ let
}; };
services.dnsmasq = { services.dnsmasq = {
enable = true; enable = true;
# Fixme: once nixos-22.11 has been removed, could be replaced by
# settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
extraConfig = '' extraConfig = ''
mx-host=domain1.com,domain1,10 mx-host=domain1.com,domain1,10
mx-host=domain2.com,domain2,10 mx-host=domain2.com,domain2,10