Compare commits
84 commits
Author | SHA1 | Date | |
---|---|---|---|
a98a93cf22 | |||
806a4cfd21 | |||
|
059b50b2e7 | ||
|
290a995de5 | ||
|
54cbacb6eb | ||
|
29916981e7 | ||
|
0d51a32e47 | ||
|
ed80b589d3 | ||
|
46a0829aa8 | ||
|
41059fc548 | ||
|
ef4756bcfc | ||
|
9f6635a035 | ||
|
79c8cfcd58 | ||
|
799fe34c12 | ||
|
d507bd9c95 | ||
|
fe6d325397 | ||
|
572c1b4d69 | ||
|
9e36323ae3 | ||
|
e47f3719f1 | ||
|
b5023b36a1 | ||
|
3f526c08e8 | ||
|
008d78cc21 | ||
|
84783b661e | ||
|
93221e4b25 | ||
|
c63f6e7b05 | ||
|
a3b03d1b5a | ||
|
69a4b7ad67 | ||
|
71b4c62d85 | ||
|
6775502be3 | ||
|
7695c856f1 | ||
|
fb3210b932 | ||
|
33554e57ce | ||
|
8b03ae5701 | ||
|
42e245b069 | ||
|
08f077c5ca | ||
|
d460e9ff62 | ||
|
0c1801b489 | ||
|
24128c3052 | ||
|
c4ec122aac | ||
|
131c48de9b | ||
|
290d00f6db | ||
|
7e09d8f537 | ||
|
1bcfcf786b | ||
|
a948c49ca7 | ||
|
42c5564791 | ||
|
fd605a419b | ||
|
d8131ffc61 | ||
|
bd99079363 | ||
|
c04e4f22da | ||
|
e2ca6e45f3 | ||
|
6d0d9fb966 | ||
|
0bbb2ac74e | ||
|
4fcab839d7 | ||
|
bc667fb6af | ||
|
31eadb6388 | ||
|
033b3d2a45 | ||
|
694e7d34f6 | ||
|
fe36e7ae0d | ||
|
3f0b7a1b5c | ||
|
737eb4f398 | ||
|
a40e9c3abb | ||
|
004c229ca4 | ||
|
f535d8123c | ||
|
15cf252a0d | ||
|
6284a20f77 | ||
|
4396125ebb | ||
|
4ce864f52a | ||
|
75728d2686 | ||
|
7de138037f | ||
|
021b5c8f73 | ||
|
46ef908c91 | ||
|
53af883255 | ||
|
4ed684481b | ||
|
f4c14572fc | ||
|
ef03562eba | ||
|
11ad4742aa | ||
|
665aa181e6 | ||
|
6e3a7b2ea6 | ||
|
f3d967f830 | ||
|
7c7ed5ce06 | ||
|
822c5f22bd | ||
|
4f0f0128d8 | ||
|
6e8142862f | ||
|
a13526a6e3 |
41 changed files with 1321 additions and 1634 deletions
17
.forgejo/workflows/build.yaml
Normal file
17
.forgejo/workflows/build.yaml
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# deploy it upstream
|
||||||
|
deploy:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- name: "Deploy to Skynet"
|
||||||
|
uses: https://forgejo.skynet.ie/Skynet/actions-deploy-to-skynet@v2
|
||||||
|
with:
|
||||||
|
input: 'simple-nixos-mailserver'
|
||||||
|
token: ${{ secrets.API_TOKEN_FORGEJO }}
|
|
@ -3,11 +3,11 @@ hydra-pr:
|
||||||
- merge_requests
|
- merge_requests
|
||||||
image: nixos/nix
|
image: nixos/nix
|
||||||
script:
|
script:
|
||||||
- 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}
|
- 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}'
|
||||||
|
|
||||||
hydra-master:
|
hydra-master:
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
image: nixos/nix
|
image: nixos/nix
|
||||||
script:
|
script:
|
||||||
- nix run -f channel:nixos-unstable hydra-cli -c hydra-cli -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver master
|
- 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'
|
||||||
|
|
|
@ -17,35 +17,6 @@ let
|
||||||
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
|
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
|
||||||
}
|
}
|
||||||
) prs;
|
) prs;
|
||||||
# This could be removed once branch 20.09 and 21.05 would have been
|
|
||||||
# removed.
|
|
||||||
mkJobset = branch: {
|
|
||||||
description = "Build ${branch} branch of Simple NixOS MailServer";
|
|
||||||
checkinterval = "60";
|
|
||||||
enabled = "1";
|
|
||||||
schedulingshares = 100;
|
|
||||||
enableemail = false;
|
|
||||||
emailoverride = "";
|
|
||||||
nixexprinput = "snm";
|
|
||||||
nixexprpath = ".hydra/default.nix";
|
|
||||||
type = 0;
|
|
||||||
inputs = {
|
|
||||||
# This is only used to allow Niv to use pkgs.fetchzip which is
|
|
||||||
# required because of Hydra restricted evaluation mode.
|
|
||||||
nixpkgs = {
|
|
||||||
value = "https://github.com/NixOS/nixpkgs b6eefa48d8e10491e43c0c6155ac12b463f6fed3";
|
|
||||||
type = "git";
|
|
||||||
emailresponsible = false;
|
|
||||||
};
|
|
||||||
snm = {
|
|
||||||
value = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver ${branch}";
|
|
||||||
type = "git";
|
|
||||||
emailresponsible = false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
keepnr = 3;
|
|
||||||
hidden = false;
|
|
||||||
};
|
|
||||||
mkFlakeJobset = branch: {
|
mkFlakeJobset = branch: {
|
||||||
description = "Build ${branch} branch of Simple NixOS MailServer";
|
description = "Build ${branch} branch of Simple NixOS MailServer";
|
||||||
checkinterval = "60";
|
checkinterval = "60";
|
||||||
|
@ -61,8 +32,8 @@ let
|
||||||
|
|
||||||
desc = prJobsets // {
|
desc = prJobsets // {
|
||||||
"master" = mkFlakeJobset "master";
|
"master" = mkFlakeJobset "master";
|
||||||
"nixos-20.09" = mkJobset "nixos-20.09";
|
"nixos-23.11" = mkFlakeJobset "nixos-23.11";
|
||||||
"nixos-21.05" = mkJobset "nixos-21.05";
|
"nixos-24.05" = mkFlakeJobset "nixos-24.05";
|
||||||
};
|
};
|
||||||
|
|
||||||
log = {
|
log = {
|
||||||
|
|
|
@ -5,9 +5,17 @@
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-20.04
|
os: ubuntu-22.04
|
||||||
tools:
|
tools:
|
||||||
python: "3.9"
|
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:
|
sphinx:
|
||||||
configuration: docs/conf.py
|
configuration: docs/conf.py
|
||||||
|
|
90
README.md
90
README.md
|
@ -8,29 +8,21 @@
|
||||||
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 21.05
|
* For NixOS 24.05
|
||||||
- Use the [SNM branch `nixos-21.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-21.05)
|
- Use the [SNM branch `nixos-24.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.05)
|
||||||
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-21.05/)
|
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/)
|
||||||
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-21.05/release-notes.html#nixos-21-05)
|
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.05/release-notes.html#nixos-24-05)
|
||||||
* For NixOS 20.09
|
* For NixOS 23.11
|
||||||
- Use the [SNM branch `nixos-20.09`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-20.09)
|
- Use the [SNM branch `nixos-23.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.11)
|
||||||
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/)
|
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.11/)
|
||||||
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-20.09/release-notes.html#nixos-20-09)
|
- [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 21.05 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. All announcements are signed by
|
can stay up to date with bug fixes and updates.
|
||||||
the gpg key with fingerprint
|
|
||||||
|
|
||||||
```
|
|
||||||
D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
@ -79,71 +71,15 @@ D9FE 4119 F082 6F15 93BD BD36 6162 DBA5 635E A16A
|
||||||
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
|
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
|
||||||
- Join the Libera Chat IRC channel `#nixos-mailserver`
|
- Join the Libera Chat IRC channel `#nixos-mailserver`
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
```nix
|
|
||||||
{ config, pkgs, ... }:
|
|
||||||
let release = "nixos-21.05";
|
|
||||||
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.
|
|
||||||
|
|
||||||
## How to Backup
|
Check out the [Setup Guide](https://nixos-mailserver.readthedocs.io/en/latest/setup-guide.html) in the project's documentation.
|
||||||
|
|
||||||
Checkout the [Complete Backup Guide](https://nixos-mailserver.readthedocs.io/en/latest/backup-guide.html). Backups are easy with `SNM`.
|
For a complete list of options, [see in readthedocs](https://nixos-mailserver.readthedocs.io/en/latest/options.html).
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) wiki page.
|
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation 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)
|
||||||
|
@ -158,6 +94,4 @@ See the [contributor tab](https://gitlab.com/simple-nixos-mailserver/nixos-mails
|
||||||
* Logo made with [Logomakr.com](https://logomakr.com)
|
* Logo made with [Logomakr.com](https://logomakr.com)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[logo]: docs/logo.png
|
[logo]: docs/logo.png
|
||||||
|
|
392
default.nix
392
default.nix
|
@ -44,6 +44,17 @@ 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;
|
||||||
|
@ -65,14 +76,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 `htpasswd` as follows
|
The user's hashed password. Use `mkpasswd` as follows
|
||||||
|
|
||||||
```
|
```
|
||||||
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
|
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
|
||||||
```
|
```
|
||||||
|
|
||||||
Warning: this is stored in plaintext in the Nix store!
|
Warning: this is stored in plaintext in the Nix store!
|
||||||
Use `hashedPasswordFile` instead.
|
Use {option}`mailserver.loginAccounts.<name>.hashedPasswordFile` instead.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -81,10 +92,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 `htpasswd` as follows
|
A file containing the user's hashed password. Use `mkpasswd` as follows
|
||||||
|
|
||||||
```
|
```
|
||||||
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
|
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -100,6 +111,15 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
aliasesRegexp = mkOption {
|
||||||
|
type = with types; listOf types.str;
|
||||||
|
example = [''/^tom\..*@domain\.com$/''];
|
||||||
|
default = [];
|
||||||
|
description = ''
|
||||||
|
Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
catchAll = mkOption {
|
catchAll = mkOption {
|
||||||
type = with types; listOf (enum cfg.domains);
|
type = with types; listOf (enum cfg.domains);
|
||||||
example = ["example.com" "example2.com"];
|
example = ["example.com" "example2.com"];
|
||||||
|
@ -149,7 +169,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.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -177,23 +197,174 @@ 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 `htpasswd` as
|
e.g. `user1@example.com`. To generate the passwords use `mkpasswd` as
|
||||||
follows
|
follows
|
||||||
|
|
||||||
```
|
```
|
||||||
nix run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
|
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
|
||||||
```
|
```
|
||||||
'';
|
'';
|
||||||
default = {};
|
default = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ldap = {
|
||||||
|
enable = mkEnableOption "LDAP support";
|
||||||
|
|
||||||
|
uris = mkOption {
|
||||||
|
type = types.listOf types.str;
|
||||||
|
example = literalExpression ''
|
||||||
|
[
|
||||||
|
"ldaps://ldap1.example.com"
|
||||||
|
"ldaps://ldap2.example.com"
|
||||||
|
]
|
||||||
|
'';
|
||||||
|
description = ''
|
||||||
|
URIs where your LDAP server can be reached
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
startTls = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether to enable StartTLS upon connection to the server.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
tlsCAFile = mkOption {
|
||||||
|
type = types.path;
|
||||||
|
default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||||
|
defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
|
||||||
|
description = ''
|
||||||
|
Certifificate trust anchors used to verify the LDAP server certificate.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
bind = {
|
||||||
|
dn = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = "cn=mail,ou=accounts,dc=example,dc=com";
|
||||||
|
description = ''
|
||||||
|
Distinguished name used by the mail server to do lookups
|
||||||
|
against the LDAP servers.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = "/run/my-secret";
|
||||||
|
description = ''
|
||||||
|
A file containing the password required to authenticate against the LDAP servers.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
searchBase = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = "ou=people,ou=accounts,dc=example,dc=com";
|
||||||
|
description = ''
|
||||||
|
Base DN at below which to search for users accounts.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
searchScope = mkOption {
|
||||||
|
type = types.enum [ "sub" "base" "one" ];
|
||||||
|
default = "sub";
|
||||||
|
description = ''
|
||||||
|
Search scope below which users accounts are looked for.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
dovecot = {
|
||||||
|
userAttrs = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = "";
|
||||||
|
description = ''
|
||||||
|
LDAP attributes to be retrieved during userdb lookups.
|
||||||
|
|
||||||
|
See the users_attrs reference at
|
||||||
|
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-attrs
|
||||||
|
in the Dovecot manual.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
userFilter = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mail=%u";
|
||||||
|
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
|
||||||
|
description = ''
|
||||||
|
Filter for user lookups in Dovecot.
|
||||||
|
|
||||||
|
See the user_filter reference at
|
||||||
|
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-filter
|
||||||
|
in the Dovecot manual.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
passAttrs = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "userPassword=password";
|
||||||
|
description = ''
|
||||||
|
LDAP attributes to be retrieved during passdb lookups.
|
||||||
|
|
||||||
|
See the pass_attrs reference at
|
||||||
|
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-attrs
|
||||||
|
in the Dovecot manual.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
passFilter = mkOption {
|
||||||
|
type = types.nullOr types.str;
|
||||||
|
default = "mail=%u";
|
||||||
|
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
|
||||||
|
description = ''
|
||||||
|
Filter for password lookups in Dovecot.
|
||||||
|
|
||||||
|
See the pass_filter reference for
|
||||||
|
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-filter
|
||||||
|
in the Dovecot manual.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
postfix = {
|
||||||
|
filter = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mail=%s";
|
||||||
|
example = "(&(objectClass=inetOrgPerson)(mail=%s))";
|
||||||
|
description = ''
|
||||||
|
LDAP filter used to search for an account by mail, where
|
||||||
|
`%s` is a substitute for the address in
|
||||||
|
question.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
uidAttribute = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mail";
|
||||||
|
example = "uid";
|
||||||
|
description = ''
|
||||||
|
The LDAP attribute referencing the account name for a user.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
mailAttribute = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "mail";
|
||||||
|
description = ''
|
||||||
|
The LDAP attribute holding mail addresses for a user.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
indexDir = mkOption {
|
indexDir = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = null;
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
Folder to store search indices. If null, indices are stored
|
Folder to store search indices. If null, indices are stored
|
||||||
along with email, which could not necessarily be desirable,
|
along with email, which could not necessarily be desirable,
|
||||||
especially when the fullTextSearch option is enable since
|
especially when {option}`mailserver.fullTextSearch.enable` is `true` since
|
||||||
indices it creates are voluminous and do not need to be backed
|
indices it creates are voluminous and do not need to be backed
|
||||||
up.
|
up.
|
||||||
|
|
||||||
|
@ -235,8 +406,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
|
||||||
<literal>body</literal>, then only body searches (as opposed to
|
`body`, then only body searches (as opposed to
|
||||||
header) are affected. If set to <literal>no<literal>, searches may
|
header) are affected. If set to `no`, searches may
|
||||||
fall back to a very slow brute force search.
|
fall back to a very slow brute force search.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -274,7 +445,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 <literal>onCalendar</literal>, but plus or minus this many seconds.";
|
description = "Run the maintenance job not exactly at the time specified with `onCalendar`, but plus or minus this many seconds.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -293,7 +464,6 @@ 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 = {
|
||||||
|
@ -326,7 +496,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
|
||||||
`extraVirtualAliases` option is that `user@elsewhere.com`
|
{option}`mailserver.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.
|
||||||
'';
|
'';
|
||||||
|
@ -360,7 +530,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`.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -403,6 +573,14 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useUTF8FolderNames = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Store mailbox names on disk using UTF-8 instead of modified UTF-7 (mUTF-7).
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
hierarchySeparator = mkOption {
|
hierarchySeparator = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = ".";
|
default = ".";
|
||||||
|
@ -441,19 +619,26 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
certificateScheme = mkOption {
|
certificateScheme = let
|
||||||
type = types.enum [ 1 2 3 ];
|
schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ];
|
||||||
default = 2;
|
translate = i: warn "Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${(builtins.elemAt schemes (i - 1))}\"'."
|
||||||
|
(builtins.elemAt schemes (i - 1));
|
||||||
|
in mkOption {
|
||||||
|
type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes);
|
||||||
|
default = "selfsigned";
|
||||||
description = ''
|
description = ''
|
||||||
Certificate Files. There are three options for these.
|
The scheme to use for managing TLS certificates:
|
||||||
|
|
||||||
1) You specify locations and manually copy certificates there.
|
1. `manual`: you specify locations via {option}`mailserver.certificateFile` and
|
||||||
2) You let the server create new (self signed) certificates on the fly.
|
{option}`mailserver.keyFile` and manually copy certificates there.
|
||||||
3) You let the server create a certificate via `Let's Encrypt`. Note that
|
2. `selfsigned`: you let the server create new (self-signed) certificates on the fly.
|
||||||
this implies that a stripped down webserver has to be started. This also
|
3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org)
|
||||||
implies that the FQDN must be set as an `A` record to point to the IP of
|
via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for
|
||||||
the server. In particular port 80 on the server will be opened. For details
|
{option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly
|
||||||
on how to set up the domain records, see the guide in the readme.
|
configured to point to your server (see the [setup guide](setup-guide.rst) for more information).
|
||||||
|
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.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -461,8 +646,9 @@ in
|
||||||
type = types.path;
|
type = types.path;
|
||||||
example = "/root/mail-server.crt";
|
example = "/root/mail-server.crt";
|
||||||
description = ''
|
description = ''
|
||||||
Scheme 1)
|
({option}`mailserver.certificateScheme` == `manual`)
|
||||||
Location of the certificate
|
|
||||||
|
Location of the certificate.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -470,8 +656,9 @@ in
|
||||||
type = types.path;
|
type = types.path;
|
||||||
example = "/root/mail-server.key";
|
example = "/root/mail-server.key";
|
||||||
description = ''
|
description = ''
|
||||||
Scheme 1)
|
({option}`mailserver.certificateScheme` == `manual`)
|
||||||
Location of the key file
|
|
||||||
|
Location of the key file.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -479,13 +666,27 @@ in
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = "/var/certs";
|
default = "/var/certs";
|
||||||
description = ''
|
description = ''
|
||||||
Scheme 2)
|
({option}`mailserver.certificateScheme` == `selfsigned`)
|
||||||
This is the folder where the certificate will be created. The name is
|
|
||||||
hardcoded to "cert-<domain>.pem" and "key-<domain>.pem" and the
|
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
|
||||||
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;
|
||||||
|
@ -575,7 +776,7 @@ in
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "mail";
|
default = "mail";
|
||||||
description = ''
|
description = ''
|
||||||
|
The DKIM selector.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -583,7 +784,7 @@ in
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = "/var/dkim";
|
default = "/var/dkim";
|
||||||
description = ''
|
description = ''
|
||||||
|
The DKIM directory.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -594,7 +795,7 @@ 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 (dkimSelector). In order to get
|
here, then you should use a different selector ({option}`mailserver.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.
|
||||||
'';
|
'';
|
||||||
|
@ -620,6 +821,67 @@ in
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
@ -661,7 +923,7 @@ in
|
||||||
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.bind;
|
cf = config.services.redis.servers.rspamd.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" ]);
|
||||||
|
@ -670,28 +932,27 @@ 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. The default value
|
Address that rspamd should use to contact redis.
|
||||||
is read from <literal>config.services.redis.bind</literal>.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
type = types.port;
|
type = types.port;
|
||||||
default = config.services.redis.port;
|
default = config.services.redis.servers.rspamd.port;
|
||||||
|
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.port";
|
||||||
description = ''
|
description = ''
|
||||||
Port that rspamd should use to contact redis. The default value is
|
Port that rspamd should use to contact redis.
|
||||||
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.requirePass;
|
default = config.services.redis.servers.rspamd.requirePass;
|
||||||
|
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.requirePass";
|
||||||
description = ''
|
description = ''
|
||||||
Password that rspamd should use to contact redis, or null if not
|
Password that rspamd should use to contact redis, or null if not required.
|
||||||
required. The default value is read from
|
|
||||||
<literal>config.services.redis.requirePass<literal>.
|
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -706,10 +967,25 @@ 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 = "config.mailserver.fqdn";
|
defaultText = lib.literalMD "{option}`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
|
||||||
|
@ -725,7 +1001,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
|
||||||
`fqdn` (which, for SSL reasons, should generally be the name
|
{option}`mailserver.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
|
||||||
|
@ -793,10 +1069,11 @@ 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 pidfile /var/run/rspamd.pid
|
check process rspamd with matching "rspamd: main process"
|
||||||
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 ...'.
|
||||||
|
@ -813,7 +1090,8 @@ 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 cmdPreexec and cmdPostexec.
|
It is exported and thus available as an environment variable to
|
||||||
|
{option}`mailserver.borgbackup.cmdPreexec` and {option}`mailserver.borgbackup.cmdPostexec`.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -873,13 +1151,14 @@ 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.";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -895,6 +1174,7 @@ 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.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -915,8 +1195,9 @@ 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"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -927,7 +1208,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.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -940,7 +1221,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 {
|
||||||
|
@ -1015,6 +1296,7 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
imports = [
|
imports = [
|
||||||
|
./mail-server/assertions.nix
|
||||||
./mail-server/borgbackup.nix
|
./mail-server/borgbackup.nix
|
||||||
./mail-server/debug.nix
|
./mail-server/debug.nix
|
||||||
./mail-server/rsnapshot.nix
|
./mail-server/rsnapshot.nix
|
||||||
|
|
|
@ -4,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, as generated by `mkpasswd`, are no longer supported. Instead
|
crypt passwords are no longer supported. Instead bcrypt passwords
|
||||||
bcrypt passwords have to be used which can be generated using `htpasswd`.
|
have to be used. These can still be generated using `mkpasswd -m bcrypt`.
|
||||||
|
|
||||||
.. code:: nix
|
.. code:: nix
|
||||||
|
|
||||||
|
@ -24,12 +24,13 @@ bcrypt passwords have to be used which can be generated using `htpasswd`.
|
||||||
in {
|
in {
|
||||||
services.radicale = {
|
services.radicale = {
|
||||||
enable = true;
|
enable = true;
|
||||||
config = ''
|
settings = {
|
||||||
[auth]
|
auth = {
|
||||||
type = htpasswd
|
type = "htpasswd";
|
||||||
htpasswd_filename = ${htpasswd}
|
htpasswd_filename = "${htpasswd}";
|
||||||
htpasswd_encryption = bcrypt
|
htpasswd_encryption = "bcrypt";
|
||||||
'';
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
services.nginx = {
|
services.nginx = {
|
||||||
|
|
32
docs/add-roundcube.rst
Normal file
32
docs/add-roundcube.rst
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
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 ];
|
||||||
|
}
|
18
docs/autodiscovery.rst
Normal file
18
docs/autodiscovery.rst
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
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.
|
||||||
|
|
12
docs/conf.py
12
docs/conf.py
|
@ -18,7 +18,7 @@
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'NixOS Mailserver'
|
project = 'NixOS Mailserver'
|
||||||
copyright = '2020, NixOS Mailserver Contributors'
|
copyright = '2022, NixOS Mailserver Contributors'
|
||||||
author = 'NixOS Mailserver Contributors'
|
author = 'NixOS Mailserver Contributors'
|
||||||
|
|
||||||
|
|
||||||
|
@ -28,8 +28,16 @@ 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']
|
||||||
|
|
||||||
|
@ -50,4 +58,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 = ['_static']
|
html_static_path = []
|
||||||
|
|
|
@ -42,7 +42,7 @@ Indices created by the full text search feature can take more disk
|
||||||
space than the emails themselves. By default, they are kept in the
|
space than the emails themselves. By default, they are kept in the
|
||||||
emails location. When enabling the full text search feature, it is
|
emails location. When enabling the full text search feature, it is
|
||||||
recommended to move indices in a different location, such as
|
recommended to move indices in a different location, such as
|
||||||
(``/var/lib/docecot/indices/%d/%n``) by using the option
|
(``/var/lib/dovecot/indices``) by using the option
|
||||||
``mailserver.indexDir``.
|
``mailserver.indexDir``.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
|
@ -10,7 +10,7 @@ Run NixOS tests
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
To run the test suite, you need to enable `Nix Flakes
|
To run the test suite, you need to enable `Nix Flakes
|
||||||
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`.
|
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`_.
|
||||||
|
|
||||||
You can then run the testsuite via
|
You can then run the testsuite via
|
||||||
|
|
||||||
|
@ -30,28 +30,20 @@ run tests manually. For instance:
|
||||||
Contributing to the documentation
|
Contributing to the documentation
|
||||||
---------------------------------
|
---------------------------------
|
||||||
|
|
||||||
The documentation is written in RST, build with Sphinx and published
|
The documentation is written in RST (except option documentation which is in CommonMark),
|
||||||
by `Read the Docs <https://readthedocs.org/>`_.
|
built with Sphinx and published by `Read the Docs <https://readthedocs.org/>`_.
|
||||||
|
|
||||||
For the syntax, see `RST/Sphinx Cheatsheet
|
For the syntax, see the `RST/Sphinx primer
|
||||||
<https://sphinx-tutorial.readthedocs.io/cheatsheet/>`_.
|
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_.
|
||||||
|
|
||||||
|
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-shell
|
$ nix build .#documentation
|
||||||
$ cd docs
|
$ xdg-open result/index.html
|
||||||
$ make html
|
|
||||||
$ firefox ./_build/html/index.html
|
|
||||||
|
|
||||||
Note if you modify some NixOS mailserver options, you would also need
|
|
||||||
to regenerate the ``options.rst`` file:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
$ nix-shell --run generate-rst-options
|
|
||||||
|
|
||||||
Nixops
|
Nixops
|
||||||
------
|
------
|
||||||
|
|
|
@ -25,9 +25,12 @@ Welcome to NixOS Mailserver's documentation!
|
||||||
|
|
||||||
backup-guide
|
backup-guide
|
||||||
add-radicale
|
add-radicale
|
||||||
|
add-roundcube
|
||||||
rspamd-tuning
|
rspamd-tuning
|
||||||
fts
|
fts
|
||||||
flakes
|
flakes
|
||||||
|
autodiscovery
|
||||||
|
ldap
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|
14
docs/ldap.rst
Normal file
14
docs/ldap.rst
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
LDAP Support
|
||||||
|
============
|
||||||
|
|
||||||
|
It is possible to manage mail user accounts with LDAP rather than with
|
||||||
|
the option `loginAccounts <options.html#mailserver-loginaccounts>`_.
|
||||||
|
|
||||||
|
All related LDAP options are described in the `LDAP options section
|
||||||
|
<options.html#mailserver-ldap>`_ and the `LDAP test
|
||||||
|
<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/tests/ldap.nix>`_
|
||||||
|
provides a getting started example.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
The LDAP support can not be enabled if some accounts are also defined with ``mailserver.loginAccounts``.
|
||||||
|
|
1156
docs/options.rst
1156
docs/options.rst
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,51 @@
|
||||||
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
|
NixOS 21.05
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
sphinx==4.0.2
|
sphinx ~= 5.3
|
||||||
sphinx_rtd_theme==0.5.2
|
sphinx_rtd_theme ~= 1.1
|
||||||
|
myst-parser ~= 0.18
|
||||||
|
linkify-it-py ~= 2.0
|
||||||
|
|
|
@ -48,18 +48,19 @@ Setup the server
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The following describes a server setup that is fairly complete. Even
|
The following describes a server setup that is fairly complete. Even
|
||||||
though there are more possible options (see the ``default.nix`` file),
|
though there are more possible options (see the `NixOS Mailserver
|
||||||
these should be the most common ones.
|
options documentation <options.html>`_), these should be the most
|
||||||
|
common ones.
|
||||||
|
|
||||||
.. code:: nix
|
.. code:: nix
|
||||||
|
|
||||||
{ config, pkgs, ... }:
|
{ config, pkgs, ... }: {
|
||||||
{
|
|
||||||
imports = [
|
imports = [
|
||||||
(builtins.fetchTarball {
|
(builtins.fetchTarball {
|
||||||
# Pick a commit from the branch you are interested in
|
# Pick a release version you are interested in and set its hash, e.g.
|
||||||
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/A-COMMIT-ID/nixos-mailserver-A-COMMIT-ID.tar.gz";
|
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz";
|
||||||
# And set its hash
|
# To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command:
|
||||||
|
# release="nixos-23.05"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
|
||||||
sha256 = "0000000000000000000000000000000000000000000000000000";
|
sha256 = "0000000000000000000000000000000000000000000000000000";
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
@ -70,7 +71,7 @@ these should be the most common ones.
|
||||||
domains = [ "example.com" ];
|
domains = [ "example.com" ];
|
||||||
|
|
||||||
# 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 run nixpkgs.apacheHttpd -c htpasswd -nbB "" "super secret password" | cut -d: -f2
|
# nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
|
||||||
loginAccounts = {
|
loginAccounts = {
|
||||||
"user1@example.com" = {
|
"user1@example.com" = {
|
||||||
hashedPasswordFile = "/a/file/containing/a/hashed/password";
|
hashedPasswordFile = "/a/file/containing/a/hashed/password";
|
||||||
|
@ -81,8 +82,10 @@ these should be the most common ones.
|
||||||
|
|
||||||
# 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 = 3;
|
certificateScheme = "acme-nginx";
|
||||||
};
|
};
|
||||||
|
security.acme.acceptTerms = true;
|
||||||
|
security.acme.defaults.email = "security@example.com";
|
||||||
}
|
}
|
||||||
|
|
||||||
After a ``nixos-rebuild switch`` your server should be running all
|
After a ``nixos-rebuild switch`` your server should be running all
|
||||||
|
@ -96,7 +99,14 @@ 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 IP’s you own. Add an entry resolving ``1.2.3.4``
|
DNS entries for the IP’s you own. Add an entry resolving ``1.2.3.4``
|
||||||
to ``mail.example.com``
|
to ``mail.example.com``.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
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
|
You can check this with
|
||||||
|
|
||||||
|
|
51
flake.lock
51
flake.lock
|
@ -16,13 +16,29 @@
|
||||||
"type": "gitlab"
|
"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": 1626852498,
|
"lastModified": 1717602782,
|
||||||
"narHash": "sha256-lOXUJvi0FJUXHTVSiC5qsMRtEUgqM4mGZpMESLuGhmo=",
|
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "16105403bdd843540cbef9c63fc0f16c1c6eaa70",
|
"rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -31,42 +47,27 @@
|
||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs-21_05": {
|
"nixpkgs-24_05": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1625692408,
|
"lastModified": 1717144377,
|
||||||
"narHash": "sha256-e9L3TLLDVIJpMnHtiNHJE62oOh6emRtSZ244bgYJUZs=",
|
"narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "c06613c25df3fe1dd26243847a3c105cf6770627",
|
"rev": "805a384895c696f802a9bf5bf4720f37385df547",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
"id": "nixpkgs",
|
||||||
"ref": "nixos-21.05",
|
"ref": "nixos-24.05",
|
||||||
"type": "indirect"
|
"type": "indirect"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"blobs": "blobs",
|
"blobs": "blobs",
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"nixpkgs-21_05": "nixpkgs-21_05",
|
"nixpkgs-24_05": "nixpkgs-24_05"
|
||||||
"utils": "utils"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"utils": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1605370193,
|
|
||||||
"narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "5021eac20303a61fafe17224c087f5519baed54d",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
116
flake.nix
116
flake.nix
|
@ -2,27 +2,30 @@
|
||||||
description = "A complete and Simple Nixos Mailserver";
|
description = "A complete and Simple Nixos Mailserver";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
utils.url = "github:numtide/flake-utils";
|
flake-compat = {
|
||||||
|
url = "github:edolstra/flake-compat";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
nixpkgs.url = "flake:nixpkgs/nixos-unstable";
|
nixpkgs.url = "flake:nixpkgs/nixos-unstable";
|
||||||
nixpkgs-21_05.url = "flake:nixpkgs/nixos-21.05";
|
nixpkgs-24_05.url = "flake:nixpkgs/nixos-24.05";
|
||||||
blobs = {
|
blobs = {
|
||||||
url = "gitlab:simple-nixos-mailserver/blobs";
|
url = "gitlab:simple-nixos-mailserver/blobs";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, utils, blobs, nixpkgs, nixpkgs-21_05 }: let
|
outputs = { self, blobs, nixpkgs, nixpkgs-24_05, ... }: let
|
||||||
|
lib = nixpkgs.lib;
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
# We want to test nixos-mailserver on several nixos releases
|
|
||||||
releases = [
|
releases = [
|
||||||
{
|
{
|
||||||
name = "unstable";
|
name = "unstable";
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
name = "21_05";
|
name = "24.05";
|
||||||
pkgs = nixpkgs-21_05.legacyPackages.${system};
|
pkgs = nixpkgs-24_05.legacyPackages.${system};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
testNames = [
|
testNames = [
|
||||||
|
@ -30,9 +33,10 @@
|
||||||
"external"
|
"external"
|
||||||
"clamav"
|
"clamav"
|
||||||
"multiple"
|
"multiple"
|
||||||
|
"ldap"
|
||||||
];
|
];
|
||||||
genTest = testName: release: {
|
genTest = testName: release: {
|
||||||
"name"= "${testName}-${release.name}";
|
"name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
|
||||||
"value"= import (./tests/. + "/${testName}.nix") {
|
"value"= import (./tests/. + "/${testName}.nix") {
|
||||||
pkgs = release.pkgs;
|
pkgs = release.pkgs;
|
||||||
inherit blobs;
|
inherit blobs;
|
||||||
|
@ -44,90 +48,80 @@
|
||||||
# external-21_05 = <derivation>;
|
# external-21_05 = <derivation>;
|
||||||
# ...
|
# ...
|
||||||
# }
|
# }
|
||||||
allTests = pkgs.lib.listToAttrs (
|
allTests = lib.listToAttrs (
|
||||||
pkgs.lib.flatten (map (t: map (r: genTest t r) releases) testNames));
|
lib.flatten (map (t: map (r: genTest t r) releases) testNames));
|
||||||
|
|
||||||
mailserverModule = import ./.;
|
mailserverModule = import ./.;
|
||||||
|
|
||||||
# Generate a rst file describing options of the NixOS mailserver module
|
# Generate a MarkDown file describing the options of the NixOS mailserver module
|
||||||
generateRstOptions = let
|
optionsDoc = let
|
||||||
eval = import (pkgs.path + "/nixos/lib/eval-config.nix") {
|
eval = lib.evalModules {
|
||||||
inherit system;
|
|
||||||
modules = [
|
modules = [
|
||||||
mailserverModule
|
mailserverModule
|
||||||
{
|
{
|
||||||
# Because the blockbook package is currently broken (we
|
_module.check = false;
|
||||||
# don't care about this package but it is part of the
|
mailserver = {
|
||||||
# NixOS module evaluation)
|
fqdn = "mx.example.com";
|
||||||
nixpkgs.config.allowBroken = true;
|
domains = [
|
||||||
mailserver.fqdn = "mx.example.com";
|
"example.com"
|
||||||
|
];
|
||||||
|
dmarcReporting = {
|
||||||
|
organizationName = "Example Corp";
|
||||||
|
domain = "example.com";
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
};
|
};
|
||||||
options = pkgs.nixosOptionsDoc {
|
options = builtins.toFile "options.json" (builtins.toJSON
|
||||||
options = eval.options;
|
(lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver")
|
||||||
};
|
(lib.optionAttrSetToDocList eval.options)));
|
||||||
in pkgs.runCommand "options.rst" { buildInputs = [pkgs.python3]; } ''
|
in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } ''
|
||||||
echo Generating options.rst from ${options.optionsJSON}/share/doc/nixos/options.json
|
echo "Generating options.md from ${options}"
|
||||||
python ${./scripts/generate-rst-options.py} ${options.optionsJSON}/share/doc/nixos/options.json > $out
|
python ${./scripts/generate-options.py} ${options} > $out
|
||||||
'';
|
|
||||||
|
|
||||||
# This is a script helping users to generate this file in the docs directory
|
|
||||||
generateRstOptionsScript = pkgs.writeScriptBin "generate-rst-options" ''
|
|
||||||
cp -v ${generateRstOptions} ./docs/options.rst
|
|
||||||
'';
|
|
||||||
|
|
||||||
# This is to ensure we don't forget to update the options.rst file
|
|
||||||
testRstOptions = pkgs.runCommand "test-rst-options" {} ''
|
|
||||||
if ! diff -q ${./docs/options.rst} ${generateRstOptions}
|
|
||||||
then
|
|
||||||
echo "The file ./docs/options.rst is not up-to-date and needs to be regenerated!"
|
|
||||||
echo " hint: run 'nix-shell --run generate-rst-options' to generate this file"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "test: ok" > $out
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
documentation = pkgs.stdenv.mkDerivation {
|
documentation = pkgs.stdenv.mkDerivation {
|
||||||
name = "documentation";
|
name = "documentation";
|
||||||
src = pkgs.lib.sourceByRegex ./docs ["logo.png" "conf.py" "Makefile" ".*rst$"];
|
src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"];
|
||||||
buildInputs = [(
|
buildInputs = [(
|
||||||
pkgs.python3.withPackages(p: [
|
pkgs.python3.withPackages (p: with p; [
|
||||||
p.sphinx
|
sphinx
|
||||||
p.sphinx_rtd_theme
|
sphinx_rtd_theme
|
||||||
|
myst-parser
|
||||||
|
linkify-it-py
|
||||||
])
|
])
|
||||||
)];
|
)];
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
cp ${generateRstOptions} options.rst
|
cp ${optionsDoc} options.md
|
||||||
mkdir -p _static
|
|
||||||
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
|
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
|
||||||
export SOURCE_DATE_EPOCH=$(${pkgs.coreutils}/bin/date +%s)
|
unset SOURCE_DATE_EPOCH
|
||||||
make html
|
make html
|
||||||
'';
|
'';
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
cp -r _build/html $out
|
cp -Tr _build/html $out
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
in rec {
|
in {
|
||||||
nixosModules.mailserver = mailserverModule ;
|
nixosModules = rec {
|
||||||
nixosModule = self.nixosModules.mailserver;
|
mailserver = mailserverModule;
|
||||||
|
default = mailserver;
|
||||||
|
};
|
||||||
|
nixosModule = self.nixosModules.default; # compatibility
|
||||||
hydraJobs.${system} = allTests // {
|
hydraJobs.${system} = allTests // {
|
||||||
test-rst-options = testRstOptions;
|
|
||||||
inherit documentation;
|
inherit documentation;
|
||||||
};
|
};
|
||||||
checks.${system} = allTests;
|
checks.${system} = allTests;
|
||||||
devShell.${system} = pkgs.mkShell {
|
packages.${system} = {
|
||||||
buildInputs = with pkgs; [
|
inherit optionsDoc documentation;
|
||||||
generateRstOptionsScript
|
};
|
||||||
(python3.withPackages (p: with p; [
|
devShells.${system}.default = pkgs.mkShell {
|
||||||
sphinx
|
inputsFrom = [ documentation ];
|
||||||
sphinx_rtd_theme
|
packages = with pkgs; [
|
||||||
]))
|
|
||||||
jq
|
|
||||||
clamav
|
clamav
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
devShell.${system} = self.devShells.${system}.default; # compatibility
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
18
mail-server/assertions.nix
Normal file
18
mail-server/assertions.nix
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{ config, lib, pkgs, ... }:
|
||||||
|
{
|
||||||
|
assertions = lib.optionals config.mailserver.ldap.enable [
|
||||||
|
{
|
||||||
|
assertion = config.mailserver.loginAccounts == {};
|
||||||
|
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
assertion = config.mailserver.forwards == {};
|
||||||
|
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.forwards";
|
||||||
|
}
|
||||||
|
] ++ lib.optionals (config.mailserver.enable && config.mailserver.certificateScheme != "acme") [
|
||||||
|
{
|
||||||
|
assertion = config.mailserver.acmeCertificateName == config.mailserver.fqdn;
|
||||||
|
message = "When the certificate scheme is not 'acme' (mailserver.certificateScheme != \"acme\"), it is not possible to define mailserver.acmeCertificateName";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
|
@ -21,22 +21,22 @@ let
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# cert :: PATH
|
# cert :: PATH
|
||||||
certificatePath = if cfg.certificateScheme == 1
|
certificatePath = if cfg.certificateScheme == "manual"
|
||||||
then cfg.certificateFile
|
then cfg.certificateFile
|
||||||
else if cfg.certificateScheme == 2
|
else if cfg.certificateScheme == "selfsigned"
|
||||||
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
||||||
else if cfg.certificateScheme == 3
|
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
|
||||||
then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem"
|
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
|
||||||
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
|
else throw "unknown certificate scheme";
|
||||||
|
|
||||||
# key :: PATH
|
# key :: PATH
|
||||||
keyPath = if cfg.certificateScheme == 1
|
keyPath = if cfg.certificateScheme == "manual"
|
||||||
then cfg.keyFile
|
then cfg.keyFile
|
||||||
else if cfg.certificateScheme == 2
|
else if cfg.certificateScheme == "selfsigned"
|
||||||
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
||||||
else if cfg.certificateScheme == 3
|
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
|
||||||
then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem"
|
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
|
||||||
else throw "Error: Certificate Scheme must be in { 1, 2, 3 }";
|
else throw "unknown certificate scheme";
|
||||||
|
|
||||||
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,4 +45,26 @@ 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}
|
||||||
|
'';
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,14 +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}"
|
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}"
|
||||||
+ (lib.optionalString (cfg.indexDir != null)
|
+ (lib.optionalString (cfg.indexDir != null)
|
||||||
":INDEX=${cfg.indexDir}/%d/%n"
|
":INDEX=${cfg.indexDir}/%d/%n"
|
||||||
);
|
);
|
||||||
|
@ -57,6 +60,42 @@ let
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
ldapConfig = pkgs.writeTextFile {
|
||||||
|
name = "dovecot-ldap.conf.ext.template";
|
||||||
|
text = ''
|
||||||
|
ldap_version = 3
|
||||||
|
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
|
||||||
|
${lib.optionalString cfg.ldap.startTls ''
|
||||||
|
tls = yes
|
||||||
|
''}
|
||||||
|
tls_require_cert = hard
|
||||||
|
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
||||||
|
dn = ${cfg.ldap.bind.dn}
|
||||||
|
sasl_bind = no
|
||||||
|
auth_bind = yes
|
||||||
|
base = ${cfg.ldap.searchBase}
|
||||||
|
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
|
||||||
|
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
|
||||||
|
user_attrs = ${cfg.ldap.dovecot.userAttrs}
|
||||||
|
''}
|
||||||
|
user_filter = ${cfg.ldap.dovecot.userFilter}
|
||||||
|
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
|
||||||
|
pass_attrs = ${cfg.ldap.dovecot.passAttrs}
|
||||||
|
''}
|
||||||
|
pass_filter = ${cfg.ldap.dovecot.passFilter}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
setPwdInLdapConfFile = appendLdapBindPwd {
|
||||||
|
name = "ldap-conf-file";
|
||||||
|
file = ldapConfig;
|
||||||
|
prefix = ''dnpass = "'';
|
||||||
|
suffix = ''"'';
|
||||||
|
passwordFile = cfg.ldap.bind.passwordFile;
|
||||||
|
destination = ldapConfFile;
|
||||||
|
};
|
||||||
|
|
||||||
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
genPasswdScript = pkgs.writeScript "generate-password-file" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
|
|
||||||
|
@ -67,6 +106,9 @@ let
|
||||||
chmod 755 "${passwdDir}"
|
chmod 755 "${passwdDir}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Prevent world-readable password files, even temporarily.
|
||||||
|
umask 077
|
||||||
|
|
||||||
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
|
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
|
||||||
if [ ! -f "$f" ]; then
|
if [ ! -f "$f" ]; then
|
||||||
echo "Expected password hash file $f does not exist!"
|
echo "Expected password hash file $f does not exist!"
|
||||||
|
@ -76,14 +118,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}"})"}:${builtins.toString cfg.vmailUID}:${builtins.toString cfg.vmailUID}::${cfg.mailDirectory}:/run/current-system/sw/bin/nologin:"
|
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
||||||
|
) 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);
|
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
|
||||||
|
@ -91,6 +137,12 @@ let
|
||||||
# The assertion garantees there is exactly one Junk mailbox.
|
# The assertion garantees there is exactly one Junk mailbox.
|
||||||
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
|
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
|
||||||
|
|
||||||
|
mkLdapSearchScope = scope: (
|
||||||
|
if scope == "sub" then "subtree"
|
||||||
|
else if scope == "one" then "onelevel"
|
||||||
|
else scope
|
||||||
|
);
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
|
@ -101,6 +153,13 @@ in
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
# for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
|
||||||
|
# the global config and tries to open shared libraries configured in there,
|
||||||
|
# which are usually not compatible.
|
||||||
|
environment.systemPackages = [
|
||||||
|
pkgs.dovecot_pigeonhole
|
||||||
|
];
|
||||||
|
|
||||||
services.dovecot2 = {
|
services.dovecot2 = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableImap = enableImap || enableImapSsl;
|
enableImap = enableImap || enableImapSsl;
|
||||||
|
@ -117,8 +176,18 @@ 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";
|
||||||
|
|
||||||
sieveScripts = {
|
pluginSettings = {
|
||||||
after = builtins.toFile "spam.sieve" ''
|
sieve = "file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve";
|
||||||
|
sieve_default = "file:${cfg.sieveDirectory}/%u/default.sieve";
|
||||||
|
sieve_default_name = "default";
|
||||||
|
};
|
||||||
|
|
||||||
|
sieve = {
|
||||||
|
extensions = [
|
||||||
|
"fileinto"
|
||||||
|
];
|
||||||
|
|
||||||
|
scripts.after = builtins.toFile "spam.sieve" ''
|
||||||
require "fileinto";
|
require "fileinto";
|
||||||
|
|
||||||
if header :is "X-Spam" "Yes" {
|
if header :is "X-Spam" "Yes" {
|
||||||
|
@ -126,8 +195,29 @@ in
|
||||||
stop;
|
stop;
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
pipeBins = map lib.getExe [
|
||||||
|
(pkgs.writeShellScriptBin "sa-learn-ham.sh"
|
||||||
|
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
|
||||||
|
(pkgs.writeShellScriptBin "sa-learn-spam.sh"
|
||||||
|
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
imapsieve.mailbox = [
|
||||||
|
{
|
||||||
|
name = junkMailboxName;
|
||||||
|
causes = [ "COPY" "APPEND" ];
|
||||||
|
before = ./dovecot/imap_sieve/report-spam.sieve;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
name = "*";
|
||||||
|
from = junkMailboxName;
|
||||||
|
causes = [ "COPY" ];
|
||||||
|
before = ./dovecot/imap_sieve/report-ham.sieve;
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
mailboxes = cfg.mailboxes;
|
mailboxes = cfg.mailboxes;
|
||||||
|
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
|
@ -217,9 +307,23 @@ in
|
||||||
|
|
||||||
userdb {
|
userdb {
|
||||||
driver = passwd-file
|
driver = passwd-file
|
||||||
args = ${passwdFile}
|
args = ${userdbFile}
|
||||||
|
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
|
||||||
|
@ -235,29 +339,7 @@ in
|
||||||
inbox = yes
|
inbox = yes
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin {
|
${lib.optionalString cfg.fullTextSearch.enable ''
|
||||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
|
||||||
sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve
|
|
||||||
sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve
|
|
||||||
sieve_default_name = default
|
|
||||||
|
|
||||||
# From elsewhere to Spam folder
|
|
||||||
imapsieve_mailbox1_name = ${junkMailboxName}
|
|
||||||
imapsieve_mailbox1_causes = COPY
|
|
||||||
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
|
|
||||||
|
|
||||||
# From Spam folder to elsewhere
|
|
||||||
imapsieve_mailbox2_name = *
|
|
||||||
imapsieve_mailbox2_from = ${junkMailboxName}
|
|
||||||
imapsieve_mailbox2_causes = COPY
|
|
||||||
imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve
|
|
||||||
|
|
||||||
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
|
|
||||||
|
|
||||||
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
|
|
||||||
}
|
|
||||||
|
|
||||||
${lib.optionalString (cfg.fullTextSearch.enable != null) ''
|
|
||||||
plugin {
|
plugin {
|
||||||
plugin = fts fts_xapian
|
plugin = fts fts_xapian
|
||||||
fts = xapian
|
fts = xapian
|
||||||
|
@ -285,17 +367,10 @@ in
|
||||||
systemd.services.dovecot2 = {
|
systemd.services.dovecot2 = {
|
||||||
preStart = ''
|
preStart = ''
|
||||||
${genPasswdScript}
|
${genPasswdScript}
|
||||||
rm -rf '${stateDir}/imap_sieve'
|
'' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
|
||||||
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 ];
|
systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]);
|
||||||
|
|
||||||
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
|
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
|
||||||
description = "Optimize dovecot indices for fts_xapian";
|
description = "Optimize dovecot indices for fts_xapian";
|
||||||
|
|
|
@ -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 == 2 then [ openssl ] else []);
|
] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, pkgs, lib, ... }:
|
{ config, 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 == 3) 80;
|
++ lib.optional (certificateScheme == "acme-nginx") 80;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,28 +17,26 @@
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
{ config, pkgs, lib, ... }:
|
||||||
|
|
||||||
with (import ./common.nix { inherit config; });
|
with (import ./common.nix { inherit config lib pkgs; });
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
acmeRoot = "/var/lib/acme/acme-challenge";
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = lib.mkIf (cfg.enable && cfg.certificateScheme == 3) {
|
config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) {
|
||||||
services.nginx = {
|
services.nginx = lib.mkIf (cfg.certificateScheme == "acme-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.fqdn}".postRun = ''
|
security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
|
||||||
systemctl reload nginx
|
"postfix.service"
|
||||||
systemctl reload postfix
|
"dovecot2.service"
|
||||||
systemctl reload dovecot2
|
];
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}" ] || [ ! -f "${dkim_txt}" ]
|
if [ ! -f "${dkim_key}" ]
|
||||||
then
|
then
|
||||||
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
|
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
|
||||||
-d "${dom}" \
|
-d "${dom}" \
|
||||||
|
@ -37,6 +37,7 @@ 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
|
||||||
'';
|
'';
|
||||||
|
|
|
@ -33,6 +33,11 @@ let
|
||||||
let to = name;
|
let to = name;
|
||||||
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
|
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
|
||||||
cfg.loginAccounts));
|
cfg.loginAccounts));
|
||||||
|
regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||||
|
(name: value:
|
||||||
|
let to = name;
|
||||||
|
in map (from: {"${from}" = to;}) value.aliasesRegexp)
|
||||||
|
cfg.loginAccounts));
|
||||||
|
|
||||||
# catchAllPostfix :: Map String [String]
|
# catchAllPostfix :: Map String [String]
|
||||||
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||||
|
@ -65,6 +70,10 @@ let
|
||||||
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
|
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
|
||||||
in builtins.toFile "valias" content;
|
in builtins.toFile "valias" content;
|
||||||
|
|
||||||
|
regex_valiases_file = let
|
||||||
|
content = lookupTableToString regex_valiases_postfix;
|
||||||
|
in builtins.toFile "regex_valias" content;
|
||||||
|
|
||||||
# denied_recipients_postfix :: [ String ]
|
# denied_recipients_postfix :: [ String ]
|
||||||
denied_recipients_postfix = (map
|
denied_recipients_postfix = (map
|
||||||
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
|
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
|
||||||
|
@ -94,6 +103,7 @@ let
|
||||||
# every alias is owned (uniquely) by its user.
|
# every alias is owned (uniquely) by its user.
|
||||||
# The user's own address is already in all_valiases_postfix.
|
# The user's own address is already in all_valiases_postfix.
|
||||||
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
||||||
|
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (lookupTableToString regex_valiases_postfix);
|
||||||
|
|
||||||
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
|
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (''
|
||||||
# Removes sensitive headers from mails handed in via the submission port.
|
# Removes sensitive headers from mails handed in via the submission port.
|
||||||
|
@ -123,6 +133,7 @@ let
|
||||||
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
|
policyd-spf = pkgs.writeText "policyd-spf.conf" cfg.policydSPFExtraConfig;
|
||||||
|
|
||||||
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
||||||
|
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
|
||||||
|
|
||||||
submissionOptions =
|
submissionOptions =
|
||||||
{
|
{
|
||||||
|
@ -133,21 +144,73 @@ let
|
||||||
smtpd_sasl_security_options = "noanonymous";
|
smtpd_sasl_security_options = "noanonymous";
|
||||||
smtpd_sasl_local_domain = "$myhostname";
|
smtpd_sasl_local_domain = "$myhostname";
|
||||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||||
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts";
|
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${lib.optionalString (regex_valiases_postfix != {}) ",pcre:/etc/postfix/regex_vaccounts"}";
|
||||||
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
smtpd_sender_restrictions = "reject_sender_login_mismatch";
|
||||||
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||||
cleanup_service_name = "submission-header-cleanup";
|
cleanup_service_name = "submission-header-cleanup";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
commonLdapConfig = ''
|
||||||
|
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
|
||||||
|
start_tls = ${if cfg.ldap.startTls then "yes" else "no"}
|
||||||
|
version = 3
|
||||||
|
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
|
||||||
|
tls_require_cert = yes
|
||||||
|
|
||||||
|
search_base = ${cfg.ldap.searchBase}
|
||||||
|
scope = ${cfg.ldap.searchScope}
|
||||||
|
|
||||||
|
bind = yes
|
||||||
|
bind_dn = ${cfg.ldap.bind.dn}
|
||||||
|
'';
|
||||||
|
|
||||||
|
ldapSenderLoginMap = pkgs.writeText "ldap-sender-login-map.cf" ''
|
||||||
|
${commonLdapConfig}
|
||||||
|
query_filter = ${cfg.ldap.postfix.filter}
|
||||||
|
result_attribute = ${cfg.ldap.postfix.mailAttribute}
|
||||||
|
'';
|
||||||
|
ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf";
|
||||||
|
appendPwdInSenderLoginMap = appendLdapBindPwd {
|
||||||
|
name = "ldap-sender-login-map";
|
||||||
|
file = ldapSenderLoginMap;
|
||||||
|
prefix = "bind_pw = ";
|
||||||
|
passwordFile = cfg.ldap.bind.passwordFile;
|
||||||
|
destination = ldapSenderLoginMapFile;
|
||||||
|
};
|
||||||
|
|
||||||
|
ldapVirtualMailboxMap = pkgs.writeText "ldap-virtual-mailbox-map.cf" ''
|
||||||
|
${commonLdapConfig}
|
||||||
|
query_filter = ${cfg.ldap.postfix.filter}
|
||||||
|
result_attribute = ${cfg.ldap.postfix.uidAttribute}
|
||||||
|
'';
|
||||||
|
ldapVirtualMailboxMapFile = "/run/postfix/ldap-virtual-mailbox-map.cf";
|
||||||
|
appendPwdInVirtualMailboxMap = appendLdapBindPwd {
|
||||||
|
name = "ldap-virtual-mailbox-map";
|
||||||
|
file = ldapVirtualMailboxMap;
|
||||||
|
prefix = "bind_pw = ";
|
||||||
|
passwordFile = cfg.ldap.bind.passwordFile;
|
||||||
|
destination = ldapVirtualMailboxMapFile;
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = with cfg; lib.mkIf enable {
|
||||||
|
|
||||||
|
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
|
||||||
|
preStart = ''
|
||||||
|
${appendPwdInVirtualMailboxMap}
|
||||||
|
${appendPwdInSenderLoginMap}
|
||||||
|
'';
|
||||||
|
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
|
||||||
|
};
|
||||||
|
|
||||||
services.postfix = {
|
services.postfix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hostname = "${sendingFqdn}";
|
hostname = "${sendingFqdn}";
|
||||||
networksStyle = "host";
|
networksStyle = "host";
|
||||||
mapFiles."valias" = valiases_file;
|
mapFiles."valias" = valiases_file;
|
||||||
|
mapFiles."regex_valias" = regex_valiases_file;
|
||||||
mapFiles."vaccounts" = vaccounts_file;
|
mapFiles."vaccounts" = vaccounts_file;
|
||||||
|
mapFiles."regex_vaccounts" = regex_vaccounts_file;
|
||||||
mapFiles."denied_recipients" = denied_recipients_file;
|
mapFiles."denied_recipients" = denied_recipients_file;
|
||||||
mapFiles."reject_senders" = reject_senders_file;
|
mapFiles."reject_senders" = reject_senders_file;
|
||||||
mapFiles."reject_recipients" = reject_recipients_file;
|
mapFiles."reject_recipients" = reject_recipients_file;
|
||||||
|
@ -170,7 +233,16 @@ in
|
||||||
virtual_gid_maps = "static:5000";
|
virtual_gid_maps = "static:5000";
|
||||||
virtual_mailbox_base = mailDirectory;
|
virtual_mailbox_base = mailDirectory;
|
||||||
virtual_mailbox_domains = vhosts_file;
|
virtual_mailbox_domains = vhosts_file;
|
||||||
virtual_mailbox_maps = mappedFile "valias";
|
virtual_mailbox_maps = [
|
||||||
|
(mappedFile "valias")
|
||||||
|
] ++ lib.optionals (cfg.ldap.enable) [
|
||||||
|
"ldap:${ldapVirtualMailboxMapFile}"
|
||||||
|
] ++ lib.optionals (regex_valiases_postfix != {}) [
|
||||||
|
(mappedRegexFile "regex_valias")
|
||||||
|
];
|
||||||
|
virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [
|
||||||
|
(mappedRegexFile "regex_valias")
|
||||||
|
]);
|
||||||
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
||||||
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
|
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
|
||||||
lmtp_destination_recipient_limit = "1";
|
lmtp_destination_recipient_limit = "1";
|
||||||
|
@ -202,9 +274,6 @@ 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";
|
||||||
|
@ -237,6 +306,9 @@ in
|
||||||
milter_protocol = "6";
|
milter_protocol = "6";
|
||||||
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_type} {auth_authen} {auth_author} {mail_addr} {mail_host} {mail_mailer}";
|
||||||
|
|
||||||
|
# Fix for https://www.postfix.org/smtp-smuggling.html
|
||||||
|
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;
|
||||||
|
smtpd_forbid_bare_newline_exclusions = "$mynetworks";
|
||||||
};
|
};
|
||||||
|
|
||||||
submissionOptions = submissionOptions;
|
submissionOptions = submissionOptions;
|
||||||
|
@ -253,7 +325,7 @@ in
|
||||||
privileged = true;
|
privileged = true;
|
||||||
chroot = false;
|
chroot = false;
|
||||||
command = "spawn";
|
command = "spawn";
|
||||||
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
|
args = [ "user=nobody" "argv=${pkgs.spf-engine}/bin/policyd-spf" "${policyd-spf}"];
|
||||||
};
|
};
|
||||||
"submission-header-cleanup" = {
|
"submission-header-cleanup" = {
|
||||||
type = "unix";
|
type = "unix";
|
||||||
|
|
|
@ -30,7 +30,7 @@ in
|
||||||
inherit debug;
|
inherit debug;
|
||||||
locals = {
|
locals = {
|
||||||
"milter_headers.conf" = { text = ''
|
"milter_headers.conf" = { text = ''
|
||||||
extended_spam_headers = yes;
|
extended_spam_headers = true;
|
||||||
''; };
|
''; };
|
||||||
"redis.conf" = { text = ''
|
"redis.conf" = { text = ''
|
||||||
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
|
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
|
||||||
|
@ -56,14 +56,17 @@ 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 ''
|
||||||
overrides = {
|
reporting {
|
||||||
"milter_headers.conf" = {
|
enabled = true;
|
||||||
text = ''
|
email = "${cfg.dmarcReporting.email}";
|
||||||
extended_spam_headers = true;
|
domain = "${cfg.dmarcReporting.domain}";
|
||||||
'';
|
org_name = "${cfg.dmarcReporting.organizationName}";
|
||||||
};
|
from_name = "${cfg.dmarcReporting.fromName}";
|
||||||
|
msgid_from = "dmarc-rua";
|
||||||
|
}''}
|
||||||
|
''; };
|
||||||
};
|
};
|
||||||
|
|
||||||
workers.rspamd_proxy = {
|
workers.rspamd_proxy = {
|
||||||
|
@ -98,11 +101,72 @@ in
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.redis.enable = true;
|
services.redis.servers.rspamd = {
|
||||||
|
enable = lib.mkDefault true;
|
||||||
|
port = lib.mkDefault 6380;
|
||||||
|
};
|
||||||
|
|
||||||
systemd.services.rspamd = {
|
systemd.services.rspamd = {
|
||||||
requires = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||||
after = [ "redis.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
after = [ "redis-rspamd.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 = {
|
||||||
|
|
|
@ -19,9 +19,9 @@
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
certificatesDeps =
|
certificatesDeps =
|
||||||
if cfg.certificateScheme == 1 then
|
if cfg.certificateScheme == "manual" then
|
||||||
[]
|
[]
|
||||||
else if cfg.certificateScheme == 2 then
|
else if cfg.certificateScheme == "selfsigned" 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 == 2) {
|
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") {
|
||||||
after = [ "local-fs.target" ];
|
after = [ "local-fs.target" ];
|
||||||
script = ''
|
script = ''
|
||||||
# Create certificates if they do not exist yet
|
# Create certificates if they do not exist yet
|
||||||
|
@ -64,6 +64,8 @@ in
|
||||||
in ''
|
in ''
|
||||||
# Create mail directory and set permissions. See
|
# Create mail directory and set permissions. See
|
||||||
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
||||||
|
# Prevent world-readable paths, even temporarily.
|
||||||
|
umask 007
|
||||||
mkdir -p ${directories}
|
mkdir -p ${directories}
|
||||||
chgrp "${vmailGroupName}" ${directories}
|
chgrp "${vmailGroupName}" ${directories}
|
||||||
chmod 02770 ${directories}
|
chmod 02770 ${directories}
|
||||||
|
|
|
@ -34,6 +34,9 @@ let
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Prevent world-readable paths, even temporarily.
|
||||||
|
umask 007
|
||||||
|
|
||||||
# Create directory to store user sieve scripts if it doesn't exist
|
# Create directory to store user sieve scripts if it doesn't exist
|
||||||
if (! test -d "${sieveDirectory}"); then
|
if (! test -d "${sieveDirectory}"); then
|
||||||
mkdir "${sieveDirectory}"
|
mkdir "${sieveDirectory}"
|
||||||
|
|
82
scripts/generate-options.py
Normal file
82
scripts/generate-options.py
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
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)
|
|
@ -1,71 +0,0 @@
|
||||||
import json
|
|
||||||
import sys
|
|
||||||
import re
|
|
||||||
|
|
||||||
header = """
|
|
||||||
Mailserver Options
|
|
||||||
==================
|
|
||||||
|
|
||||||
mailserver
|
|
||||||
~~~~~~~~~~
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
template = """
|
|
||||||
{key}
|
|
||||||
{line}
|
|
||||||
|
|
||||||
{description}
|
|
||||||
|
|
||||||
{type}
|
|
||||||
{default}
|
|
||||||
"""
|
|
||||||
|
|
||||||
f = open(sys.argv[1])
|
|
||||||
options = json.load(f)
|
|
||||||
|
|
||||||
options = {k: v for k, v in options.items()
|
|
||||||
if k.startswith("mailserver.")}
|
|
||||||
|
|
||||||
groups = ["mailserver.loginAccount",
|
|
||||||
"mailserver.certificate",
|
|
||||||
"mailserver.dkim",
|
|
||||||
"mailserver.fullTextSearch",
|
|
||||||
"mailserver.redis",
|
|
||||||
"mailserver.monitoring",
|
|
||||||
"mailserver.backup",
|
|
||||||
"mailserver.borg"]
|
|
||||||
|
|
||||||
|
|
||||||
def print_option(name, value):
|
|
||||||
if 'default' in value:
|
|
||||||
if value['default'] == "":
|
|
||||||
default = '- Default: ``""``'
|
|
||||||
else:
|
|
||||||
default = '- Default: ``{}``'.format(v['default'])
|
|
||||||
# Some default values contains OUTPUTPATHS which make the
|
|
||||||
# output not stable across nixpkgs updates.
|
|
||||||
default = re.sub('/nix/store/[\w.-]*/', '<OUTPUT-PATH>/', default) # noqa
|
|
||||||
else:
|
|
||||||
default = ""
|
|
||||||
print(template.format(
|
|
||||||
key=name,
|
|
||||||
line="-"*len(name),
|
|
||||||
description=value['description'],
|
|
||||||
type="- Type: ``{}``".format(value['type']),
|
|
||||||
default=default))
|
|
||||||
|
|
||||||
|
|
||||||
print(header)
|
|
||||||
for k, v in options.items():
|
|
||||||
if any([k.startswith(c) for c in groups]):
|
|
||||||
continue
|
|
||||||
print_option(k, v)
|
|
||||||
|
|
||||||
for c in groups:
|
|
||||||
print(c)
|
|
||||||
print("~"*len(c))
|
|
||||||
print()
|
|
||||||
for k, v in options.items():
|
|
||||||
if k.startswith(c):
|
|
||||||
print_option(k, v)
|
|
|
@ -9,7 +9,7 @@ import time
|
||||||
|
|
||||||
RETRY = 100
|
RETRY = 100
|
||||||
|
|
||||||
def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, starttls):
|
def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls):
|
||||||
print("Sending mail with subject '{}'".format(subject))
|
print("Sending mail with subject '{}'".format(subject))
|
||||||
message = "\n".join([
|
message = "\n".join([
|
||||||
"From: {from_addr}",
|
"From: {from_addr}",
|
||||||
|
@ -30,7 +30,7 @@ def _send_mail(smtp_host, smtp_port, from_addr, from_pwd, to_addr, subject, star
|
||||||
if starttls:
|
if starttls:
|
||||||
smtp.starttls()
|
smtp.starttls()
|
||||||
if from_pwd is not None:
|
if from_pwd is not None:
|
||||||
smtp.login(from_addr, from_pwd)
|
smtp.login(smtp_username or from_addr, from_pwd)
|
||||||
|
|
||||||
smtp.sendmail(from_addr, [to_addr], message)
|
smtp.sendmail(from_addr, [to_addr], message)
|
||||||
return
|
return
|
||||||
|
@ -141,6 +141,7 @@ def send_and_read(args):
|
||||||
|
|
||||||
_send_mail(smtp_host=args.smtp_host,
|
_send_mail(smtp_host=args.smtp_host,
|
||||||
smtp_port=args.smtp_port,
|
smtp_port=args.smtp_port,
|
||||||
|
smtp_username=args.smtp_username,
|
||||||
from_addr=args.from_addr,
|
from_addr=args.from_addr,
|
||||||
from_pwd=src_pwd,
|
from_pwd=src_pwd,
|
||||||
to_addr=args.to_addr,
|
to_addr=args.to_addr,
|
||||||
|
@ -171,6 +172,7 @@ parser_send_and_read = subparsers.add_parser('send-and-read', description="Send
|
||||||
parser_send_and_read.add_argument('--smtp-host', type=str)
|
parser_send_and_read.add_argument('--smtp-host', type=str)
|
||||||
parser_send_and_read.add_argument('--smtp-port', type=str, default=25)
|
parser_send_and_read.add_argument('--smtp-port', type=str, default=25)
|
||||||
parser_send_and_read.add_argument('--smtp-starttls', action='store_true')
|
parser_send_and_read.add_argument('--smtp-starttls', action='store_true')
|
||||||
|
parser_send_and_read.add_argument('--smtp-username', type=str, default='', help="username used for smtp login. If not specified, the from-addr value is used")
|
||||||
parser_send_and_read.add_argument('--from-addr', type=str)
|
parser_send_and_read.add_argument('--from-addr', type=str)
|
||||||
parser_send_and_read.add_argument('--imap-host', required=True, type=str)
|
parser_send_and_read.add_argument('--imap-host', required=True, type=str)
|
||||||
parser_send_and_read.add_argument('--imap-port', type=str, default=993)
|
parser_send_and_read.add_argument('--imap-port', type=str, default=993)
|
||||||
|
|
11
shell.nix
11
shell.nix
|
@ -1 +1,10 @@
|
||||||
(import (builtins.fetchGit "https://github.com/edolstra/flake-compat") { src = ./.; }).shellNix
|
(import
|
||||||
|
(
|
||||||
|
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
|
||||||
|
|
|
@ -189,10 +189,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.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 ${nodes.server.nixpkgs.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.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 ${nodes.server.nixpkgs.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/.* ~/")
|
||||||
|
|
|
@ -43,6 +43,11 @@ 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" = {
|
||||||
|
@ -345,7 +350,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.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 ${nodes.server.nixpkgs.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/.* ~/")
|
||||||
|
@ -489,18 +494,21 @@ 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.* Mailbox INBOX: Mailbox opened because: indexing' >&2"
|
"journalctl -u dovecot2 | grep -E 'indexer-worker.* Done indexing .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 'indexer-worker' | grep -i '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")
|
||||||
server.fail("journalctl -u postfix | grep -i warning >&2")
|
server.fail("journalctl -u postfix | grep -i warning >&2")
|
||||||
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 -i warning >&2"
|
"journalctl -u dovecot2 |grep -v 'Expunged message reappeared, giving a new UID'| grep -v 'FTS Xapian: Box is empty' | grep -i warning >&2"
|
||||||
)
|
)
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,8 +29,8 @@ let
|
||||||
|
|
||||||
hashPassword = password: pkgs.runCommand
|
hashPassword = password: pkgs.runCommand
|
||||||
"password-${password}-hashed"
|
"password-${password}-hashed"
|
||||||
{ buildInputs = [ pkgs.apacheHttpd ]; } ''
|
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } ''
|
||||||
htpasswd -nbB "" "${password}" | cut -d: -f2 > $out
|
mkpasswd -sm bcrypt <<<"$password" > $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
hashedPasswordFile = hashPassword "my-password";
|
hashedPasswordFile = hashPassword "my-password";
|
||||||
|
@ -55,7 +55,7 @@ pkgs.nixosTest {
|
||||||
mailserver = {
|
mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
fqdn = "mail.example.com";
|
fqdn = "mail.example.com";
|
||||||
domains = [ "example.com" ];
|
domains = [ "example.com" "domain.com" ];
|
||||||
localDnsResolver = false;
|
localDnsResolver = false;
|
||||||
|
|
||||||
loginAccounts = {
|
loginAccounts = {
|
||||||
|
@ -64,6 +64,7 @@ pkgs.nixosTest {
|
||||||
};
|
};
|
||||||
"user2@example.com" = {
|
"user2@example.com" = {
|
||||||
hashedPasswordFile = hashedPasswordFile;
|
hashedPasswordFile = hashedPasswordFile;
|
||||||
|
aliasesRegexp = [''/^user2.*@domain\.com$/''];
|
||||||
};
|
};
|
||||||
"send-only@example.com" = {
|
"send-only@example.com" = {
|
||||||
hashedPasswordFile = hashPassword "send-only";
|
hashedPasswordFile = hashPassword "send-only";
|
||||||
|
@ -126,6 +127,46 @@ pkgs.nixosTest {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
with subtest("regex email alias are received"):
|
||||||
|
# A mail sent to user2-regex-alias@domain.com is in the user2@example.com mailbox
|
||||||
|
machine.succeed(
|
||||||
|
" ".join(
|
||||||
|
[
|
||||||
|
"mail-check send-and-read",
|
||||||
|
"--smtp-port 587",
|
||||||
|
"--smtp-starttls",
|
||||||
|
"--smtp-host localhost",
|
||||||
|
"--imap-host localhost",
|
||||||
|
"--imap-username user2@example.com",
|
||||||
|
"--from-addr user1@example.com",
|
||||||
|
"--to-addr user2-regex-alias@domain.com",
|
||||||
|
"--src-password-file ${passwordFile}",
|
||||||
|
"--dst-password-file ${passwordFile}",
|
||||||
|
"--ignore-dkim-spf",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with subtest("user can send from regex email alias"):
|
||||||
|
# A mail sent from user2-regex-alias@domain.com, using user2@example.com credentials is received
|
||||||
|
machine.succeed(
|
||||||
|
" ".join(
|
||||||
|
[
|
||||||
|
"mail-check send-and-read",
|
||||||
|
"--smtp-port 587",
|
||||||
|
"--smtp-starttls",
|
||||||
|
"--smtp-host localhost",
|
||||||
|
"--imap-host localhost",
|
||||||
|
"--smtp-username user2@example.com",
|
||||||
|
"--from-addr user2-regex-alias@domain.com",
|
||||||
|
"--to-addr user1@example.com",
|
||||||
|
"--src-password-file ${passwordFile}",
|
||||||
|
"--dst-password-file ${passwordFile}",
|
||||||
|
"--ignore-dkim-spf",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
with subtest("vmail gid is set correctly"):
|
with subtest("vmail gid is set correctly"):
|
||||||
machine.succeed("getent group vmail | grep 5000")
|
machine.succeed("getent group vmail | grep 5000")
|
||||||
|
|
||||||
|
@ -136,7 +177,7 @@ pkgs.nixosTest {
|
||||||
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
||||||
)
|
)
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
"cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q 'This account cannot receive emails'"
|
"cat ${sendMail} | ${pkgs.netcat-gnu}/bin/nc localhost 25 | grep -q '554 5.5.0 Error'"
|
||||||
)
|
)
|
||||||
|
|
||||||
with subtest("rspamd controller serves web ui"):
|
with subtest("rspamd controller serves web ui"):
|
||||||
|
|
183
tests/ldap.nix
Normal file
183
tests/ldap.nix
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
{ pkgs ? import <nixpkgs> {}
|
||||||
|
, ...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
bindPassword = "unsafegibberish";
|
||||||
|
alicePassword = "testalice";
|
||||||
|
bobPassword = "testbob";
|
||||||
|
in
|
||||||
|
pkgs.nixosTest {
|
||||||
|
name = "ldap";
|
||||||
|
nodes = {
|
||||||
|
machine = { config, pkgs, ... }: {
|
||||||
|
imports = [
|
||||||
|
./../default.nix
|
||||||
|
./lib/config.nix
|
||||||
|
];
|
||||||
|
|
||||||
|
virtualisation.memorySize = 1024;
|
||||||
|
|
||||||
|
services.openssh = {
|
||||||
|
enable = true;
|
||||||
|
permitRootLogin = "yes";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment.systemPackages = [
|
||||||
|
(pkgs.writeScriptBin "mail-check" ''
|
||||||
|
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||||
|
'')];
|
||||||
|
|
||||||
|
environment.etc.bind-password.text = bindPassword;
|
||||||
|
|
||||||
|
services.openldap = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
children = {
|
||||||
|
"cn=schema".includes = [
|
||||||
|
"${pkgs.openldap}/etc/schema/core.ldif"
|
||||||
|
"${pkgs.openldap}/etc/schema/cosine.ldif"
|
||||||
|
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
|
||||||
|
"${pkgs.openldap}/etc/schema/nis.ldif"
|
||||||
|
];
|
||||||
|
"olcDatabase={1}mdb" = {
|
||||||
|
attrs = {
|
||||||
|
objectClass = [
|
||||||
|
"olcDatabaseConfig"
|
||||||
|
"olcMdbConfig"
|
||||||
|
];
|
||||||
|
olcDatabase = "{1}mdb";
|
||||||
|
olcDbDirectory = "/var/lib/openldap/example";
|
||||||
|
olcSuffix = "dc=example";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
declarativeContents."dc=example" = ''
|
||||||
|
dn: dc=example
|
||||||
|
objectClass: domain
|
||||||
|
dc: example
|
||||||
|
|
||||||
|
dn: cn=mail,dc=example
|
||||||
|
objectClass: organizationalRole
|
||||||
|
objectClass: simpleSecurityObject
|
||||||
|
objectClass: top
|
||||||
|
cn: mail
|
||||||
|
userPassword: ${bindPassword}
|
||||||
|
|
||||||
|
dn: ou=users,dc=example
|
||||||
|
objectClass: organizationalUnit
|
||||||
|
ou: users
|
||||||
|
|
||||||
|
dn: cn=alice,ou=users,dc=example
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
cn: alice
|
||||||
|
sn: Foo
|
||||||
|
mail: alice@example.com
|
||||||
|
userPassword: ${alicePassword}
|
||||||
|
|
||||||
|
dn: cn=bob,ou=users,dc=example
|
||||||
|
objectClass: inetOrgPerson
|
||||||
|
cn: bob
|
||||||
|
sn: Bar
|
||||||
|
mail: bob@example.com
|
||||||
|
userPassword: ${bobPassword}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
mailserver = {
|
||||||
|
enable = true;
|
||||||
|
fqdn = "mail.example.com";
|
||||||
|
domains = [ "example.com" ];
|
||||||
|
localDnsResolver = false;
|
||||||
|
|
||||||
|
ldap = {
|
||||||
|
enable = true;
|
||||||
|
uris = [
|
||||||
|
"ldap://"
|
||||||
|
];
|
||||||
|
bind = {
|
||||||
|
dn = "cn=mail,dc=example";
|
||||||
|
passwordFile = "/etc/bind-password";
|
||||||
|
};
|
||||||
|
searchBase = "ou=users,dc=example";
|
||||||
|
searchScope = "sub";
|
||||||
|
};
|
||||||
|
|
||||||
|
vmailGroupName = "vmail";
|
||||||
|
vmailUID = 5000;
|
||||||
|
|
||||||
|
enableImap = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
testScript = ''
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
|
machine.start()
|
||||||
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
|
# This function retrieves the ldap table file from a postconf
|
||||||
|
# command.
|
||||||
|
# A key lookup is achived and the returned value is compared
|
||||||
|
# to the expected value.
|
||||||
|
def test_lookup(postconf_cmdline, key, expected):
|
||||||
|
conf = machine.succeed(postconf_cmdline).rstrip()
|
||||||
|
ldap_table_path = re.match('.* =.*ldap:(.*)', conf).group(1)
|
||||||
|
value = machine.succeed(f"postmap -q {key} ldap:{ldap_table_path}").rstrip()
|
||||||
|
try:
|
||||||
|
assert value == expected
|
||||||
|
except AssertionError:
|
||||||
|
print(f"Expected {conf} lookup for key '{key}' to return '{expected}, but got '{value}'", file=sys.stderr)
|
||||||
|
raise
|
||||||
|
|
||||||
|
with subtest("Test postmap lookups"):
|
||||||
|
test_lookup("postconf virtual_mailbox_maps", "alice@example.com", "alice@example.com")
|
||||||
|
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "alice@example.com", "alice@example.com")
|
||||||
|
|
||||||
|
test_lookup("postconf virtual_mailbox_maps", "bob@example.com", "bob@example.com")
|
||||||
|
test_lookup("postconf -P submission/inet/smtpd_sender_login_maps", "bob@example.com", "bob@example.com")
|
||||||
|
|
||||||
|
with subtest("Test doveadm lookups"):
|
||||||
|
machine.succeed("doveadm user -u alice@example.com")
|
||||||
|
machine.succeed("doveadm user -u bob@example.com")
|
||||||
|
|
||||||
|
with subtest("Files containing secrets are only readable by root"):
|
||||||
|
machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'")
|
||||||
|
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'")
|
||||||
|
|
||||||
|
with subtest("Test account/mail address binding"):
|
||||||
|
machine.fail(" ".join([
|
||||||
|
"mail-check send-and-read",
|
||||||
|
"--smtp-port 587",
|
||||||
|
"--smtp-starttls",
|
||||||
|
"--smtp-host localhost",
|
||||||
|
"--smtp-username alice@example.com",
|
||||||
|
"--imap-host localhost",
|
||||||
|
"--imap-username bob@example.com",
|
||||||
|
"--from-addr bob@example.com",
|
||||||
|
"--to-addr aliceb@example.com",
|
||||||
|
"--src-password-file <(echo '${alicePassword}')",
|
||||||
|
"--dst-password-file <(echo '${bobPassword}')",
|
||||||
|
"--ignore-dkim-spf"
|
||||||
|
]))
|
||||||
|
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'")
|
||||||
|
|
||||||
|
with subtest("Test mail delivery"):
|
||||||
|
machine.succeed(" ".join([
|
||||||
|
"mail-check send-and-read",
|
||||||
|
"--smtp-port 587",
|
||||||
|
"--smtp-starttls",
|
||||||
|
"--smtp-host localhost",
|
||||||
|
"--smtp-username alice@example.com",
|
||||||
|
"--imap-host localhost",
|
||||||
|
"--imap-username bob@example.com",
|
||||||
|
"--from-addr alice@example.com",
|
||||||
|
"--to-addr bob@example.com",
|
||||||
|
"--src-password-file <(echo '${alicePassword}')",
|
||||||
|
"--dst-password-file <(echo '${bobPassword}')",
|
||||||
|
"--ignore-dkim-spf"
|
||||||
|
]))
|
||||||
|
'';
|
||||||
|
}
|
|
@ -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.nix> {
|
import <nixpkgs/nixos/tests/make-test-python.nix> {
|
||||||
|
|
||||||
machine =
|
nodes.machine =
|
||||||
{ config, pkgs, ... }:
|
{ config, pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
|
@ -26,6 +26,6 @@ import <nixpkgs/nixos/tests/make-test.nix> {
|
||||||
|
|
||||||
testScript =
|
testScript =
|
||||||
''
|
''
|
||||||
$machine->waitForUnit("multi-user.target");
|
machine.wait_for_unit("multi-user.target");
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
let
|
let
|
||||||
hashPassword = password: pkgs.runCommand
|
hashPassword = password: pkgs.runCommand
|
||||||
"password-${password}-hashed"
|
"password-${password}-hashed"
|
||||||
{ buildInputs = [ pkgs.apacheHttpd ]; }
|
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; }
|
||||||
''
|
''
|
||||||
htpasswd -nbB "" "${password}" | cut -d: -f2 > $out
|
mkpasswd -sm bcrypt <<<"$password" > $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
password = pkgs.writeText "password" "password";
|
password = pkgs.writeText "password" "password";
|
||||||
|
@ -30,6 +30,8 @@ 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
|
||||||
|
|
Loading…
Reference in a new issue