Compare commits

..

No commits in common. "master" and "irc" have entirely different histories.
master ... irc

47 changed files with 898 additions and 1756 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

@ -3,11 +3,11 @@ hydra-pr:
- merge_requests - merge_requests
image: nixos/nix image: nixos/nix
script: script:
- nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}' - nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver ${CI_MERGE_REQUEST_IID}
hydra-master: hydra-master:
only: only:
- master - master
image: nixos/nix image: nixos/nix
script: script:
- nix-shell -I nixpkgs=channel:nixos-22.05 -p hydra-cli --run 'hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master' - nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master

View file

@ -1,4 +1,4 @@
{ nixpkgs, pulls, ... }: { nixpkgs, declInput, pulls }:
let let
pkgs = import nixpkgs {}; pkgs = import nixpkgs {};
@ -8,37 +8,85 @@ let
{ enabled = 1; { enabled = 1;
hidden = false; hidden = false;
description = "PR ${num}: ${info.title}"; description = "PR ${num}: ${info.title}";
nixexprinput = "snm";
nixexprpath = ".hydra/default.nix";
checkinterval = 30; checkinterval = 30;
schedulingshares = 20; schedulingshares = 20;
enableemail = false; enableemail = false;
emailoverride = ""; emailoverride = "";
keepnr = 1; keepnr = 1;
type = 1; type = 0;
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head"; inputs = {
snm = {
type = "git";
value = "${info.target_repo_url} merge-requests/${info.iid}/head";
emailresponsible = false;
};
};
} }
) prs; ) prs;
mkFlakeJobset = branch: {
description = "Build ${branch} branch of Simple NixOS MailServer"; desc = prJobsets // {
master = {
description = "Build master branch of Simple NixOS MailServer";
checkinterval = "60"; checkinterval = "60";
enabled = "1"; enabled = "1";
nixexprinput = "snm";
nixexprpath = ".hydra/default.nix";
schedulingshares = 100; schedulingshares = 100;
enableemail = false; enableemail = false;
emailoverride = ""; emailoverride = "";
keepnr = 3; keepnr = 3;
hidden = false; hidden = false;
type = 1; type = 0;
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/${branch}"; inputs = {
snm = {
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver master";
type = "git";
emailresponsible = false;
};
};
};
"nixos-20.03" = {
description = "Build the nixos-20.03 branch of Simple NixOS MailServer";
checkinterval = "60";
enabled = "1";
nixexprinput = "snm";
nixexprpath = ".hydra/default.nix";
schedulingshares = 100;
enableemail = false;
emailoverride = "";
keepnr = 3;
hidden = false;
type = 0;
inputs = {
snm = {
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver nixos-20.03";
type = "git";
emailresponsible = false;
};
};
};
"nixos-20.09" = {
description = "Build the nixos-20.09 branch of Simple NixOS MailServer";
checkinterval = "60";
enabled = "1";
nixexprinput = "snm";
nixexprpath = ".hydra/default.nix";
schedulingshares = 100;
enableemail = false;
emailoverride = "";
keepnr = 3;
hidden = false;
type = 0;
inputs = {
snm = {
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver nixos-20.09";
type = "git";
emailresponsible = false;
};
}; };
desc = prJobsets // {
"master" = mkFlakeJobset "master";
"nixos-23.11" = mkFlakeJobset "nixos-23.11";
"nixos-24.05" = mkFlakeJobset "nixos-24.05";
}; };
log = {
pulls = prs;
jobsets = desc;
}; };
in { in {
@ -46,10 +94,5 @@ in {
cat >$out <<EOF cat >$out <<EOF
${builtins.toJSON desc} ${builtins.toJSON desc}
EOF EOF
# This is to get nice .jobsets build logs on Hydra
cat >tmp <<EOF
${builtins.toJSON log}
EOF
${pkgs.jq}/bin/jq . tmp
''; '';
} }

View file

@ -1,29 +0,0 @@
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
build:
os: ubuntu-22.04
tools:
python: "3"
apt_packages:
- nix
- proot
jobs:
pre_install:
- mkdir -p ~/.nix ~/.config/nix
- echo "experimental-features = nix-command flakes" > ~/.config/nix/nix.conf
- proot -b ~/.nix:/nix /bin/sh -c "nix build -L .#optionsDoc && cp -v result docs/options.md"
sphinx:
configuration: docs/conf.py
formats:
- pdf
- epub
python:
install:
- requirements: docs/requirements.txt

View file

@ -8,21 +8,28 @@
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 20.09
- Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05) - Use the [SNM branch `nixos-20.09`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.09)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/) - [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/)
- [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-20.09/release-notes.html#nixos-20-09)
* For NixOS 23.11 * For NixOS 20.03
- Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11) - Use the [SNM branch `nixos-20.03`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.03)
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/) - [Release notes](#nixos-2003)
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/release-notes.html#nixos-23-11)
* 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/)
- This branch is currently supporting the NixOS release 20.09 but
we could remove this support on any NixOS unstable breaking
change.
[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
@ -69,17 +76,73 @@ can stay up to date with bug fixes and updates.
### Get in touch ### Get in touch
- 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 Freenode IRC channel `#nixos-mailserver`
### Quick Start
```nix
{ config, pkgs, ... }:
let release = "nixos-20.09";
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 run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 > /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 +157,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]: logo/logo.png

View file

@ -44,17 +44,6 @@ in
description = "The domains that this mail server serves."; description = "The domains that this mail server serves.";
}; };
certificateDomains = mkOption {
type = types.listOf types.str;
example = [ "imap.example.com" "pop3.example.com" ];
default = [];
description = ''
({option}`mailserver.certificateScheme` == `acme-nginx`)
Secondary domains and subdomains for which it is necessary to generate a certificate.
'';
};
messageSizeLimit = mkOption { messageSizeLimit = mkOption {
type = types.int; type = types.int;
example = 52428800; example = 52428800;
@ -76,14 +65,14 @@ in
default = null; default = null;
example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
description = '' description = ''
The user's hashed password. Use `mkpasswd` as follows The user's hashed password. Use `htpasswd` as follows
``` ```
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
``` ```
Warning: this is stored in plaintext in the Nix store! Warning: this is stored in plaintext in the Nix store!
Use {option}`mailserver.loginAccounts.<name>.hashedPasswordFile` instead. Use `hashedPasswordFile` instead.
''; '';
}; };
@ -92,10 +81,10 @@ in
default = null; default = null;
example = "/run/keys/user1-passwordhash"; example = "/run/keys/user1-passwordhash";
description = '' description = ''
A file containing the user's hashed password. Use `mkpasswd` as follows A file containing the user's hashed password. Use `htpasswd` as follows
``` ```
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
``` ```
''; '';
}; };
@ -111,15 +100,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"];
@ -169,7 +149,7 @@ in
description = '' description = ''
Specifies if the account should be a send-only account. Specifies if the account should be a send-only account.
Emails sent to send-only accounts will be rejected from Emails sent to send-only accounts will be rejected from
unauthorized senders with the `sendOnlyRejectMessage` unauthorized senders with the sendOnlyRejectMessage
stating the reason. stating the reason.
''; '';
}; };
@ -197,190 +177,27 @@ in
}; };
description = '' description = ''
The login account of the domain. Every account is mapped to a unix user, The login account of the domain. Every account is mapped to a unix user,
e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as e.g. `user1@example.com`. To generate the passwords use `htpasswd` as
follows follows
``` ```
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
``` ```
''; '';
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 {
type = types.nullOr types.str;
default = null;
description = ''
Folder to store search indices. If null, indices are stored
along with email, which could not necessarily be desirable,
especially when {option}`mailserver.fullTextSearch.enable` is `true` since
indices it creates are voluminous and do not need to be backed
up.
Be careful when changing this option value since all indices
would be recreated at the new location (and clients would need
to resynchronize).
Note the some variables can be used in the file path. See
https://doc.dovecot.org/configuration_manual/mail_location/#variables
for details.
'';
example = "/var/lib/dovecot/indices";
};
fullTextSearch = { fullTextSearch = {
enable = mkEnableOption "Full text search indexing with xapian. This has significant performance and disk space cost."; enable = mkEnableOption "Full text search indexing with xapian. This has significant performance and disk space cost.";
indexDir = mkOption {
type = types.nullOr types.str;
default = "/var/lib/dovecot/fts_xapian";
description = ''
Folder to store search indices. If null, indices are stored along with email, which
is not necessarily desirable as indices are voluminous and do not need to be backed up.
'';
};
autoIndex = mkOption { autoIndex = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
@ -406,8 +223,8 @@ in
default = "no"; default = "no";
description = '' description = ''
Fail searches when no index is available. If set to Fail searches when no index is available. If set to
`body`, then only body searches (as opposed to <literal>body</literal>, then only body searches (as opposed to
header) are affected. If set to `no`, searches may header) are affected. If set to <literal>no<literal>, searches may
fall back to a very slow brute force search. fall back to a very slow brute force search.
''; '';
}; };
@ -445,7 +262,7 @@ in
randomizedDelaySec = mkOption { randomizedDelaySec = mkOption {
type = types.int; type = types.int;
default = 1000; default = 1000;
description = "Run the maintenance job not exactly at the time specified with `onCalendar`, but plus or minus this many seconds."; description = "Run the maintenance job not exactly at the time specified with <literal>onCalendar</literal>, but plus or minus this many seconds.";
}; };
}; };
}; };
@ -464,6 +281,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 = {
@ -488,6 +306,7 @@ in
forwards = mkOption { forwards = mkOption {
type = with types; attrsOf (either (listOf str) str); type = with types; attrsOf (either (listOf str) str);
default = {};
example = { example = {
"user@example.com" = "user@elsewhere.com"; "user@example.com" = "user@elsewhere.com";
}; };
@ -496,7 +315,7 @@ in
the value {`"user@example.com" = "user@elsewhere.com";}` the value {`"user@example.com" = "user@elsewhere.com";}`
means that mails to `user@example.com` are forwarded to means that mails to `user@example.com` are forwarded to
`user@elsewhere.com`. The difference with the `user@elsewhere.com`. The difference with the
{option}`mailserver.extraVirtualAliases` option is that `user@elsewhere.com` `extraVirtualAliases` option is that `user@elsewhere.com`
can't send mail as `user@example.com`. Also, this option can't send mail as `user@example.com`. Also, this option
allows to forward mails to external addresses. allows to forward mails to external addresses.
''; '';
@ -530,7 +349,7 @@ in
description = '' description = ''
The unix UID of the virtual mail user. Be mindful that if this is The unix UID of the virtual mail user. Be mindful that if this is
changed, you will need to manually adjust the permissions of changed, you will need to manually adjust the permissions of
`mailDirectory`. mailDirectory.
''; '';
}; };
@ -573,14 +392,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 = ".";
@ -599,7 +410,8 @@ in
The mailboxes for dovecot. The mailboxes for dovecot.
Depending on the mail client used it might be necessary to change some mailbox's name. Depending on the mail client used it might be necessary to change some mailbox's name.
''; '';
default = { default = let
defMailBoxes = {
Trash = { Trash = {
auto = "no"; auto = "no";
specialUse = "Trash"; specialUse = "Trash";
@ -617,28 +429,23 @@ in
specialUse = "Sent"; specialUse = "Sent";
}; };
}; };
in if (versionAtLeast version "20.09pre") then defMailBoxes
else (flip mapAttrsToList defMailBoxes (name: options: { inherit name; } // options));
}; };
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 +453,8 @@ in
type = types.path; type = types.path;
example = "/root/mail-server.crt"; example = "/root/mail-server.crt";
description = '' description = ''
({option}`mailserver.certificateScheme` == `manual`) Scheme 1)
Location of the certificate
Location of the certificate.
''; '';
}; };
@ -656,9 +462,8 @@ in
type = types.path; type = types.path;
example = "/root/mail-server.key"; example = "/root/mail-server.key";
description = '' description = ''
({option}`mailserver.certificateScheme` == `manual`) Scheme 1)
Location of the key file
Location of the key file.
''; '';
}; };
@ -666,27 +471,13 @@ in
type = types.path; type = types.path;
default = "/var/certs"; default = "/var/certs";
description = '' description = ''
({option}`mailserver.certificateScheme` == `selfsigned`) Scheme 2)
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 {
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 { enableImap = mkOption {
type = types.bool; type = types.bool;
default = true; default = true;
@ -776,7 +567,7 @@ in
type = types.str; type = types.str;
default = "mail"; default = "mail";
description = '' description = ''
The DKIM selector.
''; '';
}; };
@ -784,7 +575,7 @@ in
type = types.path; type = types.path;
default = "/var/dkim"; default = "/var/dkim";
description = '' description = ''
The DKIM directory.
''; '';
}; };
@ -795,93 +586,12 @@ in
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys. How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
If you have already deployed a key with a different number of bits than specified If you have already deployed a key with a different number of bits than specified
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get here, then you should use a different selector (dkimSelector). In order to get
this package to generate a key with the new number of bits, you will either have to this package to generate a key with the new number of bits, you will either have to
change the selector or delete the old key file. change the selector or delete the old key file.
''; '';
}; };
dkimHeaderCanonicalization = mkOption {
type = types.enum ["relaxed" "simple"];
default = "relaxed";
description = ''
DKIM canonicalization algorithm for message headers.
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
'';
};
dkimBodyCanonicalization = mkOption {
type = types.enum ["relaxed" "simple"];
default = "relaxed";
description = ''
DKIM canonicalization algorithm for message bodies.
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
'';
};
dmarcReporting = {
enable = mkOption {
type = types.bool;
default = false;
description = ''
Whether to send out aggregated, daily DMARC reports in response to incoming
mail, when the sender domain defines a DMARC policy including the RUA tag.
This is helpful for the mail ecosystem, because it allows third parties to
get notified about SPF/DKIM violations originating from their sender domains.
See https://rspamd.com/doc/modules/dmarc.html#reporting
'';
};
localpart = mkOption {
type = types.str;
default = "dmarc-noreply";
example = "dmarc-report";
description = ''
The local part of the email address used for outgoing DMARC reports.
'';
};
domain = mkOption {
type = types.enum (cfg.domains);
example = "example.com";
description = ''
The domain from which outgoing DMARC reports are served.
'';
};
email = mkOption {
type = types.str;
default = with cfg.dmarcReporting; "${localpart}@${domain}";
defaultText = literalExpression ''"''${localpart}@''${domain}"'';
readOnly = true;
description = ''
The email address used for outgoing DMARC reports. Read-only.
'';
};
organizationName = mkOption {
type = types.str;
example = "ACME Corp.";
description = ''
The name of your organization used in the `org_name` attribute in
DMARC reports.
'';
};
fromName = mkOption {
type = types.str;
default = cfg.dmarcReporting.organizationName;
defaultText = literalMD "{option}`mailserver.dmarcReporting.organizationName`";
description = ''
The sender name for DMARC reports. Defaults to the organization name.
'';
};
};
debug = mkOption { debug = mkOption {
type = types.bool; type = types.bool;
default = false; default = false;
@ -910,20 +620,12 @@ in
''; '';
}; };
recipientDelimiter = mkOption {
type = types.str;
default = "+";
description = ''
Configure the recipient delimiter.
'';
};
redis = { redis = {
address = mkOption { address = mkOption {
type = types.str; type = types.str;
# read the default from nixos' redis module # read the default from nixos' redis module
default = let default = let
cf = config.services.redis.servers.rspamd.bind; cf = config.services.redis.bind;
cfdefault = if cf == null then "127.0.0.1" else cf; cfdefault = if cf == null then "127.0.0.1" else cf;
ips = lib.strings.splitString " " cfdefault; ips = lib.strings.splitString " " cfdefault;
ip = lib.lists.head (ips ++ [ "127.0.0.1" ]); ip = lib.lists.head (ips ++ [ "127.0.0.1" ]);
@ -932,27 +634,28 @@ in
if (ip == "0.0.0.0" || ip == "::") if (ip == "0.0.0.0" || ip == "::")
then "127.0.0.1" then "127.0.0.1"
else if isIpv6 ip then "[${ip}]" else ip; else if isIpv6 ip then "[${ip}]" else ip;
defaultText = lib.literalMD "computed from `config.services.redis.servers.rspamd.bind`";
description = '' description = ''
Address that rspamd should use to contact redis. Address that rspamd should use to contact redis. The default value
is read from <literal>config.services.redis.bind</literal>.
''; '';
}; };
port = mkOption { port = mkOption {
type = types.port; type = types.port;
default = config.services.redis.servers.rspamd.port; default = config.services.redis.port;
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.port";
description = '' description = ''
Port that rspamd should use to contact redis. Port that rspamd should use to contact redis. The default value is
read from <literal>config.services.redis.port<literal>.
''; '';
}; };
password = mkOption { password = mkOption {
type = types.nullOr types.str; type = types.nullOr types.str;
default = config.services.redis.servers.rspamd.requirePass; default = config.services.redis.requirePass;
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.requirePass";
description = '' description = ''
Password that rspamd should use to contact redis, or null if not required. Password that rspamd should use to contact redis, or null if not
required. The default value is read from
<literal>config.services.redis.requirePass<literal>.
''; '';
}; };
}; };
@ -967,25 +670,10 @@ 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;
defaultText = lib.literalMD "{option}`mailserver.fqdn`"; defaultText = "config.mailserver.fqdn";
example = "myserver.example.com"; example = "myserver.example.com";
description = '' description = ''
The fully qualified domain name of the mail server used to The fully qualified domain name of the mail server used to
@ -1001,7 +689,7 @@ in
This setting allows the server to identify as This setting allows the server to identify as
myserver.example.com when forwarding mail, independently of myserver.example.com when forwarding mail, independently of
{option}`mailserver.fqdn` (which, for SSL reasons, should generally be the name `fqdn` (which, for SSL reasons, should generally be the name
to which the user connects). to which the user connects).
Set this to the name to which the sending IP's reverse DNS Set this to the name to which the sending IP's reverse DNS
@ -1069,11 +757,10 @@ in
stop program = "${pkgs.systemd}/bin/systemctl stop dovecot2" stop program = "${pkgs.systemd}/bin/systemctl stop dovecot2"
if failed host ${cfg.fqdn} port 993 type tcpssl sslauto protocol imap for 5 cycles then restart if failed host ${cfg.fqdn} port 993 type tcpssl sslauto protocol imap for 5 cycles then restart
check process rspamd with matching "rspamd: main process" check process rspamd with pidfile /var/run/rspamd.pid
start program = "${pkgs.systemd}/bin/systemctl start rspamd" start program = "${pkgs.systemd}/bin/systemctl start rspamd"
stop program = "${pkgs.systemd}/bin/systemctl stop rspamd" stop program = "${pkgs.systemd}/bin/systemctl stop rspamd"
''; '';
defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
description = '' description = ''
The configuration used for monitoring via monit. The configuration used for monitoring via monit.
Use a mail address that you actively check and set it via 'set alert ...'. Use a mail address that you actively check and set it via 'set alert ...'.
@ -1090,8 +777,7 @@ in
description = '' description = ''
The location where borg saves the backups. The location where borg saves the backups.
This can be a local path or a remote location such as user@host:/path/to/repo. This can be a local path or a remote location such as user@host:/path/to/repo.
It is exported and thus available as an environment variable to It is exported and thus available as an environment variable to cmdPreexec and cmdPostexec.
{option}`mailserver.borgbackup.cmdPreexec` and {option}`mailserver.borgbackup.cmdPostexec`.
''; '';
}; };
@ -1151,14 +837,13 @@ in
default = "none"; default = "none";
description = '' description = ''
The backup can be encrypted by choosing any other value than 'none'. The backup can be encrypted by choosing any other value than 'none'.
When using encryption the password/passphrase must be provided in `passphraseFile`. When using encryption the password / passphrase must be provided in passphraseFile.
''; '';
}; };
passphraseFile = mkOption { passphraseFile = mkOption {
type = types.nullOr types.path; type = types.nullOr types.path;
default = null; default = null;
description = "Path to a file containing the encryption password or passphrase.";
}; };
}; };
@ -1174,7 +859,6 @@ in
locations = mkOption { locations = mkOption {
type = types.listOf types.path; type = types.listOf types.path;
default = [cfg.mailDirectory]; default = [cfg.mailDirectory];
defaultText = lib.literalExpression "[ config.mailserver.mailDirectory ]";
description = "The locations that are to be backed up by borg."; description = "The locations that are to be backed up by borg.";
}; };
@ -1195,9 +879,8 @@ in
default = null; default = null;
description = '' description = ''
The command to be executed before each backup operation. The command to be executed before each backup operation.
This is called prior to borg init in the same script that runs borg init and create and `cmdPostexec`. This is called prior to borg init in the same script that runs borg init and create and cmdPostexec.
''; Example:
example = ''
export BORG_RSH="ssh -i /path/to/private/key" export BORG_RSH="ssh -i /path/to/private/key"
''; '';
}; };
@ -1208,7 +891,7 @@ in
description = '' description = ''
The command to be executed after each backup operation. The command to be executed after each backup operation.
This is called after borg create completed successfully and in the same script that runs This is called after borg create completed successfully and in the same script that runs
`cmdPreexec`, borg init and create. cmdPreexec, borg init and create.
''; '';
}; };
@ -1221,7 +904,7 @@ in
example = true; example = true;
description = '' description = ''
Whether to enable automatic reboot after kernel upgrades. Whether to enable automatic reboot after kernel upgrades.
This is to be used in conjunction with `system.autoUpgrade.enable = true;` This is to be used in conjunction with system.autoUpgrade.enable = true"
''; '';
}; };
method = mkOption { method = mkOption {
@ -1296,7 +979,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

@ -4,8 +4,8 @@ Add Radicale
Configuration by @dotlambda Configuration by @dotlambda
Starting with Radicale 3 (first introduced in NixOS 20.09) the traditional Starting with Radicale 3 (first introduced in NixOS 20.09) the traditional
crypt passwords are no longer supported. Instead bcrypt passwords crypt passwords, as generated by `mkpasswd`, are no longer supported. Instead
have to be used. These can still be generated using `mkpasswd -m bcrypt`. bcrypt passwords have to be used which can be generated using `htpasswd`.
.. code:: nix .. code:: nix
@ -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

@ -1,32 +0,0 @@
Add Roundcube, a webmail
========================
The NixOS module for roundcube nearly works out of the box with SNM. By
default, it sets up a nginx virtual host to serve the webmail, other web
servers may require more work.
.. code:: nix
{ config, pkgs, lib, ... }:
with lib;
{
services.roundcube = {
enable = true;
# this is the url of the vhost, not necessarily the same as the fqdn of
# the mailserver
hostName = "webmail.example.com";
extraConfig = ''
# starttls needed for authentication, so the fqdn required to match
# the certificate
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
$config['smtp_user'] = "%u";
$config['smtp_pass'] = "%p";
'';
};
services.nginx.enable = true;
networking.firewall.allowedTCPPorts = [ 80 443 ];
}

View file

@ -1,18 +0,0 @@
Autodiscovery
=============
`RFC6186 <https://www.rfc-editor.org/rfc/rfc6186>`_ allows supporting email clients to automatically discover SMTP / IMAP addresses
of the mailserver. For that, the following records are required:
================= ==== ==== ======== ====== ==== =================
Record TTL Type Priority Weight Port Value
================= ==== ==== ======== ====== ==== =================
_submission._tcp 3600 SRV 5 0 587 mail.example.com.
_submissions._tcp 3600 SRV 5 0 465 mail.example.com.
_imap._tcp 3600 SRV 5 0 143 mail.example.com.
_imaps._tcp 3600 SRV 5 0 993 mail.example.com.
================= ==== ==== ======== ====== ==== =================
Please note that only a few MUAs currently implement this. For vendor-specific
discovery mechanisms `automx <https://github.com/rseichter/automx2>`_ can be used instead.

View file

@ -18,7 +18,7 @@
# -- Project information ----------------------------------------------------- # -- Project information -----------------------------------------------------
project = 'NixOS Mailserver' project = 'NixOS Mailserver'
copyright = '2022, NixOS Mailserver Contributors' copyright = '2020, NixOS Mailserver Contributors'
author = 'NixOS Mailserver Contributors' author = 'NixOS Mailserver Contributors'
@ -28,16 +28,8 @@ author = 'NixOS Mailserver Contributors'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones. # ones.
extensions = [ extensions = [
'myst_parser'
] ]
myst_enable_extensions = [
'colon_fence',
'linkify',
]
smartquotes = False
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates'] templates_path = ['_templates']
@ -58,4 +50,4 @@ html_theme = 'sphinx_rtd_theme'
# Add any paths that contain custom static files (such as style sheets) here, # Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files, # relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css". # so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = [] html_static_path = ['_static']

View file

@ -1,30 +0,0 @@
Nix Flakes
==========
If you're using `flakes <https://nixos.wiki/wiki/Flakes>`__, you can use
the following minimal ``flake.nix`` as an example:
.. code:: nix
{
description = "NixOS configuration";
inputs.simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver/nixos-20.09";
outputs = { self, nixpkgs, simple-nixos-mailserver }: {
nixosConfigurations = {
hostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
simple-nixos-mailserver.nixosModule
{
mailserver = {
enable = true;
# ...
};
}
];
};
};
};
}

View file

@ -38,17 +38,9 @@ issues a search query, so latency will be high.
Resource requirements Resource requirements
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
Indices created by the full text search feature can take more disk Indices can take more disk space than the emails themselves. By default, they
space than the emails themselves. By default, they are kept in the are kept in a different location (``/var/lib/dovecot/fts_xapian``) than emails
emails location. When enabling the full text search feature, it is so that you can backup emails without indices.
recommended to move indices in a different location, such as
(``/var/lib/dovecot/indices``) by using the option
``mailserver.indexDir``.
.. warning::
When the value of the ``indexDir`` option is changed, all dovecot
indices needs to be recreated: clients would need to resynchronize.
Indexation itself is rather resouces intensive, in CPU, and for emails with Indexation itself is rather resouces intensive, in CPU, and for emails with
large headers, in memory as well. Initial indexation of existing emails can take large headers, in memory as well. Initial indexation of existing emails can take

View file

@ -4,46 +4,37 @@ Contribute or troubleshoot
To report an issue, please go to To report an issue, please go to
`<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues>`_. `<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues>`_.
You can also chat with us on the Libera IRC channel ``#nixos-mailserver``. You can also chat with us on the Freenode IRC channel ``#nixos-mailserver``.
Run NixOS tests Run NixOS tests
--------------- ---------------
To run the test suite, you need to enable `Nix Flakes You can run the testsuite via
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`_.
You can then run the testsuite via
:: ::
$ nix flake check -L $ nix-build tests -A extern.nixpkgs_20_03
$ nix-build tests -A intern.nixpkgs_unstable
Since Nix doesn't garantee your machine have enough resources to run ...
all test VMs in parallel, some tests can fail. You would then haev to
run tests manually. For instance:
::
$ nix build .#hydraJobs.x86_64-linux.external-unstable -L
Contributing to the documentation Contributing to the documentation
--------------------------------- ---------------------------------
The documentation is written in RST (except option documentation which is in CommonMark), The documentation is written in RST, build with Sphinx and published
built with Sphinx and published by `Read the Docs <https://readthedocs.org/>`_. by `Read the Docs <https://readthedocs.org/>`_.
For the syntax, see the `RST/Sphinx primer For the syntax, see `RST/Sphinx Cheatsheet
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_. <https://sphinx-tutorial.readthedocs.io/cheatsheet/>`_.
To build the documentation, you need to enable `Nix Flakes
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`_.
The ``shell.nix`` provides all the tooling required to build the
documentation:
:: ::
$ nix build .#documentation $ nix-shell
$ xdg-open result/index.html $ cd docs
$ make html
$ firefox ./_build/html/index.html
Nixops Nixops
------ ------

View file

@ -6,18 +6,18 @@
Welcome to NixOS Mailserver's documentation! Welcome to NixOS Mailserver's documentation!
============================================ ============================================
.. image:: logo.png .. image:: ../logo/logo.png
:width: 400 :width: 400
:alt: SNM Logo :alt: SNM Logo
.. toctree:: .. toctree::
:maxdepth: 2 :maxdepth: 2
quick-start
setup-guide setup-guide
howto-develop howto-develop
faq faq
release-notes release-notes
options
.. toctree:: .. toctree::
:maxdepth: 1 :maxdepth: 1
@ -25,12 +25,8 @@ Welcome to NixOS Mailserver's documentation!
backup-guide backup-guide
add-radicale add-radicale
add-roundcube
rspamd-tuning rspamd-tuning
fts fts
flakes
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``.

34
docs/quick-start.rst Normal file
View file

@ -0,0 +1,34 @@
Quick Start
===========
.. code:: nix
{ config, pkgs, ... }:
let release = "nixos-20.09";
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 run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2 > /hashed/password/file/location
hashedPasswordFile = "/hashed/password/file/location";
aliases = [
"info@example.com"
"postmaster@example.com"
"postmaster@example2.com"
];
};
};
};
}

View file

@ -1,61 +1,6 @@
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
-----------
- Allow Rspamd to send DMARC reporting
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/244>`__)
NixOS 22.05
-----------
- Make NixOS Mailserver options discoverable from search.nixos.org
- Add a roundcube setup guide in the documentation
NixOS 21.11
-----------
- Switch default DKIM body policy from simple to relaxed
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/247>`__)
- Ensure locally-delivered mails have the X-Original-To header
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/243>`__)
- NixOS Mailserver options are detailed in the `documentation
<https://nixos-mailserver.readthedocs.io/en/latest/options.html>`__
- New options ``dkimBodyCanonicalization`` and
``dkimHeaderCanonicalization``
- New option ``certificateDomains`` to generate certificate for
additional domains (such as ``imap.example.com``)
NixOS 21.05
-----------
- New `fullTextSearch` option to search in messages (based on Xapian)
(`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/212>`__)
- Flake support
(`Merge Request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/200>`__)
- New `openFirewall` option defaulting to `true`
- We moved from Freenode to Libera Chat
NixOS 20.09 NixOS 20.09
----------- -----------

View file

@ -1,4 +0,0 @@
sphinx ~= 5.3
sphinx_rtd_theme ~= 1.1
myst-parser ~= 0.18
linkify-it-py ~= 2.0

View file

@ -3,215 +3,272 @@ Setup Guide
Mail servers can be a tricky thing to set up. This guide is supposed to Mail servers can be a tricky thing to set up. This guide is supposed to
run you through the most important steps to achieve a 10/10 score on run you through the most important steps to achieve a 10/10 score on
`<https://mail-tester.com>`_. ``mail-tester.com``.
What you need is: What you need:
- a server running NixOS with a public IP - A server with a public IP (referred to as ``server-IP``)
- a domain name. - A Fully Qualified Domain Name (``FQDN``) where your server is
reachable, so that other servers can find yours. Common FQDN include
``mx.example.com`` (where ``example.com`` is a domain you own) or
``mail.example.com``. The domain is referred to as ``server-domain``
(``example.com`` in the above example) and the ``FQDN`` is referred
to by ``server-FQDN`` (``mx.example.com`` above).
- A list of domains you want to your email server to serve. (Note that
this does not have to include ``server-domain``, but may of course).
These will be referred to as ``domains``. As an example,
``domains = [ example1.com, example2.com ]``.
.. note:: Setup server
~~~~~~~~~~~~
In the following, we consider a server with the public IP ``1.2.3.4``
and the domain ``example.com``.
First, we will set the minimum DNS configuration to be able to deploy
an up and running mail server. Once the server is deployed, we could
then set all DNS entries required to send and receive mails on this
server.
Setup DNS A record for server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Add a DNS record to the domain ``example.com`` with the following
entries
==================== ===== ==== =============
Name (Subdomain) TTL Type Value
==================== ===== ==== =============
``mail.example.com`` 10800 A ``1.2.3.4``
==================== ===== ==== =============
You can check this with
::
$ ping mail.example.com
64 bytes from mail.example.com (1.2.3.4): icmp_seq=1 ttl=46 time=21.3 ms
...
Note that it can take a while until a DNS entry is propagated. This
DNS entry is required for the Let's Encrypt certificate generation
(which is used in the below configuration example).
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 ``default.nix``), these
options documentation <options.html>`_), these should be the most 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";
}) })
]; ];
mailserver = { mailserver = {
enable = true; enable = true;
fqdn = "mail.example.com"; fqdn = <server-FQDN>;
domains = [ "example.com" ]; domains = [ <domains> ];
# 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 run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
loginAccounts = { loginAccounts = {
"user1@example.com" = { "user1@example.com" = {
hashedPasswordFile = "/a/file/containing/a/hashed/password"; hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
aliases = ["postmaster@example.com"];
aliases = [
"postmaster@example.com"
"postmaster@example2.com"
];
# Make this user the catchAll address for domains example.com and
# example2.com
catchAll = [
"example.com"
"example2.com"
];
}; };
"user2@example.com" = { ... }; "user2@example.com" = { ... };
}; };
# Extra virtual aliases. These are email addresses that are forwarded to
# loginAccounts addresses.
extraVirtualAliases = {
# address = forward address;
"abuse@example.com" = "user1@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;
# Enable IMAP and POP3
enableImap = true;
enablePop3 = true;
enableImapSsl = true;
enablePop3Ssl = true;
# Enable the ManageSieve protocol
enableManageSieve = true;
# whether to scan inbound emails for viruses (note that this requires at least
# 1 Gb RAM for the server. Without virus scanning 256 MB RAM should be plenty)
virusScanning = false;
}; };
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 --upgrade`` your server should be good to
mail components. go. If you want to use ``nixops`` to deploy the server, look in the
subfolder ``nixops`` for some inspiration.
Setup all other DNS requirements If you're using `flakes <https://nixos.wiki/wiki/Flakes>`__, you can use
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ the following minimal ``flake.nix`` as an example:
Set rDNS (reverse DNS) entry for server .. code:: nix
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
{
description = "NixOS configuration";
inputs.simple-nixos-mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver";
outputs = { self, nixpkgs, simple-nixos-mailserver }: {
nixosConfigurations = {
hostname = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
simple-nixos-mailserver.nixosModule
{
mailserver = {
enable = true;
# ...
};
}
];
};
};
};
}
Setup everything else
~~~~~~~~~~~~~~~~~~~~~
Step 1: Set DNS entry for server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Add a DNS record to the domain ``server-domain`` with the following
entries
================ ===== ==== ======== =============
Name (Subdomain) TTL Type Priority Value
================ ===== ==== ======== =============
``server-FQDN`` 10800 A ``server-IP``
================ ===== ==== ======== =============
This resolves DNS queries for ``server-FQDN`` to ``server-IP``. You can
test if your setting is correct by
::
ping <server-FQDN>
Expected output:
::
64 bytes from <server-FQDN> (<server-IP>): icmp_seq=1 ttl=46 time=21.3 ms
...
Note that it can take a while until a DNS entry is propagated.
Step 2: Set rDNS (reverse DNS) entry for server
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Wherever you have rented your server, you should be able to set reverse Wherever you have rented your server, you should be able to set reverse
DNS entries for the IPs you own. Add an entry resolving ``1.2.3.4`` DNS entries for the IPs you own. Add an entry resolving ``server-IP``
to ``mail.example.com``. to ``server-FQDN``
.. warning:: You can test if your setting is correct by
We don't recommend setting up a mail server if you are not able to
set a reverse DNS on your public IP because sent emails would be
mostly marked as spam. Note that many residential ISP providers
don't allow you to set a reverse DNS entry.
You can check this with
:: ::
$ nix-shell -p bind --command "host 1.2.3.4" host <server-IP>
4.3.2.1.in-addr.arpa domain name pointer mail.example.com.
Note that it can take a while until a DNS entry is propagated. Expected output:
Set a ``MX`` record
^^^^^^^^^^^^^^^^^^^
Add a ``MX`` record to the domain ``example.com``.
================ ==== ======== =================
Name (Subdomain) Type Priority Value
================ ==== ======== =================
example.com MX 10 mail.example.com
================ ==== ======== =================
You can check this with
:: ::
$ nix-shell -p bind --command "host -t mx example.com" <server-IP-octets-reversed>.in-addr.arpa domain name pointer <server-FQDN>.
example.com mail is handled by 10 mail.example.com.
Note that it can take a while until a DNS entry is propagated. Note that it can take a while until a DNS entry is propagated.
Set a ``SPF`` record Step 3: Set ``MX`` Records
^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^
Add a `SPF <https://en.wikipedia.org/wiki/Sender_Policy_Framework>`_ For every ``domain`` in ``domains`` do: \* Add a ``MX`` record to the
record to the domain ``example.com``. domain ``domain``
::
| Name (Subdomain) | TTL | Type | Priority | Value |
| ---------------- | ----- | ---- | -------- | ----------------- |
| `domain` | | MX | 10 | `server-FQDN` |
You can test this via
::
dig -t MX <domain>
Expected output:
::
...
;; ANSWER SECTION:
<domain> 10800 IN MX 10 <server-FQDN>
...
Note that it can take a while until a DNS entry is propagated.
Step 4: Set ``SPF`` Records
^^^^^^^^^^^^^^^^^^^^^^^^^^^
For every ``domain`` in ``domains`` do: \* Add a ``SPF`` record to the
domain ``domain``
::
| Name (Subdomain) | TTL | Type | Priority | Value |
| ---------------- | ----- | ---- | -------- | ----------------- |
| `domain` | 10800 | TXT | | `v=spf1 ip4:<server-IP> -all` |
You can check this with ``dig -t TXT <domain>`` similar to the last
section. Note that ``SPF`` records are set as ``TXT`` records since
RFC1035.
Note that it can take a while until a DNS entry is propagated. If you
want to use multiple servers for your email handling, dont forget to
add all server IPs to this list.
Step 5: Set ``DKIM`` signature
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In this section we assume that your ``dkimSelector`` is set to ``mail``.
If you have a different selector, replace all ``mail``\ s below
accordingly.
For every ``domain`` in ``domains`` do: \* Go to your server and
navigate to the dkim key directory (by default ``/var/dkim``). There you
will find a public key for any domain in the ``domain.txt`` file. It
will look like
``mail._domainkey IN TXT "v=DKIM1; r=postmaster; g=*; k=rsa; p=<really-long-key>" ; ----- DKIM mail for domain.tld``
\* Add a ``DKIM`` record to the domain ``domain``
::
| Name (Subdomain) | TTL | Type | Priority | Value |
| ---------------- | ----- | ---- | -------- | ----------------- |
| mail._domainkey.`domain` | 10800 | TXT | | `v=DKIM1; p=<really-long-key>` |
You can check this with ``dig -t TXT mail._domainkey.<domain>`` similar
to the last section.
Note that it can take a while until a DNS entry is propagated.
Step 6: Set ``DMARC`` record
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For every ``domain`` in ``domains`` do:
- Add a ``DMARC`` record to the domain ``domain``
==================== ===== ==== ======== ==================== ==================== ===== ==== ======== ====================
Name (Subdomain) TTL Type Value Name (Subdomain) TTL Type Priority Value
==================== ===== ==== ======== ==================== ==================== ===== ==== ======== ====================
example.com 10800 TXT `v=spf1 a:mail.example.com -all` \_dmarc.\ ``domain`` 10800 TXT ``v=DMARC1; p=none``
==================== ===== ==== ======== ==================== ==================== ===== ==== ======== ====================
You can check this with You can check this with ``dig -t TXT _dmarc.<domain>`` similar to the
last section.
::
$ nix-shell -p bind --command "host -t TXT example.com"
example.com descriptive text "v=spf1 a:mail.example.com -all"
Note that it can take a while until a DNS entry is propagated. Note that it can take a while until a DNS entry is propagated.
Set ``DKIM`` signature
^^^^^^^^^^^^^^^^^^^^^^
On your server, the ``opendkim`` systemd service generated a file
containing your DKIM public key in the file
``/var/dkim/example.com.mail.txt``. The content of this file looks
like
::
mail._domainkey IN TXT "v=DKIM1; k=rsa; s=email; p=<really-long-key>" ; ----- DKIM mail for domain.tld
where ``really-long-key`` is your public key.
Based on the content of this file, we can add a ``DKIM`` record to the
domain ``example.com``.
=========================== ===== ==== ==============================
Name (Subdomain) TTL Type Value
=========================== ===== ==== ==============================
mail._domainkey.example.com 10800 TXT ``v=DKIM1; p=<really-long-key>``
=========================== ===== ==== ==============================
You can check this with
::
$ nix-shell -p bind --command "host -t txt mail._domainkey.example.com"
mail._domainkey.example.com descriptive text "v=DKIM1;p=<really-long-key>"
Note that it can take a while until a DNS entry is propagated.
Set a ``DMARC`` record
^^^^^^^^^^^^^^^^^^^^^^
Add a ``DMARC`` record to the domain ``example.com``.
======================== ===== ==== ====================
Name (Subdomain) TTL Type Value
======================== ===== ==== ====================
_dmarc.example.com 10800 TXT ``v=DMARC1; p=none``
======================== ===== ==== ====================
You can check this with
::
$ nix-shell -p bind --command "host -t TXT _dmarc.example.com"
_dmarc.example.com descriptive text "v=DMARC1; p=none"
Note that it can take a while until a DNS entry is propagated.
Test your Setup Test your Setup
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~

View file

@ -1,44 +1,12 @@
{ {
"nodes": { "nodes": {
"blobs": {
"flake": false,
"locked": {
"lastModified": 1604995301,
"narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
"owner": "simple-nixos-mailserver",
"repo": "blobs",
"rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
"type": "gitlab"
},
"original": {
"owner": "simple-nixos-mailserver",
"repo": "blobs",
"type": "gitlab"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1717602782, "lastModified": 1607522989,
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=", "narHash": "sha256-o/jWhOSAlaK7y2M57OIriRt6whuVVocS/T0mG7fd1TI=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6", "rev": "e9158eca70ae59e73fae23be5d13d3fa0cfc78b4",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -47,27 +15,25 @@
"type": "indirect" "type": "indirect"
} }
}, },
"nixpkgs-24_05": { "root": {
"inputs": {
"nixpkgs": "nixpkgs",
"utils": "utils"
}
},
"utils": {
"locked": { "locked": {
"lastModified": 1717144377, "lastModified": 1605370193,
"narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=", "narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=",
"owner": "NixOS", "owner": "numtide",
"repo": "nixpkgs", "repo": "flake-utils",
"rev": "805a384895c696f802a9bf5bf4720f37385df547", "rev": "5021eac20303a61fafe17224c087f5519baed54d",
"type": "github" "type": "github"
}, },
"original": { "original": {
"id": "nixpkgs", "owner": "numtide",
"ref": "nixos-24.05", "repo": "flake-utils",
"type": "indirect" "type": "github"
}
},
"root": {
"inputs": {
"blobs": "blobs",
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs",
"nixpkgs-24_05": "nixpkgs-24_05"
} }
} }
}, },

125
flake.nix
View file

@ -2,126 +2,25 @@
description = "A complete and Simple Nixos Mailserver"; description = "A complete and Simple Nixos Mailserver";
inputs = { inputs = {
flake-compat = { utils.url = "github:numtide/flake-utils";
url = "github:edolstra/flake-compat";
flake = false;
};
nixpkgs.url = "flake:nixpkgs/nixos-unstable"; nixpkgs.url = "flake:nixpkgs/nixos-unstable";
nixpkgs-24_05.url = "flake:nixpkgs/nixos-24.05";
blobs = {
url = "gitlab:simple-nixos-mailserver/blobs";
flake = false;
};
}; };
outputs = { self, blobs, nixpkgs, nixpkgs-24_05, ... }: let outputs = { self, utils, nixpkgs }: {
lib = nixpkgs.lib; nixosModules.mailserver = import ./.;
system = "x86_64-linux"; nixosModule = self.nixosModules.mailserver;
} // utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
releases = [ in {
{ devShell = pkgs.mkShell {
name = "unstable"; buildInputs = with pkgs; [
pkgs = nixpkgs.legacyPackages.${system}; (python3.withPackages (p: with p; [
}
{
name = "24.05";
pkgs = nixpkgs-24_05.legacyPackages.${system};
}
];
testNames = [
"internal"
"external"
"clamav"
"multiple"
"ldap"
];
genTest = testName: release: {
"name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
"value"= import (./tests/. + "/${testName}.nix") {
pkgs = release.pkgs;
inherit blobs;
};
};
# Generate an attribute set such as
# {
# external-unstable = <derivation>;
# external-21_05 = <derivation>;
# ...
# }
allTests = lib.listToAttrs (
lib.flatten (map (t: map (r: genTest t r) releases) testNames));
mailserverModule = import ./.;
# Generate a MarkDown file describing the options of the NixOS mailserver module
optionsDoc = let
eval = lib.evalModules {
modules = [
mailserverModule
{
_module.check = false;
mailserver = {
fqdn = "mx.example.com";
domains = [
"example.com"
];
dmarcReporting = {
organizationName = "Example Corp";
domain = "example.com";
};
};
}
];
};
options = builtins.toFile "options.json" (builtins.toJSON
(lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver")
(lib.optionAttrSetToDocList eval.options)));
in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } ''
echo "Generating options.md from ${options}"
python ${./scripts/generate-options.py} ${options} > $out
'';
documentation = pkgs.stdenv.mkDerivation {
name = "documentation";
src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"];
buildInputs = [(
pkgs.python3.withPackages (p: with p; [
sphinx sphinx
sphinx_rtd_theme sphinx_rtd_theme
myst-parser ]))
linkify-it-py jq
])
)];
buildPhase = ''
cp ${optionsDoc} options.md
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
unset SOURCE_DATE_EPOCH
make html
'';
installPhase = ''
cp -Tr _build/html $out
'';
};
in {
nixosModules = rec {
mailserver = mailserverModule;
default = mailserver;
};
nixosModule = self.nixosModules.default; # compatibility
hydraJobs.${system} = allTests // {
inherit documentation;
};
checks.${system} = allTests;
packages.${system} = {
inherit optionsDoc documentation;
};
devShells.${system}.default = pkgs.mkShell {
inputsFrom = [ documentation ];
packages = with pkgs; [
clamav clamav
]; ];
}; };
devShell.${system} = self.devShells.${system}.default; # compatibility });
};
} }

View file

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

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

@ -14,17 +14,19 @@
# 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, pkgs, lib, options, ... }: { config, pkgs, lib, ... }:
let let
cfg = config.mailserver; cfg = config.mailserver;
in in
{ {
config = lib.mkIf (cfg.enable && cfg.virusScanning) { config = lib.mkIf (cfg.enable && cfg.virusScanning) {
services.clamav.daemon = { services.clamav.daemon.enable = true;
enable = true;
settings.PhishingScanURLs = "no";
};
services.clamav.updater.enable = true; services.clamav.updater.enable = true;
services.clamav.daemon.extraConfig = ''
PhishingScanURLs no
'';
}; };
} }

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

@ -23,19 +23,17 @@ let
passwdDir = "/run/dovecot2"; passwdDir = "/run/dovecot2";
passwdFile = "${passwdDir}/passwd"; 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"; 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
":INDEX=${cfg.indexDir}/%d/%n" (cfg.fullTextSearch.enable && (cfg.fullTextSearch.indexDir != null))
":INDEX=${cfg.fullTextSearch.indexDir}/%d/%n"
); );
postfixCfg = config.services.postfix; postfixCfg = config.services.postfix;
@ -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!"
@ -118,48 +77,18 @@ let
cat <<EOF > ${passwdFile} cat <<EOF > ${passwdFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
) cfg.loginAccounts)}
EOF
cat <<EOF > ${userdbFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
"${name}:::::::"
+ (if lib.isString value.quota + (if lib.isString value.quota
then "userdb_quota_rule=*:storage=${value.quota}" then "userdb_quota_rule=*:storage=${value.quota}"
else "") else "")
) cfg.loginAccounts)} ) cfg.loginAccounts)}
EOF EOF
chmod 600 ${passwdFile}
''; '';
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
junkMailboxNumber = builtins.length junkMailboxes;
# 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 in
{ {
config = with cfg; lib.mkIf enable { config = with cfg; lib.mkIf enable {
assertions = [
{
assertion = junkMailboxNumber == 1;
message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
}
];
# 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,48 +105,17 @@ 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" {
fileinto "${junkMailboxName}"; fileinto "Junk";
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 = ''
@ -293,7 +191,7 @@ in
} }
} }
recipient_delimiter = ${cfg.recipientDelimiter} recipient_delimiter = +
lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox} lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
protocol lmtp { protocol lmtp {
@ -307,23 +205,9 @@ in
userdb { userdb {
driver = passwd-file driver = passwd-file
args = ${userdbFile} args = ${passwdFile}
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,7 +223,29 @@ in
inbox = yes inbox = yes
} }
${lib.optionalString cfg.fullTextSearch.enable '' 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 = Junk
imapsieve_mailbox1_causes = COPY
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
# From Spam folder to elsewhere
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Junk
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 != null) ''
plugin { plugin {
plugin = fts fts_xapian plugin = fts fts_xapian
fts = xapian fts = xapian
@ -367,10 +273,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

@ -22,6 +22,7 @@ in
{ {
config = lib.mkIf (cfg.enable && cfg.localDnsResolver) { config = lib.mkIf (cfg.enable && cfg.localDnsResolver) {
services.kresd.enable = true; services.kresd.enable = true;
networking.nameservers = [ "127.0.0.1" ];
}; };
} }

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,26 +17,28 @@
{ 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;
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
acmeRoot = acmeRoot;
}; };
}; };
security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [ security.acme.certs."${cfg.fqdn}".postRun = ''
"postfix.service" systemctl reload nginx
"dovecot2.service" systemctl reload postfix
]; systemctl reload dovecot2
'';
}; };
} }

View file

@ -29,7 +29,7 @@ let
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt"; dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
in in
'' ''
if [ ! -f "${dkim_key}" ] if [ ! -f "${dkim_key}" ] || [ ! -f "${dkim_txt}" ]
then then
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \ ${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
-d "${dom}" \ -d "${dom}" \
@ -37,7 +37,6 @@ let
--directory="${cfg.dkimKeyDirectory}" --directory="${cfg.dkimKeyDirectory}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}" mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}" mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
chmod 644 "${dkim_txt}"
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}" echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
fi fi
''; '';
@ -60,7 +59,7 @@ in
keyPath = cfg.dkimKeyDirectory; keyPath = cfg.dkimKeyDirectory;
domains = "csl:${builtins.concatStringsSep "," cfg.domains}"; domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
configFile = pkgs.writeText "opendkim.conf" ('' configFile = pkgs.writeText "opendkim.conf" (''
Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization} Canonicalization relaxed/simple
UMask 0002 UMask 0002
Socket ${dkim.socket} Socket ${dkim.socket}
KeyTable file:${keyTable} KeyTable file:${keyTable}

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;
@ -223,7 +160,7 @@ in
config = { config = {
# Extra Config # Extra Config
mydestination = ""; mydestination = "";
recipient_delimiter = cfg.recipientDelimiter; recipient_delimiter = "+";
smtpd_banner = "${fqdn} ESMTP NO UCE"; smtpd_banner = "${fqdn} ESMTP NO UCE";
disable_vrfy_command = true; disable_vrfy_command = true;
message_size_limit = toString cfg.messageSizeLimit; message_size_limit = toString cfg.messageSizeLimit;
@ -233,19 +170,8 @@ 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
lmtp_destination_recipient_limit = "1";
# sasl with dovecot # sasl with dovecot
smtpd_sasl_type = "dovecot"; smtpd_sasl_type = "dovecot";
@ -274,6 +200,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,26 +235,18 @@ 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;
submissionsOptions = submissionOptions; submissionsOptions = submissionOptions;
masterConfig = { masterConfig = {
"lmtp" = {
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
# D => Delivered-To, O => X-Original-To, R => Return-Path
args = [ "flags=O" ];
};
"policy-spf" = { "policy-spf" = {
type = "unix"; type = "unix";
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}";
@ -56,17 +56,14 @@ in
# Disable outbound email signing, we use opendkim for this # Disable outbound email signing, we use opendkim for this
enabled = false; enabled = false;
''; }; ''; };
"dmarc.conf" = { text = '' };
${lib.optionalString cfg.dmarcReporting.enable ''
reporting { overrides = {
enabled = true; "milter_headers.conf" = {
email = "${cfg.dmarcReporting.email}"; text = ''
domain = "${cfg.dmarcReporting.domain}"; extended_spam_headers = true;
org_name = "${cfg.dmarcReporting.organizationName}"; '';
from_name = "${cfg.dmarcReporting.fromName}"; };
msgid_from = "dmarc-rua";
}''}
''; };
}; };
workers.rspamd_proxy = { workers.rspamd_proxy = {
@ -101,72 +98,11 @@ in
}; };
services.redis.servers.rspamd = { services.redis.enable = true;
enable = lib.mkDefault true;
port = lib.mkDefault 6380;
};
systemd.services.rspamd = { systemd.services.rspamd = {
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); requires = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service"); after = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
};
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
# Explicitly select yesterday's date to work around broken
# default behaviour when called without a date.
# https://github.com/rspamd/rspamd/issues/4062
script = ''
${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
'';
serviceConfig = {
User = "${config.services.rspamd.user}";
Group = "${config.services.rspamd.group}";
AmbientCapabilities = [];
CapabilityBoundingSet = "";
DevicePolicy = "closed";
IPAddressAllow = "localhost";
LockPersonality = true;
NoNewPrivileges = true;
PrivateDevices = true;
PrivateMounts = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
ProcSubset = "pid";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
];
UMask = "0077";
};
};
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
description = "Daily delivery of aggregated DMARC reports";
wantedBy = [
"timers.target"
];
timerConfig = {
OnCalendar = "daily";
Persistent = true;
RandomizedDelaySec = 86400;
FixedRandomDelay = true;
};
}; };
systemd.services.postfix = { systemd.services.postfix = {

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
@ -59,13 +59,13 @@ in
preStart = let preStart = let
directories = lib.strings.escapeShellArgs ( directories = lib.strings.escapeShellArgs (
[ mailDirectory ] [ mailDirectory ]
++ lib.optional (cfg.indexDir != null) cfg.indexDir ++ lib.optional
(cfg.fullTextSearch.enable && (cfg.fullTextSearch.indexDir != null))
cfg.fullTextSearch.indexDir
); );
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

@ -21,7 +21,7 @@ with config.mailserver;
let let
vmail_user = { vmail_user = {
name = vmailUserName; name = vmailUserName;
isSystemUser = true; isNormalUser = false;
uid = vmailUID; uid = vmailUID;
home = mailDirectory; home = mailDirectory;
createHome = true; createHome = true;
@ -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}"

33
nix/sources.json Normal file
View file

@ -0,0 +1,33 @@
{
"blobs": {
"sha256": "1g687x3b2r4ar5i4xyav5qzpy9fp1phx9wf70f4j3scwny0g7hn1",
"type": "tarball",
"url": "https://gitlab.com/simple-nixos-mailserver/blobs/-/archive/2cccdf1ca48316f2cfd1c9a0017e8de5a7156265/blobs-2cccdf1ca48316f2cfd1c9a0017e8de5a7156265.tar.gz",
"url_template": "https://gitlab.com/simple-nixos-mailserver/blobs/-/archive/<version>/blobs-<version>.tar.gz",
"version": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265"
},
"nixpkgs-20.09": {
"branch": "release-20.09",
"description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to",
"homepage": "https://github.com/NixOS/nixpkgs",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b6eefa48d8e10491e43c0c6155ac12b463f6fed3",
"sha256": "0hrp7gshy62bsj719xd6hk6z284pzr8ksw1vvxvyfrffq1f7d8k9",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/b6eefa48d8e10491e43c0c6155ac12b463f6fed3.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nixpkgs-unstable": {
"branch": "master",
"description": "A read-only mirror of NixOS/nixpkgs tracking the released channels. Send issues and PRs to",
"homepage": "https://github.com/NixOS/nixpkgs",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ee9154c4c1a51a6b1129e8bf8b728ab6432eda3e",
"sha256": "1j1laj1b44022k2jmidp0gb8jx1yp4rsjgzcxmxkwlkv7b04bza3",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/ee9154c4c1a51a6b1129e8bf8b728ab6432eda3e.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
}
}

134
nix/sources.nix Normal file
View file

@ -0,0 +1,134 @@
# This file has been generated by Niv.
let
#
# The fetchers. fetch_<type> fetches specs of type <type>.
#
fetch_file = pkgs: spec:
if spec.builtin or true then
builtins_fetchurl { inherit (spec) url sha256; }
else
pkgs.fetchurl { inherit (spec) url sha256; };
fetch_tarball = pkgs: spec:
if spec.builtin or true then
builtins_fetchTarball { inherit (spec) url sha256; }
else
pkgs.fetchzip { inherit (spec) url sha256; };
fetch_git = spec:
builtins.fetchGit { url = spec.repo; inherit (spec) rev ref; };
fetch_builtin-tarball = spec:
builtins.trace
''
WARNING:
The niv type "builtin-tarball" will soon be deprecated. You should
instead use `builtin = true`.
$ niv modify <package> -a type=tarball -a builtin=true
''
builtins_fetchTarball { inherit (spec) url sha256; };
fetch_builtin-url = spec:
builtins.trace
''
WARNING:
The niv type "builtin-url" will soon be deprecated. You should
instead use `builtin = true`.
$ niv modify <package> -a type=file -a builtin=true
''
(builtins_fetchurl { inherit (spec) url sha256; });
#
# Various helpers
#
# The set of packages used when specs are fetched using non-builtins.
mkPkgs = sources:
let
sourcesNixpkgs =
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) {};
hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
hasThisAsNixpkgsPath = <nixpkgs> == ./.;
in
if builtins.hasAttr "nixpkgs" sources
then sourcesNixpkgs
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
import <nixpkgs> {}
else
abort
''
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
add a package called "nixpkgs" to your sources.json.
'';
# The actual fetching function.
fetch = pkgs: name: spec:
if ! builtins.hasAttr "type" spec then
abort "ERROR: niv spec ${name} does not have a 'type' attribute"
else if spec.type == "file" then fetch_file pkgs spec
else if spec.type == "tarball" then fetch_tarball pkgs spec
else if spec.type == "git" then fetch_git spec
else if spec.type == "builtin-tarball" then fetch_builtin-tarball spec
else if spec.type == "builtin-url" then fetch_builtin-url spec
else
abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
# Ports of functions for older nix versions
# a Nix version of mapAttrs if the built-in doesn't exist
mapAttrs = builtins.mapAttrs or (
f: set: with builtins;
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
);
# fetchTarball version that is compatible between all the versions of Nix
builtins_fetchTarball = { url, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchTarball;
in
if lessThan nixVersion "1.12" then
fetchTarball { inherit url; }
else
fetchTarball attrs;
# fetchurl version that is compatible between all the versions of Nix
builtins_fetchurl = { url, sha256 }@attrs:
let
inherit (builtins) lessThan nixVersion fetchurl;
in
if lessThan nixVersion "1.12" then
fetchurl { inherit url; }
else
fetchurl attrs;
# Create the final "sources" from the config
mkSources = config:
mapAttrs (
name: spec:
if builtins.hasAttr "outPath" spec
then abort
"The values in sources.json should not have an 'outPath' attribute"
else
spec // { outPath = fetch config.pkgs name spec; }
) config.sources;
# The "config" used by the fetchers
mkConfig =
{ sourcesFile ? ./sources.json
, sources ? builtins.fromJSON (builtins.readFile sourcesFile)
, pkgs ? mkPkgs sources
}: rec {
# The sources, i.e. the attribute set of spec name to spec
inherit sources;
# The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
inherit pkgs;
};
in
mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }

View file

@ -1,82 +0,0 @@
import json
import sys
header = """
# Mailserver options
## `mailserver`
"""
template = """
`````{{option}} {key}
{description}
{type}
{default}
{example}
`````
"""
f = open(sys.argv[1])
options = json.load(f)
groups = ["mailserver.loginAccounts",
"mailserver.certificate",
"mailserver.dkim",
"mailserver.dmarcReporting",
"mailserver.fullTextSearch",
"mailserver.redis",
"mailserver.ldap",
"mailserver.monitoring",
"mailserver.backup",
"mailserver.borgbackup"]
def render_option_value(opt, attr):
if attr in opt:
if isinstance(opt[attr], dict) and '_type' in opt[attr]:
if opt[attr]['_type'] == 'literalExpression':
if '\n' in opt[attr]['text']:
res = '\n```nix\n' + opt[attr]['text'].rstrip('\n') + '\n```'
else:
res = '```{}```'.format(opt[attr]['text'])
elif opt[attr]['_type'] == 'literalMD':
res = opt[attr]['text']
else:
s = str(opt[attr])
if s == "":
res = '`""`'
elif '\n' in s:
res = '\n```\n' + s.rstrip('\n') + '\n```'
else:
res = '```{}```'.format(s)
res = '- ' + attr + ': ' + res
else:
res = ""
return res
def print_option(opt):
if isinstance(opt['description'], dict) and '_type' in opt['description']: # mdDoc
description = opt['description']['text']
else:
description = opt['description']
print(template.format(
key=opt['name'],
description=description or "",
type="- type: ```{}```".format(opt['type']),
default=render_option_value(opt, 'default'),
example=render_option_value(opt, 'example')))
print(header)
for opt in options:
if any([opt['name'].startswith(c) for c in groups]):
continue
print_option(opt)
for c in groups:
print('## `{}`'.format(c))
print()
for opt in options:
if opt['name'].startswith(c):
print_option(opt)

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

@ -1,10 +1 @@
(import (import (builtins.fetchGit "https://github.com/edolstra/flake-compat") { src = ./.; }).shellNix
(
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
}
)
{ src = ./.; }
).shellNix

View file

@ -14,12 +14,19 @@
# 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/>
{ pkgs ? import <nixpkgs> {}, blobs}: { pkgs ? import <nixpkgs> {}}:
pkgs.nixosTest { pkgs.nixosTest {
name = "clamav"; name = "clamav";
nodes = { nodes = {
server = { config, pkgs, lib, ... }: server = { config, pkgs, lib, ... }:
let
sources = import ../nix/sources.nix;
blobs = pkgs.fetchzip {
url = sources.blobs.url;
sha256 = sources.blobs.sha256;
};
in
{ {
imports = [ imports = [
../default.nix ../default.nix
@ -189,10 +196,10 @@ pkgs.nixosTest {
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket. # TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
server.wait_until_succeeds( server.wait_until_succeeds(
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" "timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
) )
server.wait_until_succeeds( server.wait_until_succeeds(
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]" "timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
) )
client.execute("cp -p /etc/root/.* ~/") client.execute("cp -p /etc/root/.* ~/")
@ -217,12 +224,12 @@ pkgs.nixosTest {
with subtest("virus scan file"): with subtest("virus scan file"):
server.succeed( server.succeed(
'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2' 'clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2'
) )
with subtest("virus scan email"): with subtest("virus scan email"):
client.succeed( client.succeed(
'set +o pipefail; msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2' 'msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
) )
server.succeed("journalctl -u rspamd | grep -i eicar") server.succeed("journalctl -u rspamd | grep -i eicar")
# give the mail server some time to process the mail # give the mail server some time to process the mail

47
tests/default.nix Normal file
View file

@ -0,0 +1,47 @@
# Generate an attribute sets containing all tests for all releaeses
# It looks like:
# - extern.nixpkgs_20.03
# - extern.nixpkgs_unstable
# - intern.nixpkgs_20.03
# - intern.nixpkgs_unstable
with builtins;
let
sources = import ../nix/sources.nix;
releases = listToAttrs (map genRelease releaseNames);
genRelease = name: {
name = name;
value = import sources."${name}" {};
};
genTest = testName: release:
let
pkgs = releases."${release}";
test = pkgs.callPackage (./. + "/${testName}.nix") { };
in {
"name"= builtins.replaceStrings ["." "-"] ["_" "_"] release;
"value"= test;
};
releaseNames = [
"nixpkgs-unstable"
"nixpkgs-20.09"
];
testNames = [
"intern"
"extern"
"clamav"
"multiple"
];
# Generate an attribute set containing one test per releases
genTests = testName: {
name = testName;
value = listToAttrs (map (genTest testName) (builtins.attrNames releases));
};
in listToAttrs (map genTests testNames)

View file

@ -14,10 +14,10 @@
# 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/>
{ pkgs ? import <nixpkgs> {}, ...}: { pkgs ? import <nixpkgs> {}}:
pkgs.nixosTest { pkgs.nixosTest {
name = "external"; name = "extern";
nodes = { nodes = {
server = { config, pkgs, ... }: server = { config, pkgs, ... }:
{ {
@ -43,11 +43,6 @@ pkgs.nixosTest {
domains = [ "example.com" "example2.com" ]; domains = [ "example.com" "example2.com" ];
rewriteMessageId = true; rewriteMessageId = true;
dkimKeyBits = 1535; dkimKeyBits = 1535;
dmarcReporting = {
enable = true;
domain = "example.com";
organizationName = "ACME Corp";
};
loginAccounts = { loginAccounts = {
"user1@example.com" = { "user1@example.com" = {
@ -81,8 +76,6 @@ pkgs.nixosTest {
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201 # special use depends on https://github.com/NixOS/nixpkgs/pull/93201
autoIndexExclude = [ (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") ]; autoIndexExclude = [ (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") ];
enforced = "yes"; enforced = "yes";
# fts-xapian warns when memory is low, which makes the test fail
memoryLimit = 100000;
}; };
}; };
}; };
@ -350,7 +343,7 @@ pkgs.nixosTest {
# TODO put this blocking into the systemd units? # TODO put this blocking into the systemd units?
server.wait_until_succeeds( server.wait_until_succeeds(
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" "timeout 1 ${nodes.server.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
) )
client.execute("cp -p /etc/root/.* ~/") client.execute("cp -p /etc/root/.* ~/")
@ -494,13 +487,12 @@ pkgs.nixosTest {
client.fail("search Junk a >&2") client.fail("search Junk a >&2")
# check that search really goes through the indexer # check that search really goes through the indexer
server.succeed( server.succeed(
"journalctl -u dovecot2 | grep -E 'indexer-worker.* Done indexing .INBOX.' >&2" "journalctl -u dovecot2 | grep -E 'indexer-worker.*Indexed . messages in INBOX' >&2"
) )
# check that Junk is not indexed # check that Junk is not indexed
server.fail("journalctl -u dovecot2 | grep 'indexer-worker' | grep -i 'JUNK' >&2") server.fail(
"journalctl -u dovecot2 | grep -E 'indexer-worker.*Indexed . messages in Junk' >&2"
with subtest("dmarc reporting"): )
server.systemctl("start rspamd-dmarc-reporter.service")
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 +500,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

@ -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/>
{ pkgs ? import <nixpkgs> {}, ...}: { pkgs ? import <nixpkgs> {}}:
let let
sendMail = pkgs.writeTextFile { sendMail = pkgs.writeTextFile {
@ -29,15 +29,15 @@ let
hashPassword = password: pkgs.runCommand hashPassword = password: pkgs.runCommand
"password-${password}-hashed" "password-${password}-hashed"
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } '' { buildInputs = [ pkgs.apacheHttpd ]; } ''
mkpasswd -sm bcrypt <<<"$password" > $out htpasswd -nbB "" "${password}" | cut -d: -f2 > $out
''; '';
hashedPasswordFile = hashPassword "my-password"; hashedPasswordFile = hashPassword "my-password";
passwordFile = pkgs.writeText "password" "my-password"; passwordFile = pkgs.writeText "password" "my-password";
in in
pkgs.nixosTest { pkgs.nixosTest {
name = "internal"; name = "intern";
nodes = { nodes = {
machine = { config, pkgs, ... }: { machine = { config, pkgs, ... }: {
imports = [ imports = [
@ -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")
@ -174,15 +133,15 @@ pkgs.nixosTest {
machine.wait_for_open_port(25) machine.wait_for_open_port(25)
# TODO put this blocking into the systemd units # TODO put this blocking into the systemd units
machine.wait_until_succeeds( machine.wait_until_succeeds(
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" "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"):
machine.succeed( machine.succeed(
"set +o pipefail; ${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'" "${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'"
) )
with subtest("imap port 143 is closed and imaps is serving SSL"): with subtest("imap port 143 is closed and imaps is serving SSL"):

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

@ -14,9 +14,9 @@
# 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/>
import <nixpkgs/nixos/tests/make-test-python.nix> { import <nixpkgs/nixos/tests/make-test.nix> {
nodes.machine = machine =
{ config, pkgs, ... }: { config, pkgs, ... }:
{ {
imports = [ imports = [
@ -26,6 +26,6 @@ import <nixpkgs/nixos/tests/make-test-python.nix> {
testScript = testScript =
'' ''
machine.wait_for_unit("multi-user.target"); $machine->waitForUnit("multi-user.target");
''; '';
} }

View file

@ -1,13 +1,13 @@
# This tests is used to test features requiring several mail domains. # This tests is used to test features requiring several mail domains.
{ pkgs ? import <nixpkgs> {}, ...}: { pkgs ? import <nixpkgs> {}}:
let let
hashPassword = password: pkgs.runCommand hashPassword = password: pkgs.runCommand
"password-${password}-hashed" "password-${password}-hashed"
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } { buildInputs = [ pkgs.apacheHttpd ]; }
'' ''
mkpasswd -sm bcrypt <<<"$password" > $out htpasswd -nbB "" "${password}" | cut -d: -f2 > $out
''; '';
password = pkgs.writeText "password" "password"; password = pkgs.writeText "password" "password";
@ -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
@ -70,10 +68,10 @@ pkgs.nixosTest {
# TODO put this blocking into the systemd units? # TODO put this blocking into the systemd units?
domain1.wait_until_succeeds( domain1.wait_until_succeeds(
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" "timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
) )
domain2.wait_until_succeeds( domain2.wait_until_succeeds(
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]" "timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
) )
# user@domain1.com sends a mail to user@domain2.com # user@domain1.com sends a mail to user@domain2.com