Compare commits
267 Commits
havefun-23
...
havefun-25
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6aa7e2b18 | ||
|
|
23f0a53ca6 | ||
|
|
a14fe3b293 | ||
|
|
c5bd875089 | ||
|
|
507d5dcef9 | ||
|
|
faeb1b04d8 | ||
|
|
8d35f004ee | ||
|
|
4987d275a9 | ||
|
|
a35a181671 | ||
|
|
cbdf90f639 | ||
|
|
b88e6182f0 | ||
|
|
b946f74261 | ||
|
|
345cbc11df | ||
|
|
1cb4295b74 | ||
|
|
db66559815 | ||
|
|
17c6816f67 | ||
|
|
1a3a618a30 | ||
|
|
61cff94a28 | ||
|
|
eeda8ba39e | ||
|
|
b633223a33 | ||
|
|
edb7b661e4 | ||
|
|
b99f353ab8 | ||
|
|
5965fae920 | ||
|
|
a1532a552f | ||
|
|
e3ee0fcceb | ||
|
|
44dd1778a0 | ||
|
|
3555a546ab | ||
|
|
bd56d97299 | ||
|
|
6f17c29eb8 | ||
|
|
1cedddf425 | ||
|
|
0812ca1e48 | ||
|
|
ed771e37f7 | ||
|
|
619e35dce2 | ||
|
|
6dbbac29f9 | ||
|
|
cc54c4fa85 | ||
|
|
1337e2eece | ||
|
|
58659fbdfd | ||
|
|
9f7291ce68 | ||
|
|
82c2225914 | ||
|
|
85f0a94466 | ||
|
|
70256c7d6e | ||
|
|
6005d88bed | ||
|
|
9b57654b31 | ||
|
|
4a05bb1911 | ||
|
|
1e80fb2594 | ||
|
|
0ab40d0575 | ||
|
|
bf2b313365 | ||
|
|
d2534fa431 | ||
|
|
39ead49eb4 | ||
|
|
c709476ac5 | ||
|
|
54f37811dd | ||
|
|
b49ae46f22 | ||
|
|
1a2d7a4bf5 | ||
|
|
cc5f180427 | ||
|
|
63b8e1615f | ||
|
|
958c112fba | ||
|
|
2204f55329 | ||
|
|
2be40a9653 | ||
|
|
b7d2f287f3 | ||
|
|
57d9624c71 | ||
|
|
fc955088e3 | ||
|
|
43f87f5520 | ||
|
|
aa06b2f489 | ||
|
|
eb656cd361 | ||
|
|
b76a547bec | ||
|
|
cea6f25a40 | ||
|
|
027e6bcd76 | ||
|
|
ce87c8a977 | ||
|
|
29de3e6865 | ||
|
|
80d21ed7a1 | ||
|
|
e9953aa154 | ||
|
|
dda91cfc15 | ||
|
|
c2df33f76a | ||
|
|
2b240501e0 | ||
|
|
0aeb2849ad | ||
|
|
47786932cb | ||
|
|
358a44674e | ||
|
|
679bce8bbb | ||
|
|
334e370c1f | ||
|
|
d6d2053b80 | ||
|
|
6004878dc6 | ||
|
|
f9a52ca4b5 | ||
|
|
a40574beb5 | ||
|
|
b38dc8085c | ||
|
|
b10c54606b | ||
|
|
c45b8a1253 | ||
|
|
d91d94be94 | ||
|
|
b9e28e23af | ||
|
|
67f0b864cc | ||
|
|
cfb3136cf0 | ||
|
|
6ef1eb9ce1 | ||
|
|
9d8caf5944 | ||
|
|
3c1cff431c | ||
|
|
f25495cabf | ||
|
|
62ea8a7e00 | ||
|
|
601b33d2a7 | ||
|
|
ed6d699eb4 | ||
|
|
64aca4f2ce | ||
|
|
217ec6008a | ||
|
|
0774c93ae6 | ||
|
|
f08ee8da38 | ||
|
|
cf6ef5e9ca | ||
|
|
7405122dde | ||
|
|
6652b57dda | ||
|
|
c8f809fa76 | ||
|
|
5c1b9921e6 | ||
|
|
67b0a7e946 | ||
|
|
a2152f9807 | ||
|
|
fb56bcf747 | ||
|
|
b555b3e8dc | ||
|
|
1a7f3d718c | ||
|
|
03433d472f | ||
|
|
c7497cd5f6 | ||
|
|
5f592b5960 | ||
|
|
21ce4b4ff8 | ||
|
|
efebf59b13 | ||
|
|
4fd9508d41 | ||
|
|
3828b00dea | ||
|
|
e27326d317 | ||
|
|
23cc9a3996 | ||
|
|
e0ab4eeb67 | ||
|
|
8e0074c4e5 | ||
|
|
3b7cda8cc5 | ||
|
|
3f1c6960d3 | ||
|
|
54cb3e5784 | ||
|
|
f1bd4b8215 | ||
|
|
e540dc864c | ||
|
|
8b27add088 | ||
|
|
49980abd25 | ||
|
|
f9b15192b8 | ||
|
|
d6d6308ba2 | ||
|
|
c4628a4c04 | ||
|
|
8c835feaa7 | ||
|
|
c9f61e02ae | ||
|
|
145afc5393 | ||
|
|
ea1b0f8e2b | ||
|
|
c8bc3e4f1f | ||
|
|
519a85a801 | ||
|
|
ffd0e6f8f2 | ||
|
|
7cb61e6e3a | ||
|
|
a1e9276656 | ||
|
|
233c5e1a70 | ||
|
|
506c6151d6 | ||
|
|
11bfdbf136 | ||
|
|
10cccc7706 | ||
|
|
6a78dc3375 | ||
|
|
792225e256 | ||
|
|
53007af63f | ||
|
|
51d48f1492 | ||
|
|
b4ae17d224 | ||
|
|
f7a221bc69 | ||
|
|
dceb60ea7d | ||
|
|
826a3b2fcf | ||
|
|
0cbdf465e4 | ||
|
|
e287d83ab1 | ||
|
|
2ed7a94782 | ||
|
|
433520257a | ||
|
|
aa8366d234 | ||
|
|
9a6190ceea | ||
|
|
1e51a503b1 | ||
|
|
fce540024a | ||
|
|
040f07ff45 | ||
|
|
a73982f5b4 | ||
|
|
fbfd948535 | ||
|
|
4c25278507 | ||
|
|
3268d8b0d8 | ||
|
|
4839fa6614 | ||
|
|
ddc6ce61db | ||
|
|
a6eb2a8f9a | ||
|
|
a7d580b934 | ||
|
|
f9fcbe9430 | ||
|
|
1615c93511 | ||
|
|
313f94ed8f | ||
|
|
ff9087adb4 | ||
|
|
d0ac5ce64c | ||
|
|
dccca0506a | ||
|
|
41e513da64 | ||
|
|
1899fbe3fb | ||
|
|
dd83a2c7ad | ||
|
|
235dba2d82 | ||
|
|
edd828ca88 | ||
|
|
1ce644871b | ||
|
|
da66510f68 | ||
|
|
1f82d59d67 | ||
|
|
61b3a2c5ec | ||
|
|
ef1e02e555 | ||
|
|
1feca02008 | ||
|
|
b92870c240 | ||
|
|
8970ed0849 | ||
|
|
a7d2b05a99 | ||
|
|
4a09d6460a | ||
|
|
a1ff289bf9 | ||
|
|
7bb0f43503 | ||
|
|
86b48f368f | ||
|
|
e488e3639a | ||
|
|
2e254b4b5e | ||
|
|
1471e54b92 | ||
|
|
fac7efe946 | ||
|
|
155ba08be7 | ||
|
|
71c5fe04f1 | ||
|
|
8b4990905c | ||
|
|
f6a64f713c | ||
|
|
b343c5e8fa | ||
|
|
776162c162 | ||
|
|
6f3ece9181 | ||
|
|
2d0b3fdeb0 | ||
|
|
4320259e34 | ||
|
|
7091fad860 | ||
|
|
2520e662f7 | ||
|
|
630b5c4fdd | ||
|
|
2c37e563fd | ||
|
|
8800bccab8 | ||
|
|
84bf0c0c07 | ||
|
|
a071813b97 | ||
|
|
ca69f91f6b | ||
|
|
35185c023e | ||
|
|
75b1908f24 | ||
|
|
95e2de368f | ||
|
|
b859c910ab | ||
|
|
46fe2c25c8 | ||
|
|
ab52efd622 | ||
|
|
42651ce2d3 | ||
|
|
bba070a1fe | ||
|
|
745c6ee861 | ||
|
|
7bdf5003c7 | ||
|
|
1873ed0908 | ||
|
|
efe77ce806 | ||
|
|
b4fbffe79c | ||
|
|
0c40a0b2c6 | ||
|
|
9b5df96132 | ||
|
|
90539a1a99 | ||
|
|
c8ec4d5e43 | ||
|
|
f23faf97d6 | ||
|
|
8c1c4640b8 | ||
|
|
6b425d13f5 | ||
|
|
ade37b2765 | ||
|
|
dc0569066e | ||
|
|
87ffaad9a3 | ||
|
|
4a5eb4baea | ||
|
|
63209b1def | ||
|
|
26a56d0a8f | ||
|
|
c43d8c4a3c | ||
|
|
6db6c0dc72 | ||
|
|
e4aabd3de6 | ||
|
|
1cf6d01989 | ||
|
|
0a801316cd | ||
|
|
9919033068 | ||
|
|
e901c56849 | ||
|
|
3a082011dc | ||
|
|
af7d3bf5da | ||
|
|
059b50b2e7 | ||
|
|
290a995de5 | ||
|
|
54cbacb6eb | ||
|
|
29916981e7 | ||
|
|
0d51a32e47 | ||
|
|
ed80b589d3 | ||
|
|
46a0829aa8 | ||
|
|
41059fc548 | ||
|
|
ef4756bcfc | ||
|
|
9f6635a035 | ||
|
|
79c8cfcd58 | ||
|
|
799fe34c12 | ||
|
|
d507bd9c95 | ||
|
|
fe6d325397 | ||
|
|
572c1b4d69 | ||
|
|
9e36323ae3 | ||
|
|
e47f3719f1 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
result
|
result
|
||||||
|
.direnv
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
|
.hydra-cli:
|
||||||
|
image: docker.nix-community.org/nixpkgs/nix-flakes
|
||||||
|
script:
|
||||||
|
- nix run --inputs-from ./. nixpkgs#hydra-cli -- -H https://hydra.nix-community.org jobset-wait simple-nixos-mailserver "${jobset}"
|
||||||
|
|
||||||
hydra-pr:
|
hydra-pr:
|
||||||
|
extends: .hydra-cli
|
||||||
only:
|
only:
|
||||||
- merge_requests
|
- merge_requests
|
||||||
image: nixos/nix
|
variables:
|
||||||
script:
|
jobset: $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:
|
||||||
|
extends: .hydra-cli
|
||||||
only:
|
only:
|
||||||
- master
|
- master
|
||||||
image: nixos/nix
|
variables:
|
||||||
script:
|
jobset: 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'
|
|
||||||
|
|||||||
@@ -4,22 +4,21 @@ let
|
|||||||
pkgs = import nixpkgs { };
|
pkgs = import nixpkgs { };
|
||||||
|
|
||||||
prs = builtins.fromJSON (builtins.readFile pulls);
|
prs = builtins.fromJSON (builtins.readFile pulls);
|
||||||
prJobsets = pkgs.lib.mapAttrs (num: info:
|
prJobsets = pkgs.lib.mapAttrs (num: info: {
|
||||||
{ enabled = 1;
|
enabled = 1;
|
||||||
hidden = false;
|
hidden = false;
|
||||||
description = "PR ${num}: ${info.title}";
|
description = "PR ${num}: ${info.title}";
|
||||||
checkinterval = 30;
|
checkinterval = 300;
|
||||||
schedulingshares = 20;
|
schedulingshares = 20;
|
||||||
enableemail = false;
|
enableemail = false;
|
||||||
emailoverride = "";
|
emailoverride = "";
|
||||||
keepnr = 1;
|
keepnr = 1;
|
||||||
type = 1;
|
type = 1;
|
||||||
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;
|
|
||||||
mkFlakeJobset = branch: {
|
mkFlakeJobset = branch: {
|
||||||
description = "Build ${branch} branch of Simple NixOS MailServer";
|
description = "Build ${branch} branch of Simple NixOS MailServer";
|
||||||
checkinterval = "60";
|
checkinterval = 300;
|
||||||
enabled = "1";
|
enabled = "1";
|
||||||
schedulingshares = 100;
|
schedulingshares = 100;
|
||||||
enableemail = false;
|
enableemail = false;
|
||||||
@@ -32,8 +31,8 @@ let
|
|||||||
|
|
||||||
desc = prJobsets // {
|
desc = prJobsets // {
|
||||||
"master" = mkFlakeJobset "master";
|
"master" = mkFlakeJobset "master";
|
||||||
"nixos-22.11" = mkFlakeJobset "nixos-22.11";
|
"nixos-25.05" = mkFlakeJobset "nixos-25.05";
|
||||||
"nixos-23.05" = mkFlakeJobset "nixos-23.05";
|
"nixos-25.11" = mkFlakeJobset "nixos-25.11";
|
||||||
};
|
};
|
||||||
|
|
||||||
log = {
|
log = {
|
||||||
@@ -41,13 +40,14 @@ let
|
|||||||
jobsets = desc;
|
jobsets = desc;
|
||||||
};
|
};
|
||||||
|
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
jobsets = pkgs.runCommand "spec-jobsets.json" { } ''
|
jobsets = pkgs.runCommand "spec-jobsets.json" { } ''
|
||||||
cat >$out <<EOF
|
cat >$out <<'EOF'
|
||||||
${builtins.toJSON desc}
|
${builtins.toJSON desc}
|
||||||
EOF
|
EOF
|
||||||
# This is to get nice .jobsets build logs on Hydra
|
# This is to get nice .jobsets build logs on Hydra
|
||||||
cat >tmp <<EOF
|
cat >tmp <<'EOF'
|
||||||
${builtins.toJSON log}
|
${builtins.toJSON log}
|
||||||
EOF
|
EOF
|
||||||
${pkgs.jq}/bin/jq . tmp
|
${pkgs.jq}/bin/jq . tmp
|
||||||
|
|||||||
@@ -5,17 +5,18 @@
|
|||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: ubuntu-22.04
|
os: ubuntu-24.04
|
||||||
tools:
|
tools:
|
||||||
python: "3"
|
python: "3"
|
||||||
apt_packages:
|
apt_packages:
|
||||||
- nix
|
- curl
|
||||||
- proot
|
- proot
|
||||||
jobs:
|
jobs:
|
||||||
pre_install:
|
pre_install:
|
||||||
- mkdir -p ~/.nix ~/.config/nix
|
- curl -L https://github.com/DavHau/nix-portable/releases/latest/download/nix-portable-$(uname -m) > ./nix-portable
|
||||||
- echo "experimental-features = nix-command flakes" > ~/.config/nix/nix.conf
|
- chmod +x ./nix-portable
|
||||||
- proot -b ~/.nix:/nix /bin/sh -c "nix build -L .#optionsDoc && cp -v result docs/options.md"
|
- ./nix-portable nix build --print-build-logs .#optionsDoc
|
||||||
|
- ./nix-portable nix store cat $(readlink result) > docs/options.md
|
||||||
|
|
||||||
sphinx:
|
sphinx:
|
||||||
configuration: docs/conf.py
|
configuration: docs/conf.py
|
||||||
|
|||||||
102
README.md
102
README.md
@@ -1,75 +1,85 @@
|
|||||||
# ![Simple Nixos MailServer][logo]
|
# ![Simple Nixos MailServer][logo]
|
||||||
|
|
||||||

|

|
||||||
[](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/commits/master)
|
[](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/commits/master)
|
||||||
|
|
||||||
|
|
||||||
## Release branches
|
## Release branches
|
||||||
|
|
||||||
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 23.05
|
* For NixOS 25.11
|
||||||
- Use the [SNM branch `nixos-23.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-23.05)
|
* Use the [SNM branch `nixos-25.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-25.11)
|
||||||
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/)
|
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-25.11/)
|
||||||
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-23.05/release-notes.html#nixos-23-05)
|
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.11/release-notes.html#nixos-25-11)
|
||||||
* For NixOS 22.11
|
* For NixOS 25.05
|
||||||
- Use the [SNM branch `nixos-22.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-22.11)
|
* Use the [SNM branch `nixos-25.05`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-25.05)
|
||||||
- [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/)
|
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/)
|
||||||
- [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-22.11/release-notes.html#nixos-22-11)
|
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/release-notes.html#nixos-25-05)
|
||||||
* For NixOS unstable
|
* For NixOS unstable
|
||||||
- Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
|
* Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
|
||||||
- [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
|
* [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
|
||||||
|
|
||||||
[Subscribe to SNM Announcement List](https://www.freelists.org/list/snm)
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
### v2.0
|
|
||||||
* [x] Continous Integration Testing
|
* [x] Continous Integration Testing
|
||||||
* [x] Multiple Domains
|
* [x] Multiple Domains
|
||||||
* Postfix MTA
|
* Postfix
|
||||||
- [x] smtp on port 25
|
* [x] SMTP on port 25
|
||||||
- [x] submission tls on port 465
|
* [x] Submission TLS on port 465
|
||||||
- [x] submission starttls on port 587
|
* [x] Submission StartTLS on port 587
|
||||||
- [x] lmtp with dovecot
|
* [x] LMTP with Dovecot
|
||||||
|
* [x] DANE and MTA-STS validation
|
||||||
|
* [x] SMTP TLS Reports ([RFC 8460](https://www.rfc-editor.org/rfc/rfc8460))
|
||||||
* Dovecot
|
* Dovecot
|
||||||
- [x] maildir folders
|
* [x] Maildir folders
|
||||||
- [x] imap with tls on port 993
|
* [x] IMAP with TLS on port 993
|
||||||
- [x] pop3 with tls on port 995
|
* [x] POP3 with TLS on port 995
|
||||||
- [x] imap with starttls on port 143
|
* [x] IMAP with StartTLS on port 143
|
||||||
- [x] pop3 with starttls on port 110
|
* [x] POP3 with StartTLS on port 110
|
||||||
* Certificates
|
* Certificates
|
||||||
- [x] manual certificates
|
* [x] ACME
|
||||||
- [x] on the fly creation
|
* [x] Custom certificates
|
||||||
- [x] Let's Encrypt
|
|
||||||
* Spam Filtering
|
* Spam Filtering
|
||||||
- [x] via rspamd
|
* [x] Via Rspamd
|
||||||
* Virus Scanning
|
* Virus Scanning
|
||||||
- [x] via clamav
|
* [x] Via ClamAV
|
||||||
* DKIM Signing
|
* DKIM Signing
|
||||||
- [x] via opendkim
|
* [x] Via Rspamd
|
||||||
* User Management
|
* User Management
|
||||||
- [x] declarative user management
|
* [x] Declarative user management
|
||||||
- [x] declarative password management
|
* [x] Declarative password management
|
||||||
* Sieves
|
* [x] LDAP users
|
||||||
- [x] A simple standard script that moves spam
|
* Sieve
|
||||||
- [x] Allow user defined sieve scripts
|
* [x] Allow user defined sieve scripts
|
||||||
- [x] ManageSieve support
|
* [x] Moving mails from/to junk trains the Bayes filter
|
||||||
|
* [x] ManageSieve support
|
||||||
* User Aliases
|
* User Aliases
|
||||||
- [x] Regular aliases
|
* [x] Regular aliases
|
||||||
- [x] Catch all aliases
|
* [x] Catch all aliases
|
||||||
|
* Improve the Forwarding Experience
|
||||||
|
* [x] [Sender Rewriting Scheme](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme)
|
||||||
|
|
||||||
### In the future
|
### In the future
|
||||||
|
|
||||||
|
* Automatic client configuration
|
||||||
|
* [ ] [Autoconfig](https://web.archive.org/web/20210624004729/https://developer.mozilla.org/en-US/docs/Mozilla/Thunderbird/Autoconfiguration)
|
||||||
|
* [ ] [Autodiscovery](https://learn.microsoft.com/en-us/exchange/architecture/client-access/autodiscover?view=exchserver-2019)
|
||||||
|
* [ ] [Mobileconfig](https://support.apple.com/guide/profile-manager/distribute-profiles-manually-pmdbd71ebc9/mac)
|
||||||
* DKIM Signing
|
* DKIM Signing
|
||||||
- [ ] Allow a per domain selector
|
* [ ] Allow per domain selectors
|
||||||
|
* [ ] Allow passing DKIM signing keys
|
||||||
|
* Improve the Forwarding Experience
|
||||||
|
* [ ] Support [ARC](https://en.wikipedia.org/wiki/Authenticated_Received_Chain) signing with [Rspamd](https://rspamd.com/doc/modules/arc.html)
|
||||||
|
* User management
|
||||||
|
* [ ] Allow local and LDAP user to coexist
|
||||||
|
* OpenID Connect
|
||||||
|
* Depends on relevant clients adding support, e.g. [Thunderbird](https://bugzilla.mozilla.org/show_bug.cgi?id=1602166)
|
||||||
|
|
||||||
### Get in touch
|
### Get in touch
|
||||||
|
|
||||||
- Subscribe to the [mailing list](https://www.freelists.org/archive/snm/)
|
* Matrix: [#nixos-mailserver:nixos.org](https://matrix.to/#/#nixos-mailserver:nixos.org)
|
||||||
- Join the Libera Chat IRC channel `#nixos-mailserver`
|
* IRC: `#nixos-mailserver` on [Libera Chat](https://libera.chat/guides/connect)
|
||||||
|
|
||||||
## How to Set Up a 10/10 Mail Server Guide
|
## How to Set Up a 10/10 Mail Server Guide
|
||||||
|
|
||||||
@@ -82,16 +92,18 @@ For a complete list of options, [see in readthedocs](https://nixos-mailserver.re
|
|||||||
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page.
|
See the [How to Develop SNM](https://nixos-mailserver.readthedocs.io/en/latest/howto-develop.html) documentation page.
|
||||||
|
|
||||||
## 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)
|
||||||
|
|
||||||
### Alternative Implementations
|
### Alternative Implementations
|
||||||
|
|
||||||
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
|
* [NixCloud Webservices](https://github.com/nixcloud/nixcloud-webservices)
|
||||||
|
|
||||||
### Credits
|
### Credits
|
||||||
|
|
||||||
* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao)
|
* send mail graphic by [tnp_dreamingmao](https://thenounproject.com/dreamingmao)
|
||||||
from [TheNounProject](https://thenounproject.com/) is licensed under
|
from [TheNounProject](https://thenounproject.com/) is licensed under
|
||||||
[CC BY 3.0](http://creativecommons.org/~/3.0/)
|
[CC BY 3.0](http://creativecommons.org/~/3.0/)
|
||||||
* Logo made with [Logomakr.com](https://logomakr.com)
|
* Logo made with [Logomakr.com](https://logomakr.com)
|
||||||
|
|
||||||
|
|
||||||
[logo]: docs/logo.png
|
[logo]: docs/logo.png
|
||||||
|
|||||||
651
default.nix
651
default.nix
@@ -14,17 +14,64 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ config, lib, pkgs, ... }:
|
{
|
||||||
|
config,
|
||||||
with lib;
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
|
inherit (lib)
|
||||||
|
literalExpression
|
||||||
|
literalMD
|
||||||
|
mkDefault
|
||||||
|
mkEnableOption
|
||||||
|
mkOption
|
||||||
|
mkOptionType
|
||||||
|
mkRemovedOptionModule
|
||||||
|
mkRenamedOptionModule
|
||||||
|
types
|
||||||
|
warn
|
||||||
|
;
|
||||||
|
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.mailserver = {
|
options.mailserver = {
|
||||||
enable = mkEnableOption "nixos-mailserver";
|
enable = mkEnableOption "nixos-mailserver";
|
||||||
|
|
||||||
|
enableNixpkgsReleaseCheck = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Whether to check for a release mismatch between NixOS mailserver and
|
||||||
|
Nixpkgs.
|
||||||
|
|
||||||
|
Using mismatched versions is likely to cause compatibility issues
|
||||||
|
and may require migrations that make an eventual rollback tricky.
|
||||||
|
|
||||||
|
It is therefore highly recommended to use a release of NixOS mailserver
|
||||||
|
that corresponds with your chosen release of Nixpkgs.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
stateVersion = mkOption {
|
||||||
|
type = types.nullOr types.ints.positive;
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
Tracking stateful version changes as an incrementing number.
|
||||||
|
|
||||||
|
When a new release comes out we may require manual migration steps to
|
||||||
|
be completed, before the new version can be put into production.
|
||||||
|
|
||||||
|
If your `stateVersion` is too low one or multiple assertions may
|
||||||
|
trigger to give you instructions on what migrations steps are required
|
||||||
|
to continue. Increase the `stateVersion` as instructed by the assertion
|
||||||
|
message.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
openFirewall = mkOption {
|
openFirewall = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
@@ -37,6 +84,46 @@ in
|
|||||||
description = "The fully qualified domain name of the mail server.";
|
description = "The fully qualified domain name of the mail server.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
systemName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = "${cfg.systemDomain} mail system";
|
||||||
|
defaultText = literalExpression "\${config.mailserver.systemDomain} mail system";
|
||||||
|
example = "ACME Corp.";
|
||||||
|
description = ''
|
||||||
|
The sender name given in automated reports.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemContact = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
example = "postmaster@example.com";
|
||||||
|
description = ''
|
||||||
|
The email address where the administrative contact for this mail server is reachable.
|
||||||
|
|
||||||
|
Currently, this is only required when one of the following features is enabled:
|
||||||
|
- SMTP TLS reports (`mailserver.tlsrpt.enable`)
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
systemDomain = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default =
|
||||||
|
if (config.networking.domain != null && lib.elem config.networking.domain cfg.domains) then
|
||||||
|
config.networking.domain
|
||||||
|
else
|
||||||
|
lib.head cfg.domains;
|
||||||
|
defaultText = literalExpression ''
|
||||||
|
if config.networking.domain != null && lib.elem config.networking.domain cfg.domains then
|
||||||
|
config.networking.domain
|
||||||
|
else
|
||||||
|
lib.head cfg.domains
|
||||||
|
'';
|
||||||
|
example = literalExpression "config.networking.domain";
|
||||||
|
description = ''
|
||||||
|
The primary domain used for sending automated reports.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
domains = mkOption {
|
domains = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
example = [ "example.com" ];
|
example = [ "example.com" ];
|
||||||
@@ -46,7 +133,10 @@ in
|
|||||||
|
|
||||||
certificateDomains = mkOption {
|
certificateDomains = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
example = [ "imap.example.com" "pop3.example.com" ];
|
example = [
|
||||||
|
"imap.example.com"
|
||||||
|
"pop3.example.com"
|
||||||
|
];
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
({option}`mailserver.certificateScheme` == `acme-nginx`)
|
({option}`mailserver.certificateScheme` == `acme-nginx`)
|
||||||
@@ -63,7 +153,10 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
loginAccounts = mkOption {
|
loginAccounts = mkOption {
|
||||||
type = types.attrsOf (types.submodule ({ name, ... }: {
|
type = types.attrsOf (
|
||||||
|
types.submodule (
|
||||||
|
{ name, ... }:
|
||||||
|
{
|
||||||
options = {
|
options = {
|
||||||
name = mkOption {
|
name = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
@@ -102,7 +195,10 @@ in
|
|||||||
|
|
||||||
aliases = mkOption {
|
aliases = mkOption {
|
||||||
type = with types; listOf types.str;
|
type = with types; listOf types.str;
|
||||||
example = ["abuse@example.com" "postmaster@example.com"];
|
example = [
|
||||||
|
"abuse@example.com"
|
||||||
|
"postmaster@example.com"
|
||||||
|
];
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
A list of aliases of this login account.
|
A list of aliases of this login account.
|
||||||
@@ -122,7 +218,10 @@ in
|
|||||||
|
|
||||||
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"
|
||||||
|
];
|
||||||
default = [ ];
|
default = [ ];
|
||||||
description = ''
|
description = ''
|
||||||
For which domains should this account act as a catch all?
|
For which domains should this account act as a catch all?
|
||||||
@@ -186,7 +285,9 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
config.name = mkDefault name;
|
config.name = mkDefault name;
|
||||||
}));
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
example = {
|
example = {
|
||||||
user1 = {
|
user1 = {
|
||||||
hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
|
hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
|
||||||
@@ -234,7 +335,7 @@ in
|
|||||||
tlsCAFile = mkOption {
|
tlsCAFile = mkOption {
|
||||||
type = types.path;
|
type = types.path;
|
||||||
default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
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)";
|
defaultText = literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
|
||||||
description = ''
|
description = ''
|
||||||
Certifificate trust anchors used to verify the LDAP server certificate.
|
Certifificate trust anchors used to verify the LDAP server certificate.
|
||||||
'';
|
'';
|
||||||
@@ -268,7 +369,11 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
searchScope = mkOption {
|
searchScope = mkOption {
|
||||||
type = types.enum [ "sub" "base" "one" ];
|
type = types.enum [
|
||||||
|
"sub"
|
||||||
|
"base"
|
||||||
|
"one"
|
||||||
|
];
|
||||||
default = "sub";
|
default = "sub";
|
||||||
description = ''
|
description = ''
|
||||||
Search scope below which users accounts are looked for.
|
Search scope below which users accounts are looked for.
|
||||||
@@ -277,26 +382,26 @@ in
|
|||||||
|
|
||||||
dovecot = {
|
dovecot = {
|
||||||
userAttrs = mkOption {
|
userAttrs = mkOption {
|
||||||
type = types.str;
|
type = types.nullOr types.str;
|
||||||
default = "";
|
default = null;
|
||||||
description = ''
|
description = ''
|
||||||
LDAP attributes to be retrieved during userdb lookups.
|
LDAP attributes to be retrieved during userdb lookups.
|
||||||
|
|
||||||
See the users_attrs reference at
|
See the users_attrs reference at
|
||||||
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-attrs
|
https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#user-attrs
|
||||||
in the Dovecot manual.
|
in the Dovecot manual.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
userFilter = mkOption {
|
userFilter = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "mail=%u";
|
default = "mail=%{user}";
|
||||||
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
|
example = "(&(objectClass=inetOrgPerson)(mail=%{user}))";
|
||||||
description = ''
|
description = ''
|
||||||
Filter for user lookups in Dovecot.
|
Filter for user lookups in Dovecot.
|
||||||
|
|
||||||
See the user_filter reference at
|
See the user_filter reference at
|
||||||
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-filter
|
https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#user-filter
|
||||||
in the Dovecot manual.
|
in the Dovecot manual.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
@@ -308,20 +413,20 @@ in
|
|||||||
LDAP attributes to be retrieved during passdb lookups.
|
LDAP attributes to be retrieved during passdb lookups.
|
||||||
|
|
||||||
See the pass_attrs reference at
|
See the pass_attrs reference at
|
||||||
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-attrs
|
https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#pass-attrs
|
||||||
in the Dovecot manual.
|
in the Dovecot manual.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
passFilter = mkOption {
|
passFilter = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = "mail=%u";
|
default = "mail=%{user}";
|
||||||
example = "(&(objectClass=inetOrgPerson)(mail=%u))";
|
example = "(&(objectClass=inetOrgPerson)(mail=%{user}))";
|
||||||
description = ''
|
description = ''
|
||||||
Filter for password lookups in Dovecot.
|
Filter for password lookups in Dovecot.
|
||||||
|
|
||||||
See the pass_filter reference for
|
See the pass_filter reference for
|
||||||
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-filter
|
https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#pass-filter
|
||||||
in the Dovecot manual.
|
in the Dovecot manual.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
@@ -373,14 +478,28 @@ in
|
|||||||
to resynchronize).
|
to resynchronize).
|
||||||
|
|
||||||
Note the some variables can be used in the file path. See
|
Note the some variables can be used in the file path. See
|
||||||
https://doc.dovecot.org/configuration_manual/mail_location/#variables
|
https://doc.dovecot.org/2.3/configuration_manual/mail_location/#variables
|
||||||
for details.
|
for details.
|
||||||
'';
|
'';
|
||||||
example = "/var/lib/dovecot/indices";
|
example = "/var/lib/dovecot/indices";
|
||||||
};
|
};
|
||||||
|
|
||||||
fullTextSearch = {
|
fullTextSearch = {
|
||||||
enable = mkEnableOption "Full text search indexing with xapian. This has significant performance and disk space cost.";
|
enable = mkEnableOption ''
|
||||||
|
Full text search indexing with Xapian through the fts_flatcurve plugin.
|
||||||
|
This has significant performance and disk space cost.
|
||||||
|
'';
|
||||||
|
memoryLimit = mkOption {
|
||||||
|
type = types.nullOr types.int;
|
||||||
|
default = null;
|
||||||
|
example = 2000;
|
||||||
|
description = ''
|
||||||
|
Memory limit for the indexer process, in MiB.
|
||||||
|
If null, leaves the default (which is rather low),
|
||||||
|
and if 0, no limit.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
autoIndex = mkOption {
|
autoIndex = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = true;
|
||||||
@@ -389,20 +508,22 @@ in
|
|||||||
autoIndexExclude = mkOption {
|
autoIndexExclude = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
default = [ ];
|
default = [ ];
|
||||||
example = [ "\\Trash" "SomeFolder" "Other/*" ];
|
example = [
|
||||||
|
"\\Trash"
|
||||||
|
"SomeFolder"
|
||||||
|
"Other/*"
|
||||||
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Mailboxes to exclude from automatic indexing.
|
Mailboxes to exclude from automatic indexing.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
indexAttachments = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = false;
|
|
||||||
description = "Also index text-only attachements. Binary attachements are never indexed.";
|
|
||||||
};
|
|
||||||
|
|
||||||
enforced = mkOption {
|
enforced = mkOption {
|
||||||
type = types.enum [ "yes" "no" "body" ];
|
type = types.enum [
|
||||||
|
"yes"
|
||||||
|
"no"
|
||||||
|
"body"
|
||||||
|
];
|
||||||
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
|
||||||
@@ -412,46 +533,65 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
minSize = mkOption {
|
languages = mkOption {
|
||||||
type = types.int;
|
type = types.nonEmptyListOf types.str;
|
||||||
default = 2;
|
default = [ "en" ];
|
||||||
description = "Size of the smallest n-gram to index.";
|
example = [
|
||||||
};
|
"en"
|
||||||
maxSize = mkOption {
|
"de"
|
||||||
type = types.int;
|
];
|
||||||
default = 20;
|
description = ''
|
||||||
description = "Size of the largest n-gram to index.";
|
A list of languages that the full text search should detect.
|
||||||
};
|
At least one language must be specified.
|
||||||
memoryLimit = mkOption {
|
The language listed first is the default and is used when language recognition fails.
|
||||||
type = types.nullOr types.int;
|
See <https://doc.dovecot.org/main/core/plugins/fts.html#fts_languages>.
|
||||||
default = null;
|
'';
|
||||||
example = 2000;
|
|
||||||
description = "Memory limit for the indexer process, in MiB. If null, leaves the default (which is rather low), and if 0, no limit.";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
maintenance = {
|
substringSearch = mkOption {
|
||||||
enable = mkOption {
|
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = false;
|
||||||
description = "Regularly optmize indices, as recommended by upstream.";
|
description = ''
|
||||||
|
If enabled, allows substring searches.
|
||||||
|
See <https://doc.dovecot.org/main/core/plugins/fts_flatcurve.html#fts_flatcurve_substring_search>.
|
||||||
|
|
||||||
|
Enabling this requires significant additional storage space.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
onCalendar = mkOption {
|
headerExcludes = mkOption {
|
||||||
type = types.str;
|
type = types.listOf types.str;
|
||||||
default = "daily";
|
default = [
|
||||||
description = "When to run the maintenance job. See systemd.time(7) for more information about the format.";
|
"Received"
|
||||||
|
"DKIM-*"
|
||||||
|
"X-*"
|
||||||
|
"Comments"
|
||||||
|
];
|
||||||
|
description = ''
|
||||||
|
The list of headers to exclude.
|
||||||
|
See <https://doc.dovecot.org/main/core/plugins/fts.html#fts_header_excludes>.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
randomizedDelaySec = mkOption {
|
filters = mkOption {
|
||||||
type = types.int;
|
type = types.listOf types.str;
|
||||||
default = 1000;
|
default = [
|
||||||
description = "Run the maintenance job not exactly at the time specified with `onCalendar`, but plus or minus this many seconds.";
|
"normalizer-icu"
|
||||||
};
|
"snowball"
|
||||||
|
"stopwords"
|
||||||
|
];
|
||||||
|
description = ''
|
||||||
|
The list of filters to apply.
|
||||||
|
<https://doc.dovecot.org/main/core/plugins/fts.html#filter-configuration>.
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
lmtpSaveToDetailMailbox = mkOption {
|
lmtpSaveToDetailMailbox = mkOption {
|
||||||
type = types.enum ["yes" "no"];
|
type = types.enum [
|
||||||
|
"yes"
|
||||||
|
"no"
|
||||||
|
];
|
||||||
default = "yes";
|
default = "yes";
|
||||||
description = ''
|
description = ''
|
||||||
If an email address is delimited by a "+", should it be filed into a
|
If an email address is delimited by a "+", should it be filed into a
|
||||||
@@ -460,18 +600,40 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
lmtpMemoryLimit = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 256;
|
||||||
|
description = ''
|
||||||
|
The memory limit for the LMTP service, in megabytes.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
quotaStatusMemoryLimit = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 256;
|
||||||
|
description = ''
|
||||||
|
The memory limit for the quota-status service, in megabytes.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
extraVirtualAliases = mkOption {
|
extraVirtualAliases = mkOption {
|
||||||
type = let
|
type =
|
||||||
|
let
|
||||||
loginAccount = mkOptionType {
|
loginAccount = mkOptionType {
|
||||||
name = "Login Account";
|
name = "Login Account";
|
||||||
check = (account: builtins.elem account (builtins.attrNames cfg.loginAccounts));
|
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 = {
|
||||||
"info@example.com" = "user1@example.com";
|
"info@example.com" = "user1@example.com";
|
||||||
"postmaster@example.com" = "user1@example.com";
|
"postmaster@example.com" = "user1@example.com";
|
||||||
"abuse@example.com" = "user1@example.com";
|
"abuse@example.com" = "user1@example.com";
|
||||||
"multi@example.com" = [ "user1@example.com" "user2@example.com" ];
|
"multi@example.com" = [
|
||||||
|
"user1@example.com"
|
||||||
|
"user2@example.com"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
description = ''
|
description = ''
|
||||||
Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that
|
Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that
|
||||||
@@ -506,7 +668,10 @@ in
|
|||||||
|
|
||||||
rejectSender = mkOption {
|
rejectSender = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
example = [ "@example.com" "spammer@example.net" ];
|
example = [
|
||||||
|
"example.com"
|
||||||
|
"spammer@example.net"
|
||||||
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Reject emails from these addresses from unauthorized senders.
|
Reject emails from these addresses from unauthorized senders.
|
||||||
Use if a spammer is using the same domain or the same sender over and over.
|
Use if a spammer is using the same domain or the same sender over and over.
|
||||||
@@ -516,7 +681,10 @@ in
|
|||||||
|
|
||||||
rejectRecipients = mkOption {
|
rejectRecipients = mkOption {
|
||||||
type = types.listOf types.str;
|
type = types.listOf types.str;
|
||||||
example = [ "sales@example.com" "info@example.com" ];
|
example = [
|
||||||
|
"sales@example.com"
|
||||||
|
"info@example.com"
|
||||||
|
];
|
||||||
description = ''
|
description = ''
|
||||||
Reject emails addressed to these local addresses from unauthorized senders.
|
Reject emails addressed to these local addresses from unauthorized senders.
|
||||||
Use if a spammer has found email addresses in a catchall domain but you do
|
Use if a spammer has found email addresses in a catchall domain but you do
|
||||||
@@ -570,7 +738,7 @@ in
|
|||||||
- /var/vmail/example.com/user/.folder.subfolder/ (default layout)
|
- /var/vmail/example.com/user/.folder.subfolder/ (default layout)
|
||||||
- /var/vmail/example.com/user/folder/subfolder/ (FS layout)
|
- /var/vmail/example.com/user/folder/subfolder/ (FS layout)
|
||||||
|
|
||||||
See https://wiki2.dovecot.org/MailboxFormat/Maildir for details.
|
See https://doc.dovecot.org/main/core/config/mailbox_formats/maildir.html#maildir-mailbox-format for details.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -591,7 +759,7 @@ in
|
|||||||
This affects how mailboxes appear to mail clients and sieve scripts.
|
This affects how mailboxes appear to mail clients and sieve scripts.
|
||||||
For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example".
|
For instance when using "." then in a sieve script "example.com" would refer to the mailbox "com" in the parent mailbox "example".
|
||||||
This does not determine the way your mails are stored on disk.
|
This does not determine the way your mails are stored on disk.
|
||||||
See https://wiki.dovecot.org/Namespaces for details.
|
See https://doc.dovecot.org/main/core/config/namespaces.html#namespaces for details.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -620,12 +788,30 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
certificateScheme = let
|
certificateScheme =
|
||||||
schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ];
|
let
|
||||||
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))}\"'."
|
schemes = [
|
||||||
|
"manual"
|
||||||
|
"selfsigned"
|
||||||
|
"acme-nginx"
|
||||||
|
"acme"
|
||||||
|
];
|
||||||
|
translate =
|
||||||
|
i:
|
||||||
|
warn
|
||||||
|
"Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${
|
||||||
|
(builtins.elemAt schemes (i - 1))
|
||||||
|
}\"'."
|
||||||
(builtins.elemAt schemes (i - 1));
|
(builtins.elemAt schemes (i - 1));
|
||||||
in mkOption {
|
in
|
||||||
type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes);
|
mkOption {
|
||||||
|
type =
|
||||||
|
with types;
|
||||||
|
coercedTo (enum [
|
||||||
|
1
|
||||||
|
2
|
||||||
|
3
|
||||||
|
]) translate (enum schemes);
|
||||||
default = "selfsigned";
|
default = "selfsigned";
|
||||||
description = ''
|
description = ''
|
||||||
The scheme to use for managing TLS certificates:
|
The scheme to use for managing TLS certificates:
|
||||||
@@ -675,11 +861,35 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
acmeCertificateName = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
default = cfg.fqdn;
|
||||||
|
defaultText = literalExpression "config.mailserver.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 = false;
|
||||||
description = ''
|
description = ''
|
||||||
Whether to enable IMAP with STARTTLS on port 143.
|
Whether to enable IMAP with STARTTLS on port 143.
|
||||||
|
|
||||||
|
The use of this port is deprecated per RFC 8314 4.1.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
imapMemoryLimit = mkOption {
|
||||||
|
type = types.int;
|
||||||
|
default = 256;
|
||||||
|
description = ''
|
||||||
|
The memory limit for the imap service, in megabytes.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -693,9 +903,11 @@ in
|
|||||||
|
|
||||||
enableSubmission = mkOption {
|
enableSubmission = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = true;
|
default = false;
|
||||||
description = ''
|
description = ''
|
||||||
Whether to enable SMTP with STARTTLS on port 587.
|
Whether to enable SMTP with STARTTLS on port 587.
|
||||||
|
|
||||||
|
The use of this port is discouraged per RFC 8314 3.3, see also Appendix A.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -712,6 +924,8 @@ in
|
|||||||
default = false;
|
default = false;
|
||||||
description = ''
|
description = ''
|
||||||
Whether to enable POP3 with STARTTLS on port on port 110.
|
Whether to enable POP3 with STARTTLS on port on port 110.
|
||||||
|
|
||||||
|
The use of this port is deprecated per RFC 8314 4.1.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -776,11 +990,27 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dkimKeyType = mkOption {
|
||||||
|
type = types.enum [
|
||||||
|
"rsa"
|
||||||
|
"ed25519"
|
||||||
|
];
|
||||||
|
default = "rsa";
|
||||||
|
description = ''
|
||||||
|
The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018).
|
||||||
|
|
||||||
|
If you have already deployed a key with a different type than specified
|
||||||
|
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
|
||||||
|
this package to generate a key with the new type, you will either have to
|
||||||
|
change the selector or delete the old key file.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
dkimKeyBits = mkOption {
|
dkimKeyBits = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 1024;
|
default = 2048;
|
||||||
description = ''
|
description = ''
|
||||||
How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
|
How many bits in generated DKIM keys. RFC8301 suggests a minimum RSA key length of 2048 bit.
|
||||||
|
|
||||||
If you have already deployed a key with a different number of bits than specified
|
If you have already deployed a key with a different number of bits than specified
|
||||||
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
|
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
|
||||||
@@ -789,26 +1019,6 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
dkimHeaderCanonicalization = mkOption {
|
|
||||||
type = types.enum ["relaxed" "simple"];
|
|
||||||
default = "relaxed";
|
|
||||||
description = ''
|
|
||||||
DKIM canonicalization algorithm for message headers.
|
|
||||||
|
|
||||||
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
dkimBodyCanonicalization = mkOption {
|
|
||||||
type = types.enum ["relaxed" "simple"];
|
|
||||||
default = "relaxed";
|
|
||||||
description = ''
|
|
||||||
DKIM canonicalization algorithm for message bodies.
|
|
||||||
|
|
||||||
See https://datatracker.ietf.org/doc/html/rfc6376/#section-3.4 for details.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
dmarcReporting = {
|
dmarcReporting = {
|
||||||
enable = mkOption {
|
enable = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
@@ -824,62 +1034,47 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
localpart = mkOption {
|
excludeDomains = mkOption {
|
||||||
type = types.str;
|
type = types.listOf types.str;
|
||||||
default = "dmarc-noreply";
|
default = [ ];
|
||||||
example = "dmarc-report";
|
|
||||||
description = ''
|
description = ''
|
||||||
The local part of the email address used for outgoing DMARC reports.
|
List of domains or eSLDs to be excluded from 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 {
|
tlsrpt.enable = mkEnableOption "delivery of SMTP TLS reports according to RFC 8460";
|
||||||
|
|
||||||
|
debug = {
|
||||||
|
all = mkOption {
|
||||||
type = types.bool;
|
type = types.bool;
|
||||||
default = false;
|
default = false;
|
||||||
description = ''
|
description = ''
|
||||||
Whether to enable verbose logging for mailserver related services. This
|
Whether to enable verbose logging for all mailserver related services.
|
||||||
intended be used for development purposes only, you probably don't want
|
This intended be used for development purposes only, you probably
|
||||||
to enable this unless you're hacking on nixos-mailserver.
|
don't want to enable this unless you're hacking on nixos-mailserver.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dovecot = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = cfg.debug.all;
|
||||||
|
defaultText = lib.literalExpression "config.mailserver.debug.all";
|
||||||
|
description = ''
|
||||||
|
Whether to enable verbose logging for Dovecot.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
rspamd = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = cfg.debug.all;
|
||||||
|
defaultText = lib.literalExpression "config.mailserver.debug.all";
|
||||||
|
description = ''
|
||||||
|
Whether to enable verbose logging for Rspamd.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
maxConnectionsPerUser = mkOption {
|
maxConnectionsPerUser = mkOption {
|
||||||
type = types.int;
|
type = types.int;
|
||||||
default = 100;
|
default = 100;
|
||||||
@@ -906,39 +1101,60 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
srs = {
|
||||||
|
enable = mkEnableOption "Sender Rewrite Scheme";
|
||||||
|
|
||||||
|
domain = mkOption {
|
||||||
|
type = with types; nullOr str;
|
||||||
|
default = config.mailserver.systemDomain;
|
||||||
|
defaultText = literalExpression "config.mailserver.systemDomain";
|
||||||
|
example = "srs.example.com";
|
||||||
|
description = ''
|
||||||
|
Mail domain used for ephemeral SRS envelope addresses.
|
||||||
|
|
||||||
|
:::{note}
|
||||||
|
This domain can only support relaxed SPF alignment.
|
||||||
|
:::
|
||||||
|
|
||||||
|
:::{important}
|
||||||
|
For privacy reasons you should use a dedicated domain when serving multiple unrelated domains.
|
||||||
|
:::
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
redis = {
|
redis = {
|
||||||
|
configureLocally = mkOption {
|
||||||
|
type = types.bool;
|
||||||
|
default = true;
|
||||||
|
description = ''
|
||||||
|
Whether to provision a local Redis instance.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
address = mkOption {
|
address = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
# read the default from nixos' redis module
|
# read the default from nixos' redis module
|
||||||
default = let
|
default = config.services.redis.servers.rspamd.unixSocket;
|
||||||
cf = config.services.redis.servers.rspamd.bind;
|
defaultText = literalExpression "config.services.redis.servers.rspamd.unixSocket";
|
||||||
cfdefault = if cf == null then "127.0.0.1" else cf;
|
|
||||||
ips = lib.strings.splitString " " cfdefault;
|
|
||||||
ip = lib.lists.head (ips ++ [ "127.0.0.1" ]);
|
|
||||||
isIpv6 = ip: lib.lists.elem ":" (lib.stringToCharacters ip);
|
|
||||||
in
|
|
||||||
if (ip == "0.0.0.0" || ip == "::")
|
|
||||||
then "127.0.0.1"
|
|
||||||
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.
|
Path, IP address or hostname that Rspamd should use to contact Redis.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
port = mkOption {
|
port = mkOption {
|
||||||
type = types.port;
|
type = with types; nullOr port;
|
||||||
default = config.services.redis.servers.rspamd.port;
|
default = null;
|
||||||
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.port";
|
example = literalExpression "config.services.redis.servers.rspamd.port";
|
||||||
description = ''
|
description = ''
|
||||||
Port that rspamd should use to contact redis.
|
Port that Rspamd should use to contact Redis.
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
password = mkOption {
|
password = mkOption {
|
||||||
type = types.nullOr types.str;
|
type = types.nullOr types.str;
|
||||||
default = config.services.redis.servers.rspamd.requirePass;
|
default = config.services.redis.servers.rspamd.requirePass;
|
||||||
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.requirePass";
|
defaultText = literalExpression "config.services.redis.servers.rspamd.requirePass";
|
||||||
description = ''
|
description = ''
|
||||||
Password that rspamd should use to contact redis, or null if not required.
|
Password that rspamd should use to contact redis, or null if not required.
|
||||||
'';
|
'';
|
||||||
@@ -955,25 +1171,10 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
smtpdForbidBareNewline = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = true;
|
|
||||||
description = ''
|
|
||||||
With "smtpd_forbid_bare_newline = yes", the Postfix SMTP server
|
|
||||||
disconnects a remote SMTP client that sends a line ending in a 'bare
|
|
||||||
newline'.
|
|
||||||
|
|
||||||
This feature was added in Postfix 3.8.4 against SMTP Smuggling and will
|
|
||||||
default to "yes" in Postfix 3.9.
|
|
||||||
|
|
||||||
https://www.postfix.org/smtp-smuggling.html
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
sendingFqdn = mkOption {
|
sendingFqdn = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = cfg.fqdn;
|
default = cfg.fqdn;
|
||||||
defaultText = lib.literalMD "{option}`mailserver.fqdn`";
|
defaultText = 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
|
||||||
@@ -997,18 +1198,6 @@ in
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
policydSPFExtraConfig = mkOption {
|
|
||||||
type = types.lines;
|
|
||||||
default = "";
|
|
||||||
example = ''
|
|
||||||
skip_addresses = 127.0.0.0/8,::ffff:127.0.0.0/104,::1
|
|
||||||
'';
|
|
||||||
description = ''
|
|
||||||
Extra configuration options for policyd-spf. This can be use to among
|
|
||||||
other things skip spf checking for some IP addresses.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
monitoring = {
|
monitoring = {
|
||||||
enable = mkEnableOption "monitoring via monit";
|
enable = mkEnableOption "monitoring via monit";
|
||||||
|
|
||||||
@@ -1061,7 +1250,7 @@ in
|
|||||||
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)";
|
defaultText = 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 ...'.
|
||||||
@@ -1103,7 +1292,15 @@ in
|
|||||||
|
|
||||||
compression = {
|
compression = {
|
||||||
method = mkOption {
|
method = mkOption {
|
||||||
type = types.nullOr (types.enum ["none" "lz4" "zstd" "zlib" "lzma"]);
|
type = types.nullOr (
|
||||||
|
types.enum [
|
||||||
|
"none"
|
||||||
|
"lz4"
|
||||||
|
"zstd"
|
||||||
|
"zlib"
|
||||||
|
"lzma"
|
||||||
|
]
|
||||||
|
);
|
||||||
default = null;
|
default = null;
|
||||||
description = "Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4.";
|
description = "Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4.";
|
||||||
};
|
};
|
||||||
@@ -1162,7 +1359,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 ]";
|
defaultText = 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.";
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1202,27 +1399,6 @@ in
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
rebootAfterKernelUpgrade = {
|
|
||||||
enable = mkOption {
|
|
||||||
type = types.bool;
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
description = ''
|
|
||||||
Whether to enable automatic reboot after kernel upgrades.
|
|
||||||
This is to be used in conjunction with `system.autoUpgrade.enable = true;`
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
method = mkOption {
|
|
||||||
type = types.enum [ "reboot" "systemctl kexec" ];
|
|
||||||
default = "reboot";
|
|
||||||
description = ''
|
|
||||||
Whether to issue a full "reboot" or just a "systemctl kexec"-only reboot.
|
|
||||||
It is recommended to use the default value because the quicker kexec reboot has a number of problems.
|
|
||||||
Also if your server is running in a virtual machine the regular reboot will already be very quick.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
backup = {
|
backup = {
|
||||||
enable = mkEnableOption "backup via rsnapshot";
|
enable = mkEnableOption "backup via rsnapshot";
|
||||||
|
|
||||||
@@ -1284,9 +1460,33 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
imports = [
|
imports = [
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] ''
|
||||||
|
This option is not needed for fts-flatcurve
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] ''
|
||||||
|
This option is not needed for fts-flatcurve
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] ''
|
||||||
|
This option is not needed for fts-flatcurve
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] ''
|
||||||
|
This option is not supported by fts-flatcurve
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] ''
|
||||||
|
This option is not needed since fts-xapian 1.8.3
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] ''
|
||||||
|
Text attachments are always indexed since fts-xapian 1.4.8
|
||||||
|
'')
|
||||||
|
(mkRenamedOptionModule
|
||||||
|
[ "mailserver" "rebootAfterKernelUpgrade" "enable" ]
|
||||||
|
[ "system" "autoUpgrade" "allowReboot" ]
|
||||||
|
)
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "method" ] ''
|
||||||
|
Use `system.autoUpgrade` instead.
|
||||||
|
'')
|
||||||
./mail-server/assertions.nix
|
./mail-server/assertions.nix
|
||||||
./mail-server/borgbackup.nix
|
./mail-server/borgbackup.nix
|
||||||
./mail-server/debug.nix
|
|
||||||
./mail-server/rsnapshot.nix
|
./mail-server/rsnapshot.nix
|
||||||
./mail-server/clamav.nix
|
./mail-server/clamav.nix
|
||||||
./mail-server/monit.nix
|
./mail-server/monit.nix
|
||||||
@@ -1295,11 +1495,36 @@ in
|
|||||||
./mail-server/networking.nix
|
./mail-server/networking.nix
|
||||||
./mail-server/systemd.nix
|
./mail-server/systemd.nix
|
||||||
./mail-server/dovecot.nix
|
./mail-server/dovecot.nix
|
||||||
./mail-server/opendkim.nix
|
|
||||||
./mail-server/postfix.nix
|
./mail-server/postfix.nix
|
||||||
./mail-server/rspamd.nix
|
./mail-server/rspamd.nix
|
||||||
./mail-server/nginx.nix
|
./mail-server/nginx.nix
|
||||||
./mail-server/kresd.nix
|
./mail-server/kresd.nix
|
||||||
./mail-server/post-upgrade-check.nix
|
(mkRemovedOptionModule [ "mailserver" "policydSPFExtraConfig" ] ''
|
||||||
|
SPF checking has been migrated to Rspamd, which makes this config redundant. Please look into the rspamd config to migrate your settings.
|
||||||
|
It may be that they are redundant and are already configured in rspamd like for skip_addresses.
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "dkimHeaderCanonicalization" ] ''
|
||||||
|
DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization.
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "dkimBodyCanonicalization" ] ''
|
||||||
|
DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization.
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "smtpdForbidBareNewline" ] ''
|
||||||
|
The workaround for the SMTP Smuggling attack is default enabled in Postfix >3.9. Use `services.postfix.config.smtpd_forbid_bare_newline` if you need to deviate from its default.
|
||||||
|
'')
|
||||||
|
(mkRenamedOptionModule [ "mailserver" "dmarcReporting" "domain" ] [ "mailserver" "systemDomain" ])
|
||||||
|
(mkRenamedOptionModule
|
||||||
|
[ "mailserver" "dmarcReporting" "organizationName" ]
|
||||||
|
[ "mailserver" "systemName" ]
|
||||||
|
)
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "dmarcReporting" "localpart" ] ''
|
||||||
|
The localpart is now fixed at `noreply-dmarc` to simplify the configuration.
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "dmarcReporting" "email" ] ''
|
||||||
|
The address is now fixed at `noreply-dmarc@''${config.mailserver.systemDomain}` to simplify the configuration.
|
||||||
|
'')
|
||||||
|
(mkRemovedOptionModule [ "mailserver" "dmarcReporting" "fromName" ] ''
|
||||||
|
The name in the `FROM` field for DMARC report now uses the `mailserver.systemName`.
|
||||||
|
'')
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,12 +24,13 @@ have to be used. These can still be generated using `mkpasswd -m bcrypt`.
|
|||||||
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 = {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ servers may require more work.
|
|||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
# starttls needed for authentication, so the fqdn required to match
|
# starttls needed for authentication, so the fqdn required to match
|
||||||
# the certificate
|
# the certificate
|
||||||
$config['smtp_server'] = "tls://${config.mailserver.fqdn}";
|
$config['smtp_host'] = "tls://${config.mailserver.fqdn}";
|
||||||
$config['smtp_user'] = "%u";
|
$config['smtp_user'] = "%u";
|
||||||
$config['smtp_pass'] = "%p";
|
$config['smtp_pass'] = "%p";
|
||||||
'';
|
'';
|
||||||
|
|||||||
14
docs/advanced-configurations.rst
Normal file
14
docs/advanced-configurations.rst
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
Advanced Configurations
|
||||||
|
=======================
|
||||||
|
|
||||||
|
Congratulations on completing the `Setup Guide <setup-guide.html>`_!
|
||||||
|
|
||||||
|
If you're an experienced mailserver admin, then you probably know what you want
|
||||||
|
to do next. Our How-to guides (accessible in the navigation sidebar)
|
||||||
|
might help you accomplish your goals. If not, consider contributing a guide!
|
||||||
|
|
||||||
|
If this is your first mailserver, consider the following:
|
||||||
|
|
||||||
|
- Set up `backups <backup-guide.html>`_.
|
||||||
|
- Enable `DMARC reporting <options.html#mailserver-dmarcreporting>`_ to be a
|
||||||
|
good citizen in the mail ecosystem.
|
||||||
@@ -14,6 +14,13 @@ forget to ``chown`` them to ``virtualMail:virtualMail`` if you copy them
|
|||||||
back (or whatever you specified as ``vmailUserName``, and
|
back (or whatever you specified as ``vmailUserName``, and
|
||||||
``vmailGoupName``).
|
``vmailGoupName``).
|
||||||
|
|
||||||
|
If you enabled ``enableManageSieve`` then you also may want to backup
|
||||||
|
``/var/sieve`` or whatever you have specified as ``sieveDirectory``.
|
||||||
|
The same considerations regarding file ownership apply as for the
|
||||||
|
Maildir.
|
||||||
|
|
||||||
|
To backup spam and ham training data, backup ``/var/lib/redis-rspamd``.
|
||||||
|
|
||||||
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever
|
Finally you can (optionally) make a backup of ``/var/dkim`` (or whatever
|
||||||
you specified as ``dkimKeyDirectory``). If you should lose those don’t
|
you specified as ``dkimKeyDirectory``). If you should lose those don’t
|
||||||
worry, new ones will be created on the fly. But you will need to repeat
|
worry, new ones will be created on the fly. But you will need to repeat
|
||||||
|
|||||||
22
docs/conf.py
22
docs/conf.py
@@ -17,9 +17,9 @@
|
|||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
||||||
project = 'NixOS Mailserver'
|
project = "NixOS Mailserver"
|
||||||
copyright = '2022, NixOS Mailserver Contributors'
|
copyright = "2022, NixOS Mailserver Contributors"
|
||||||
author = 'NixOS Mailserver Contributors'
|
author = "NixOS Mailserver Contributors"
|
||||||
|
|
||||||
|
|
||||||
# -- General configuration ---------------------------------------------------
|
# -- General configuration ---------------------------------------------------
|
||||||
@@ -27,33 +27,31 @@ author = 'NixOS Mailserver Contributors'
|
|||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# 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_parser'
|
|
||||||
]
|
|
||||||
|
|
||||||
myst_enable_extensions = [
|
myst_enable_extensions = [
|
||||||
'colon_fence',
|
"colon_fence",
|
||||||
'linkify',
|
"linkify",
|
||||||
]
|
]
|
||||||
|
|
||||||
smartquotes = False
|
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"]
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
# This pattern also affects html_static_path and html_extra_path.
|
# This pattern also affects html_static_path and html_extra_path.
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||||
|
|
||||||
master_doc = 'index'
|
master_doc = "index"
|
||||||
|
|
||||||
# -- Options for HTML output -------------------------------------------------
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# a list of builtin themes.
|
||||||
#
|
#
|
||||||
html_theme = 'sphinx_rtd_theme'
|
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,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Nix Flakes
|
Nix Flakes
|
||||||
==========
|
==========
|
||||||
|
|
||||||
If you're using `flakes <https://nixos.wiki/wiki/Flakes>`__, you can use
|
If you're using `flakes <https://wiki.nixos.org/wiki/Flakes>`__, you can use
|
||||||
the following minimal ``flake.nix`` as an example:
|
the following minimal ``flake.nix`` as an example:
|
||||||
|
|
||||||
.. code:: nix
|
.. code:: nix
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Full text search
|
|||||||
By default, when your IMAP client searches for an email containing some
|
By default, when your IMAP client searches for an email containing some
|
||||||
text in its *body*, dovecot will read all your email sequentially. This
|
text in its *body*, dovecot will read all your email sequentially. This
|
||||||
is very slow and IO intensive. To speed body searches up, it is possible to
|
is very slow and IO intensive. To speed body searches up, it is possible to
|
||||||
*index* emails with a plugin to dovecot, ``fts_xapian``.
|
*index* emails with a plugin to dovecot, ``fts_flatcurve``.
|
||||||
|
|
||||||
Enabling full text search
|
Enabling full text search
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
@@ -20,8 +20,6 @@ To enable indexing for full text search here is an example configuration.
|
|||||||
enable = true;
|
enable = true;
|
||||||
# index new email as they arrive
|
# index new email as they arrive
|
||||||
autoIndex = true;
|
autoIndex = true;
|
||||||
# this only applies to plain text attachments, binary attachments are never indexed
|
|
||||||
indexAttachments = true;
|
|
||||||
enforced = "body";
|
enforced = "body";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -61,8 +59,8 @@ Mitigating resources requirements
|
|||||||
|
|
||||||
You can:
|
You can:
|
||||||
|
|
||||||
* disable indexation of attachements ``mailserver.fullTextSearch.indexAttachments = false``
|
* exclude some headers from indexation with ``mailserver.fullTextSearch.headerExcludes``
|
||||||
* reduce the size of ngrams to be indexed ``mailserver.fullTextSearch.minSize`` and ``maxSize``
|
* disable expensive token normalisation in ``mailserver.fullTextSearch.filters``
|
||||||
* disable automatic indexation for some folders with
|
* disable automatic indexation for some folders with
|
||||||
``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by
|
``mailserver.fullTextSearch.autoIndexExclude``. Folders can be specified by
|
||||||
name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard.
|
name (``"Trash"``), by special use (``"\\Junk"``) or with a wildcard.
|
||||||
|
|||||||
@@ -4,13 +4,33 @@ Contribute or troubleshoot
|
|||||||
To report an issue, please go to
|
To report an issue, please go to
|
||||||
`<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues>`_.
|
`<https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues>`_.
|
||||||
|
|
||||||
You can also chat with us on the Libera IRC channel ``#nixos-mailserver``.
|
If you have questions, feel free to reach out:
|
||||||
|
|
||||||
|
* Matrix: `#nixos-mailserver:nixos.org <https://matrix.to/#/#nixos-mailserver:nixos.org>`__
|
||||||
|
* IRC: `#nixos-mailserver <ircs://irc.libera.chat/nixos-mailserver>`__ on `Libera Chat <https://libera.chat/guides/connect>`__
|
||||||
|
|
||||||
|
All our workflows rely on Nix being configured with `Flakes <https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
|
||||||
|
|
||||||
|
Development Shell
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
We provide a `flake.nix` devshell that automatically sets up pre-commit hooks,
|
||||||
|
which allows for fast feedback cycles when making changes to the repository.
|
||||||
|
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
$ nix develop
|
||||||
|
|
||||||
|
|
||||||
|
We recommend setting up `direnv <https://direnv.net/>`__ to automatically
|
||||||
|
attach to the development environment when entering the project directories.
|
||||||
|
|
||||||
Run NixOS tests
|
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://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
|
||||||
|
|
||||||
You can then run the testsuite via
|
You can then run the testsuite via
|
||||||
|
|
||||||
@@ -37,7 +57,7 @@ For the syntax, see the `RST/Sphinx primer
|
|||||||
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_.
|
<https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html>`_.
|
||||||
|
|
||||||
To build the documentation, you need to enable `Nix Flakes
|
To build the documentation, you need to enable `Nix Flakes
|
||||||
<https://nixos.wiki/wiki/Flakes#Installing_flakes>`_.
|
<https://wiki.nixos.org/wiki/Flakes#Installing_flakes>`__.
|
||||||
|
|
||||||
|
|
||||||
::
|
::
|
||||||
@@ -45,28 +65,43 @@ To build the documentation, you need to enable `Nix Flakes
|
|||||||
$ nix build .#documentation
|
$ nix build .#documentation
|
||||||
$ xdg-open result/index.html
|
$ xdg-open result/index.html
|
||||||
|
|
||||||
Nixops
|
|
||||||
------
|
|
||||||
|
|
||||||
You can test the setup via ``nixops``. After installation, do
|
Manual migrations
|
||||||
|
-----------------
|
||||||
|
|
||||||
::
|
We need to take great care around providing a migration story around breaking
|
||||||
|
changes. If manual intervention becomes necessary we provide the `stateVersion`
|
||||||
|
option to notify the user that they need to complete a migration before
|
||||||
|
they can deploy an update.
|
||||||
|
|
||||||
$ nixops create nixops/single-server.nix nixops/vbox.nix -d mail
|
If that is the case for your change, find the highest `stateVersion` that is
|
||||||
$ nixops deploy -d mail
|
being asserted on in `mail-server/assertions.nix`. Then pick the next number
|
||||||
$ nixops info -d mail
|
and add a new assertion, write a good summary describing the issue and what
|
||||||
|
remediation steps are necessary. Finally reference the URL to the specific
|
||||||
|
section on the migration page in the documentation.
|
||||||
|
|
||||||
You can then test the server via e.g. \ ``telnet``. To log into it, use
|
.. code-block:: nix
|
||||||
|
|
||||||
::
|
{
|
||||||
|
assertions = [
|
||||||
|
{
|
||||||
|
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 1;
|
||||||
|
message = ''
|
||||||
|
Problem: The home directory for the foobar service is snafu.
|
||||||
|
Remediation:
|
||||||
|
- Stop the `foobar.service`
|
||||||
|
- Rename `/var/lib/foobaz` to `/var/lib/foobar`
|
||||||
|
- Increase the `mailserver.stateVersion` to 1.
|
||||||
|
|
||||||
$ nixops ssh -d mail mailserver
|
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#specific-anchor-here for further details.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
Imap
|
The setup guide should always reference the latest `stateVersion`, since we
|
||||||
----
|
don't require any migration steps for new setups.
|
||||||
|
|
||||||
To test imap manually use
|
The migration documentation should paint a more complete picture about the steps
|
||||||
|
that need to be carried out and why this has become necessary. Make sure to
|
||||||
::
|
reference the correct anchor in the URL you put into the assertion message.
|
||||||
|
|
||||||
$ openssl s_client -host mail.example.com -port 143 -starttls imap
|
|
||||||
|
|||||||
@@ -14,23 +14,31 @@ Welcome to NixOS Mailserver's documentation!
|
|||||||
:maxdepth: 2
|
:maxdepth: 2
|
||||||
|
|
||||||
setup-guide
|
setup-guide
|
||||||
|
advanced-configurations
|
||||||
howto-develop
|
howto-develop
|
||||||
faq
|
faq
|
||||||
release-notes
|
release-notes
|
||||||
options
|
options
|
||||||
|
migrations
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
:caption: Features
|
||||||
|
|
||||||
|
fts
|
||||||
|
ldap
|
||||||
|
srs
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 0
|
||||||
:caption: How-to
|
:caption: How-to
|
||||||
|
|
||||||
backup-guide
|
backup-guide
|
||||||
add-radicale
|
add-radicale
|
||||||
add-roundcube
|
add-roundcube
|
||||||
rspamd-tuning
|
rspamd-tuning
|
||||||
fts
|
|
||||||
flakes
|
flakes
|
||||||
autodiscovery
|
autodiscovery
|
||||||
ldap
|
|
||||||
|
|
||||||
Indices and tables
|
Indices and tables
|
||||||
==================
|
==================
|
||||||
|
|||||||
117
docs/migrations.rst
Normal file
117
docs/migrations.rst
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
Migrations
|
||||||
|
==========
|
||||||
|
|
||||||
|
With mail server configuration best practices changing over time we might need
|
||||||
|
to make changes that require you to complete manual migration steps before you
|
||||||
|
can deploy a new version of NixOS mailserver.
|
||||||
|
|
||||||
|
The initial `mailserver.stateVersion` value should be copied from the setup
|
||||||
|
guide that you used to initially set up your mail server. If in doubt you can
|
||||||
|
always initialize it at `1` and walk through all assertions, that might apply
|
||||||
|
to your setup.
|
||||||
|
|
||||||
|
NixOS 25.11
|
||||||
|
-----------
|
||||||
|
|
||||||
|
#3 Dovecot mail directory migration
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The way the Dovecot home directory for login accounts were previously set up
|
||||||
|
resulted in shared home directories for all those users. This is not a
|
||||||
|
supported Dovecot configuration.
|
||||||
|
|
||||||
|
To resolve this we migrated the home directory into the individual
|
||||||
|
`domain/localpart` subdirectory below the `mailserver.mailDirectory`.
|
||||||
|
|
||||||
|
But since this now overlaps with the location of the Maildir, it must be
|
||||||
|
migrated into the `mail/` directory below the home directory.
|
||||||
|
And while the LDAP home directory is not affected we use this migration to
|
||||||
|
keep the Maildir configurations of LDAP users in sync with those of local
|
||||||
|
accounts.
|
||||||
|
|
||||||
|
This is a big step forward, since we can now more cleanly colocate other
|
||||||
|
data directories, like sieve in the home directory, which in turn simplifies
|
||||||
|
backups.
|
||||||
|
|
||||||
|
This migration is required for every configuration.
|
||||||
|
|
||||||
|
For remediating this issue the following steps are required:
|
||||||
|
|
||||||
|
1. Copy the `migration script <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/migrations/nixos-mailserver-migration-03.py>`_ script to your mailserver
|
||||||
|
and make it executable:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
wcurl https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/raw/master/migrations/nixos-mailserver-migration-03.py
|
||||||
|
chmod +x nixos-mailserver-migration-03.py
|
||||||
|
|
||||||
|
2. Stop the ``dovecot2.service``.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
systemctl stop dovecot2.service
|
||||||
|
|
||||||
|
3. Create a backup or snapshot of your ``mailserver.mailDirectory``, so you can restore
|
||||||
|
should anything go wrong.
|
||||||
|
|
||||||
|
4. Run the migration script under your virtual mail user with the following arguments:
|
||||||
|
|
||||||
|
- ``--layout default`` unless ``useFSLayout`` is enabled, then ``--layout folder``
|
||||||
|
- The value of ``mailserver.mailDirectory``, which defaults to ``/var/vmail``
|
||||||
|
|
||||||
|
The script should be run under the user who owns the ``mailDirectory``.
|
||||||
|
If run as root it will try to switch into the appropriate user by itself.
|
||||||
|
|
||||||
|
The script will not modify your data unless called with ``--execute``.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
./nixos-mailserver-migration-03.py --layout default /var/vmail
|
||||||
|
|
||||||
|
5. Review the commands. They should be
|
||||||
|
|
||||||
|
- create a ``mail`` directory for each accounnt,
|
||||||
|
- move maildir contents from the parent directory into it,
|
||||||
|
- suggest removal of files that do not belong to the maildir
|
||||||
|
|
||||||
|
- their removal is not mandatory and the script **will not** remove them when called with ``--execute``
|
||||||
|
- review these items carefully if you want to remove them yourself
|
||||||
|
|
||||||
|
- remove obsolete files from the old home directory location
|
||||||
|
|
||||||
|
6. Rerun the command with ``--execute`` or run the commands manually.
|
||||||
|
|
||||||
|
7. Update the ``mailserver.stateVersion`` to ``3``.
|
||||||
|
|
||||||
|
#2 Dovecot LDAP home directory migration
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The Dovecot configuration for LDAP home directories previously did not respect
|
||||||
|
the ``mailserver.mailDirectory`` setting.
|
||||||
|
|
||||||
|
This means that home directories were unconditionally located at
|
||||||
|
``/var/vmail/ldap/%{user}``.
|
||||||
|
|
||||||
|
This migration is required if you both:
|
||||||
|
|
||||||
|
* enabled the LDAP integration (``mailserver.ldap.enable``)
|
||||||
|
* and customized the default mail directory (``mailserver.mailDirectory != "/var/vmail"``)
|
||||||
|
|
||||||
|
For remediating this issue the following steps are required:
|
||||||
|
|
||||||
|
1. Stop ``dovecot2.service``.
|
||||||
|
2. Move ``/var/vmail/ldap`` below your ``m̀ailserver.mailDirectory``.
|
||||||
|
3. Update the ``mailserver.stateVersion`` to ``2``.
|
||||||
|
|
||||||
|
#1 Initialization
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
This option was introduced in the NixOS 25.11 release cycle, in which case you
|
||||||
|
can safely initialize its value at `1`.
|
||||||
|
|
||||||
|
.. code-block:: nix
|
||||||
|
|
||||||
|
mailserver.stateVersion = 1;
|
||||||
|
|
||||||
@@ -1,6 +1,94 @@
|
|||||||
Release Notes
|
Release Notes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
NixOS 25.11
|
||||||
|
-----------
|
||||||
|
|
||||||
|
- The ``systemName`` and ``systemDomain`` options have been introduced to have
|
||||||
|
reusable configurations for automated reports (DMARC, TLSRPT). They come with
|
||||||
|
reasonable defaults, but it is suggested to check and change them as needed.
|
||||||
|
- Support for the `Sender Rewriting Scheme`_ has been added, which allows
|
||||||
|
forwarding mail without breaking SPF by rewriting the envelope address.
|
||||||
|
- The default key length for new DKIM RSA keys was increased to 2048 bits as
|
||||||
|
recommended in `RFC 8301 3.2`_.
|
||||||
|
We recommend rotating existing keys, as the RFC advises that signatures from
|
||||||
|
1024 bit keys should not be considered valid any longer.
|
||||||
|
- IMAP access over port ``143/tcp`` is now default disabled in line
|
||||||
|
with `RFC 8314 4.1`_. Use IMAP over implicit TLS on port ``993/tcp``
|
||||||
|
instead. If you still require this feature you can reenable it using
|
||||||
|
``mailserver.enableImap``, but it is scheduled for removal after the 25.11
|
||||||
|
release.
|
||||||
|
- SMTP server and client now support and prefer a hybrid key exchange
|
||||||
|
(X25519MLKEM768)
|
||||||
|
- SMTP access over STARTTLS on port ``587/tcp`` is now default disabled in line
|
||||||
|
with `RFC 8314 3.3`_. If you still require this feature you can renable it using
|
||||||
|
``mailserver.enableSubmission``.
|
||||||
|
- DMARC reports are now sent with the ``noreply-dmarc`` localpart from the
|
||||||
|
system domain.
|
||||||
|
- DANE and MTA-STS are now validated for outgoing SMTP connections using
|
||||||
|
`postfix-tlspol`_.
|
||||||
|
- SMTP TLS connection reports (`RFC 8460`_) are now supported using
|
||||||
|
`tlsrpt-reporter`_. They can be enabled with the ``mailserver.tlsrpt.enable``
|
||||||
|
option.
|
||||||
|
|
||||||
|
.. _Sender Rewriting Scheme: srs.html
|
||||||
|
.. _RFC 8301 3.2: https://www.rfc-editor.org/rfc/rfc8301#section-3.2
|
||||||
|
.. _RFC 8314 3.3: https://www.rfc-editor.org/rfc/rfc8314#section-3.3
|
||||||
|
.. _RFC 8314 4.1: https://www.rfc-editor.org/rfc/rfc8314#section-4.1
|
||||||
|
.. _RFC 8460: https://www.rfc-editor.org/rfc/rfc8460
|
||||||
|
.. _postfix-tlspol: https://github.com/Zuplu/postfix-tlspol
|
||||||
|
.. _tlsrpt-reporter: https://github.com/sys4/tlsrpt-reporter
|
||||||
|
|
||||||
|
NixOS 25.05
|
||||||
|
-----------
|
||||||
|
|
||||||
|
- OpenDKIM has been removed and DKIM signing is now handled by Rspamd, which only supports ``relaxed`` canoncalizaliaton.
|
||||||
|
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/374>`__)
|
||||||
|
- Rspamd now connects to Redis over its Unix Domain Socket by default
|
||||||
|
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/375>`__)
|
||||||
|
|
||||||
|
- If you need to revert TCP connections, configure ``mailserver.redis.address`` to reference the value of ``config.services.redis.servers.rspamd.bind``.
|
||||||
|
- The integration with policyd-spf was removed and SPF handling is now fully based on Rspamd scoring.
|
||||||
|
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/380>`__)
|
||||||
|
- Switch to the more efficient `fts-flatcurve` indexer for full text search
|
||||||
|
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/361>`__).
|
||||||
|
|
||||||
|
This makes use of a new index, which will be automatically re-generated the
|
||||||
|
next time a folder is searched.
|
||||||
|
The operation is now quick enough to be performed "just-in-time".
|
||||||
|
Alternatively, all indices can be immediately re-generated for all users and
|
||||||
|
folders by running
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
doveadm fts rescan -u '*' && doveadm index -u '*' -q '*'
|
||||||
|
|
||||||
|
The previous index (which is not automatically discarded to allow rollbacks)
|
||||||
|
can be cleaned up by removing all the `xapian-indexes` directories within
|
||||||
|
``mailserver.indexDir``.
|
||||||
|
- Individual domains can now be excluded from DMARC Reporting through ``mailserver.dmarcReporting.excludedDomains``.
|
||||||
|
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/297>`__)
|
||||||
|
- Configuring ``mailserver.forwards`` is now possible when the setup relies on LDAP.
|
||||||
|
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/313>`__)
|
||||||
|
- Support for TLS 1.1 was disabled in accordance with `Mozilla's recommendations <https://ssl-config.mozilla.org/#server=postfix>`_.
|
||||||
|
(`merge request <https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/merge_requests/234>`__)
|
||||||
|
|
||||||
|
NixOS 24.11
|
||||||
|
-----------
|
||||||
|
|
||||||
|
- No new feature, only bug fixes and documentation improvements
|
||||||
|
|
||||||
|
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
|
NixOS 23.05
|
||||||
-----------
|
-----------
|
||||||
@@ -34,7 +122,6 @@ NixOS 21.11
|
|||||||
- New option ``certificateDomains`` to generate certificate for
|
- New option ``certificateDomains`` to generate certificate for
|
||||||
additional domains (such as ``imap.example.com``)
|
additional domains (such as ``imap.example.com``)
|
||||||
|
|
||||||
|
|
||||||
NixOS 21.05
|
NixOS 21.05
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ sphinx ~= 5.3
|
|||||||
sphinx_rtd_theme ~= 1.1
|
sphinx_rtd_theme ~= 1.1
|
||||||
myst-parser ~= 0.18
|
myst-parser ~= 0.18
|
||||||
linkify-it-py ~= 2.0
|
linkify-it-py ~= 2.0
|
||||||
|
standard-imghdr
|
||||||
|
|||||||
@@ -24,17 +24,14 @@ You can run the training in a root shell as follows:
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
# Path to the controller socket
|
|
||||||
export RSOCK="/var/run/rspamd/worker-controller.sock"
|
|
||||||
|
|
||||||
# Learn the Junk folder as spam
|
# Learn the Junk folder as spam
|
||||||
rspamc -h $RSOCK learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
|
rspamc learn_spam /var/vmail/$DOMAIN/$USER/.Junk/cur/
|
||||||
|
|
||||||
# Learn the INBOX as ham
|
# Learn the INBOX as ham
|
||||||
rspamc -h $RSOCK learn_ham /var/vmail/$DOMAIN/$USER/cur/
|
rspamc learn_ham /var/vmail/$DOMAIN/$USER/cur/
|
||||||
|
|
||||||
# Check that training was successful
|
# Check that training was successful
|
||||||
rspamc -h $RSOCK stat | grep learned
|
rspamc stat | grep learned
|
||||||
|
|
||||||
Tune symbol weight
|
Tune symbol weight
|
||||||
~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -20,25 +20,30 @@ an up and running mail server. Once the server is deployed, we could
|
|||||||
then set all DNS entries required to send and receive mails on this
|
then set all DNS entries required to send and receive mails on this
|
||||||
server.
|
server.
|
||||||
|
|
||||||
Setup DNS A record for server
|
Setup DNS A/AAAA records for server
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
Add a DNS record to the domain ``example.com`` with the following
|
Add DNS records to the domain ``example.com`` with the following
|
||||||
entries
|
entries
|
||||||
|
|
||||||
==================== ===== ==== =============
|
==================== ===== ==== =============
|
||||||
Name (Subdomain) TTL Type Value
|
Name (Subdomain) TTL Type Value
|
||||||
==================== ===== ==== =============
|
==================== ===== ==== =============
|
||||||
``mail.example.com`` 10800 A ``1.2.3.4``
|
``mail.example.com`` 10800 A ``1.2.3.4``
|
||||||
|
``mail.example.com`` 10800 AAAA ``2001::1``
|
||||||
==================== ===== ==== =============
|
==================== ===== ==== =============
|
||||||
|
|
||||||
|
If your server does not have an IPv6 address, you must skip the `AAAA` record.
|
||||||
|
|
||||||
You can check this with
|
You can check this with
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
$ ping mail.example.com
|
$ nix-shell -p bind --command "host -t A mail.example.com"
|
||||||
64 bytes from mail.example.com (1.2.3.4): icmp_seq=1 ttl=46 time=21.3 ms
|
mail.example.com has address 1.2.3.4
|
||||||
...
|
|
||||||
|
$ nix-shell -p bind --command "host -t AAAA mail.example.com"
|
||||||
|
mail.example.com has address 2001::1
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated. This
|
Note that it can take a while until a DNS entry is propagated. This
|
||||||
DNS entry is required for the Let's Encrypt certificate generation
|
DNS entry is required for the Let's Encrypt certificate generation
|
||||||
@@ -58,15 +63,16 @@ common ones.
|
|||||||
imports = [
|
imports = [
|
||||||
(builtins.fetchTarball {
|
(builtins.fetchTarball {
|
||||||
# Pick a release version you are interested in and set its hash, e.g.
|
# Pick a release version you are interested in and set its hash, e.g.
|
||||||
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-23.05/nixos-mailserver-nixos-23.05.tar.gz";
|
url = "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/nixos-25.11/nixos-mailserver-nixos-25.11.tar.gz";
|
||||||
# To get the sha256 of the nixos-mailserver tarball, we can use the nix-prefetch-url command:
|
# 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
|
# release="nixos-25.11"; nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/${release}/nixos-mailserver-${release}.tar.gz" --unpack
|
||||||
sha256 = "0000000000000000000000000000000000000000000000000000";
|
sha256 = "0000000000000000000000000000000000000000000000000000";
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
mailserver = {
|
mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
stateVersion = 3;
|
||||||
fqdn = "mail.example.com";
|
fqdn = "mail.example.com";
|
||||||
domains = [ "example.com" ];
|
domains = [ "example.com" ];
|
||||||
|
|
||||||
@@ -98,8 +104,11 @@ 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:
|
||||||
to ``mail.example.com``.
|
|
||||||
|
- Add an entry resolving IPv4 address ``1.2.3.4`` to ``mail.example.com``.
|
||||||
|
- Add an entry resolving IPv6 ``2001::1`` to ``mail.example.com``. Again, this
|
||||||
|
must be skipped if your server does not have an IPv6 address.
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
@@ -115,6 +124,9 @@ You can check this with
|
|||||||
$ nix-shell -p bind --command "host 1.2.3.4"
|
$ nix-shell -p bind --command "host 1.2.3.4"
|
||||||
4.3.2.1.in-addr.arpa domain name pointer mail.example.com.
|
4.3.2.1.in-addr.arpa domain name pointer mail.example.com.
|
||||||
|
|
||||||
|
$ nix-shell -p bind --command "host 2001::1"
|
||||||
|
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.2.ip6.arpa domain name pointer mail.example.com.
|
||||||
|
|
||||||
Note that it can take a while until a DNS entry is propagated.
|
Note that it can take a while until a DNS entry is propagated.
|
||||||
|
|
||||||
Set a ``MX`` record
|
Set a ``MX`` record
|
||||||
@@ -162,25 +174,26 @@ Note that it can take a while until a DNS entry is propagated.
|
|||||||
Set ``DKIM`` signature
|
Set ``DKIM`` signature
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
On your server, the ``opendkim`` systemd service generated a file
|
On your server, the ``rspamd`` systemd service generated a file
|
||||||
containing your DKIM public key in the file
|
containing your DKIM public key in the file
|
||||||
``/var/dkim/example.com.mail.txt``. The content of this file looks
|
``/var/dkim/example.com.mail.txt``. The content of this file looks
|
||||||
like
|
like
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
mail._domainkey IN TXT "v=DKIM1; k=rsa; s=email; p=<really-long-key>" ; ----- DKIM mail for domain.tld
|
mail._domainkey IN TXT ( "v=DKIM1; k=rsa; "
|
||||||
|
"p=<really-long-key>" ) ; ----- DKIM key mail for nixos.org
|
||||||
|
|
||||||
where ``really-long-key`` is your public key.
|
where ``really-long-key`` is your public key.
|
||||||
|
|
||||||
Based on the content of this file, we can add a ``DKIM`` record to the
|
Based on the content of this file, we can add a ``DKIM`` record to the
|
||||||
domain ``example.com``.
|
domain ``example.com``.
|
||||||
|
|
||||||
=========================== ===== ==== ==============================
|
=========================== ===== ==== ================================================
|
||||||
Name (Subdomain) TTL Type Value
|
Name (Subdomain) TTL Type Value
|
||||||
=========================== ===== ==== ==============================
|
=========================== ===== ==== ================================================
|
||||||
mail._domainkey.example.com 10800 TXT ``v=DKIM1; p=<really-long-key>``
|
mail._domainkey.example.com 10800 TXT ``v=DKIM1; k=rsa; s=email; p=<really-long-key>``
|
||||||
=========================== ===== ==== ==============================
|
=========================== ===== ==== ================================================
|
||||||
|
|
||||||
You can check this with
|
You can check this with
|
||||||
|
|
||||||
@@ -225,3 +238,8 @@ Besides that, you can send an email to
|
|||||||
score, and let `mxtoolbox.com <http://mxtoolbox.com/>`__ take a look at
|
score, and let `mxtoolbox.com <http://mxtoolbox.com/>`__ take a look at
|
||||||
your setup, but if you followed the steps closely then everything should
|
your setup, but if you followed the steps closely then everything should
|
||||||
be awesome!
|
be awesome!
|
||||||
|
|
||||||
|
Next steps (optional)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Take a look through our `Advanced Configurations <advanced-configurations.html>`_.
|
||||||
|
|||||||
102
docs/srs.rst
Normal file
102
docs/srs.rst
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
Sender Rewriting Scheme
|
||||||
|
=======================
|
||||||
|
|
||||||
|
The Sender Rewriting Scheme (SRS) allows mail servers to forward emails without
|
||||||
|
breaking SPF checks. By rewriting the envelope sender to an address within the
|
||||||
|
forwarder’s domain, SRS ensures that forwarded messages pass SPF validation,
|
||||||
|
preventing them from being rejected as spoofed or unauthorized.
|
||||||
|
|
||||||
|
How SRS works in practice
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
1. ``alice@foo.example`` receives an E-Mail from ``bob@bar.example``. Both the
|
||||||
|
envelope sender as well as the ``From`` header show ``bob@bar.example``. This
|
||||||
|
results in strict SPF alignment, because ``bar.example`` is the domain used in
|
||||||
|
both the ``Return-Path`` and ``FROM`` headers.
|
||||||
|
|
||||||
|
2. ``alice@foo.example`` forwards the mail to ``charlie@moo.example`` and
|
||||||
|
uses SRS to rewrite the envelope sender to originate from the local SRS domain
|
||||||
|
(e.g. `SRS0=HHH=TT=bar.example=alice@foo.example`). The ``FROM`` header remains
|
||||||
|
unchanged. This ensures that the forwarded mail succeeds SPF checks.
|
||||||
|
|
||||||
|
3. The email reaches ``charlie@moo.example``. SPF passes because the sender
|
||||||
|
domain in the envelope has been rewritten. The mismatch between envelope sender
|
||||||
|
domain and ``FROM`` domain does however break strict SPF alignment.
|
||||||
|
|
||||||
|
Enabling SRS
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
In a simple setup just enabling SRS will use your ``mailserver.systemDomain``
|
||||||
|
when rewriting the envelope sender domain.
|
||||||
|
|
||||||
|
.. code:: nix
|
||||||
|
|
||||||
|
{
|
||||||
|
mailserver = {
|
||||||
|
srs = {
|
||||||
|
enable = true;
|
||||||
|
#domain = "srs.example.com";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
..
|
||||||
|
|
||||||
|
While you can reuse an existing email domain for SRS, it is recommended to
|
||||||
|
configure a dedicated SRS domain. This is particularly important under the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
* Multiple unrelated mail domains are hosted on the mailserver
|
||||||
|
* The mail domain requires strict SPF alignment in its DMARC policy
|
||||||
|
|
||||||
|
Required DNS changes
|
||||||
|
~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
In the following example we assume that you want to set up a dedicated SRS
|
||||||
|
domain. If that is not the case you already have SPF and DKIM set up for the
|
||||||
|
system domain. If you have a DMARC record on the system domain, make sure it
|
||||||
|
uses a relaxed SPF alignment policy (``aspf=r``).
|
||||||
|
|
||||||
|
First we set up an MX record. This is so that we can receive and route bounces
|
||||||
|
that can result from forwards.
|
||||||
|
|
||||||
|
======================== ===== ==== ======== =====================
|
||||||
|
Name (Subdomain) TTL Type Priority Value
|
||||||
|
======================== ===== ==== ======== =====================
|
||||||
|
srs.example.com 10800 MX 10 ``mail.example.com``
|
||||||
|
======================== ===== ==== ==============================
|
||||||
|
|
||||||
|
Next up is the SPF record on the SRS domain to allow SPF authentication.
|
||||||
|
|
||||||
|
======================== ===== ==== ===================
|
||||||
|
Name (Subdomain) TTL Type Value
|
||||||
|
======================== ===== ==== ===================
|
||||||
|
srs.example.com 10800 TXT ``v=spf1 mx -all``
|
||||||
|
======================== ===== ==== ===================
|
||||||
|
|
||||||
|
Then we deploy the DKIM record with the `p=<value>` taken from
|
||||||
|
``/var/dkim/srs.example.com.mail.txt``, that appears after deploying with SRS
|
||||||
|
enabled.
|
||||||
|
|
||||||
|
=============================== ===== ==== ========================================
|
||||||
|
Name (Subdomain) TTL Type Value
|
||||||
|
=============================== ===== ==== ========================================
|
||||||
|
mail._domainkey.srs.example.com 10800 TXT ``v=DKIM1; k=rsa; p=<really-long-key>``
|
||||||
|
=============================== ===== ==== ========================================
|
||||||
|
|
||||||
|
Finally we can tie this together in the DMARC record to require receivers to
|
||||||
|
verify the requested SPF/DKIM alignment.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The SRS domain can only support relaxed SPF alignment due to the envelope
|
||||||
|
sender and ``FROM`` header mismatch.
|
||||||
|
|
||||||
|
======================== ===== ==== =========================================
|
||||||
|
Name (Subdomain) TTL Type Value
|
||||||
|
======================== ===== ==== =========================================
|
||||||
|
_dmarc.srs.example.com 10800 TXT ``v=DMARC1; p=reject; aspf=r; adkim=s;``
|
||||||
|
======================== ===== ==== =========================================
|
||||||
|
|
||||||
|
We can safely configure a ``reject`` policy on the SRS domain, to enforce the
|
||||||
|
SPF and DKIM alignment as configured above.
|
||||||
109
flake.lock
generated
109
flake.lock
generated
@@ -19,11 +19,11 @@
|
|||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1668681692,
|
"lastModified": 1761588595,
|
||||||
"narHash": "sha256-Ht91NGdewz8IQLtWZ9LCeNXMSXHUss+9COoqu6JLmXU=",
|
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||||
"owner": "edolstra",
|
"owner": "edolstra",
|
||||||
"repo": "flake-compat",
|
"repo": "flake-compat",
|
||||||
"rev": "009399224d5e398d03b22badca40a37ac85412a1",
|
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -32,74 +32,73 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"git-hooks": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": [
|
||||||
|
"flake-compat"
|
||||||
|
],
|
||||||
|
"gitignore": "gitignore",
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1763319842,
|
||||||
|
"narHash": "sha256-YG19IyrTdnVn0l3DvcUYm85u3PaqBt6tI6VvolcuHnA=",
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"rev": "7275fa67fbbb75891c16d9dee7d88e58aea2d761",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "cachix",
|
||||||
|
"repo": "git-hooks.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"gitignore": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"git-hooks",
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1709087332,
|
||||||
|
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "hercules-ci",
|
||||||
|
"repo": "gitignore.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1670751203,
|
"lastModified": 1764020296,
|
||||||
"narHash": "sha256-XdoH1v3shKDGlrwjgrNX/EN8s3c+kQV7xY6cLCE8vcI=",
|
"narHash": "sha256-6zddwDs2n+n01l+1TG6PlyokDdXzu/oBmEejcH5L5+A=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "64e0bf055f9d25928c31fb12924e59ff8ce71e60",
|
"rev": "a320ce8e6e2cc6b4397eef214d202a50a4583829",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"id": "nixpkgs",
|
|
||||||
"ref": "nixos-unstable",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-22_11": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1669558522,
|
|
||||||
"narHash": "sha256-yqxn+wOiPqe6cxzOo4leeJOp1bXE/fjPEi/3F/bBHv8=",
|
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-25.11-small",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "ce5fe99df1f15a09a91a86be9738d68fadfbad82",
|
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "nixpkgs",
|
|
||||||
"ref": "nixos-22.11",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs-23_05": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1684782344,
|
|
||||||
"narHash": "sha256-SHN8hPYYSX0thDrMLMWPWYulK3YFgASOrCsIL3AJ78g=",
|
|
||||||
"owner": "NixOS",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "8966c43feba2c701ed624302b6a935f97bcbdf88",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"id": "nixpkgs",
|
|
||||||
"ref": "nixos-23.05",
|
|
||||||
"type": "indirect"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"blobs": "blobs",
|
"blobs": "blobs",
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
"nixpkgs": "nixpkgs",
|
"git-hooks": "git-hooks",
|
||||||
"nixpkgs-22_11": "nixpkgs-22_11",
|
"nixpkgs": "nixpkgs"
|
||||||
"nixpkgs-23_05": "nixpkgs-23_05",
|
|
||||||
"utils": "utils"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"utils": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1605370193,
|
|
||||||
"narHash": "sha256-YyMTf3URDL/otKdKgtoMChu4vfVL3vCMkRqpGifhUn0=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"rev": "5021eac20303a61fafe17224c087f5519baed54d",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "flake-utils",
|
|
||||||
"type": "github"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
166
flake.nix
166
flake.nix
@@ -3,60 +3,80 @@
|
|||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
flake-compat = {
|
flake-compat = {
|
||||||
|
# for shell.nix compat
|
||||||
url = "github:edolstra/flake-compat";
|
url = "github:edolstra/flake-compat";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
utils.url = "github:numtide/flake-utils";
|
git-hooks = {
|
||||||
nixpkgs.url = "flake:nixpkgs/nixos-unstable";
|
url = "github:cachix/git-hooks.nix";
|
||||||
nixpkgs-22_11.url = "flake:nixpkgs/nixos-22.11";
|
inputs.flake-compat.follows = "flake-compat";
|
||||||
nixpkgs-23_05.url = "flake:nixpkgs/nixos-23.05";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small";
|
||||||
blobs = {
|
blobs = {
|
||||||
url = "gitlab:simple-nixos-mailserver/blobs";
|
url = "gitlab:simple-nixos-mailserver/blobs";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, utils, blobs, nixpkgs, nixpkgs-22_11, nixpkgs-23_05, ... }: let
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
blobs,
|
||||||
|
git-hooks,
|
||||||
|
nixpkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
lib = nixpkgs.lib;
|
lib = nixpkgs.lib;
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
releases = [
|
releases = [
|
||||||
{
|
{
|
||||||
name = "unstable";
|
name = "nixos-25.11";
|
||||||
|
nixpkgs = nixpkgs;
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
}
|
}
|
||||||
{
|
|
||||||
name = "23.05";
|
|
||||||
pkgs = nixpkgs-23_05.legacyPackages.${system};
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
testNames = [
|
testNames = [
|
||||||
"internal"
|
|
||||||
"external"
|
|
||||||
"clamav"
|
"clamav"
|
||||||
"multiple"
|
"external"
|
||||||
|
"internal"
|
||||||
"ldap"
|
"ldap"
|
||||||
|
"multiple"
|
||||||
];
|
];
|
||||||
genTest = testName: release: {
|
|
||||||
"name"= "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
|
genTest =
|
||||||
"value"= import (./tests/. + "/${testName}.nix") {
|
testName: release:
|
||||||
|
let
|
||||||
pkgs = release.pkgs;
|
pkgs = release.pkgs;
|
||||||
inherit blobs;
|
nixos-lib = import (release.nixpkgs + "/nixos/lib") {
|
||||||
|
inherit (pkgs) lib;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
name = "${testName}-${builtins.replaceStrings [ "." ] [ "_" ] release.name}";
|
||||||
|
value = nixos-lib.runTest {
|
||||||
|
hostPkgs = pkgs;
|
||||||
|
imports = [ ./tests/${testName}.nix ];
|
||||||
|
_module.args = { inherit blobs; };
|
||||||
|
extraBaseModules.imports = [ ./default.nix ];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# Generate an attribute set such as
|
# Generate an attribute set such as
|
||||||
# {
|
# {
|
||||||
# external-unstable = <derivation>;
|
# external-unstable = <derivation>;
|
||||||
# external-21_05 = <derivation>;
|
# external-21_05 = <derivation>;
|
||||||
# ...
|
# ...
|
||||||
# }
|
# }
|
||||||
allTests = lib.listToAttrs (
|
allTests = lib.listToAttrs (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 MarkDown file describing the options of the NixOS mailserver module
|
# Generate a MarkDown file describing the options of the NixOS mailserver module
|
||||||
optionsDoc = let
|
optionsDoc =
|
||||||
|
let
|
||||||
eval = lib.evalModules {
|
eval = lib.evalModules {
|
||||||
modules = [
|
modules = [
|
||||||
mailserverModule
|
mailserverModule
|
||||||
@@ -64,35 +84,46 @@
|
|||||||
_module.check = false;
|
_module.check = false;
|
||||||
mailserver = {
|
mailserver = {
|
||||||
fqdn = "mx.example.com";
|
fqdn = "mx.example.com";
|
||||||
|
systemDomain = "example.com";
|
||||||
domains = [
|
domains = [
|
||||||
"example.com"
|
"example.com"
|
||||||
];
|
];
|
||||||
dmarcReporting = {
|
|
||||||
organizationName = "Example Corp";
|
|
||||||
domain = "example.com";
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
options = builtins.toFile "options.json" (builtins.toJSON
|
options = builtins.toFile "options.json" (
|
||||||
(lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver")
|
builtins.toJSON (
|
||||||
(lib.optionAttrSetToDocList eval.options)));
|
lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") (
|
||||||
in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } ''
|
lib.optionAttrSetToDocList eval.options
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
in
|
||||||
|
pkgs.runCommand "options.md" { buildInputs = [ pkgs.python3Minimal ]; } ''
|
||||||
echo "Generating options.md from ${options}"
|
echo "Generating options.md from ${options}"
|
||||||
python ${./scripts/generate-options.py} ${options} > $out
|
python ${./scripts/generate-options.py} ${options} > $out
|
||||||
|
echo $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
documentation = pkgs.stdenv.mkDerivation {
|
documentation = pkgs.stdenv.mkDerivation {
|
||||||
name = "documentation";
|
name = "documentation";
|
||||||
src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"];
|
src = lib.sourceByRegex ./docs [
|
||||||
buildInputs = [(
|
"logo\\.png"
|
||||||
pkgs.python3.withPackages (p: with p; [
|
"conf\\.py"
|
||||||
|
"Makefile"
|
||||||
|
".*\\.rst"
|
||||||
|
];
|
||||||
|
buildInputs = [
|
||||||
|
(pkgs.python3.withPackages (
|
||||||
|
p: with p; [
|
||||||
sphinx
|
sphinx
|
||||||
sphinx_rtd_theme
|
sphinx-rtd-theme
|
||||||
myst-parser
|
myst-parser
|
||||||
])
|
linkify-it-py
|
||||||
)];
|
]
|
||||||
|
))
|
||||||
|
];
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
cp ${optionsDoc} options.md
|
cp ${optionsDoc} options.md
|
||||||
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
|
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
|
||||||
@@ -104,7 +135,8 @@
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
nixosModules = rec {
|
nixosModules = rec {
|
||||||
mailserver = mailserverModule;
|
mailserver = mailserverModule;
|
||||||
default = mailserver;
|
default = mailserver;
|
||||||
@@ -112,17 +144,71 @@
|
|||||||
nixosModule = self.nixosModules.default; # compatibility
|
nixosModule = self.nixosModules.default; # compatibility
|
||||||
hydraJobs.${system} = allTests // {
|
hydraJobs.${system} = allTests // {
|
||||||
inherit documentation;
|
inherit documentation;
|
||||||
|
inherit (self.checks.${system}) pre-commit;
|
||||||
|
};
|
||||||
|
checks.${system} = allTests // {
|
||||||
|
pre-commit = git-hooks.lib.${system}.run {
|
||||||
|
src = ./.;
|
||||||
|
hooks = {
|
||||||
|
# docs
|
||||||
|
markdownlint = {
|
||||||
|
enable = true;
|
||||||
|
settings.configuration = {
|
||||||
|
# Max line length, doesn't seem to correclty account for lines containing links
|
||||||
|
# https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md
|
||||||
|
MD013 = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
rstcheck = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.rstcheckWithSphinx;
|
||||||
|
entry = lib.getExe pkgs.rstcheckWithSphinx;
|
||||||
|
files = "\\.rst$";
|
||||||
|
};
|
||||||
|
|
||||||
|
# nix
|
||||||
|
deadnix.enable = true;
|
||||||
|
nixfmt-rfc-style.enable = true;
|
||||||
|
|
||||||
|
# python
|
||||||
|
pyright.enable = true;
|
||||||
|
ruff = {
|
||||||
|
enable = true;
|
||||||
|
args = [
|
||||||
|
"--extend-select"
|
||||||
|
"I"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
ruff-format.enable = true;
|
||||||
|
|
||||||
|
# scripts
|
||||||
|
shellcheck.enable = true;
|
||||||
|
|
||||||
|
# sieve
|
||||||
|
check-sieve = {
|
||||||
|
enable = true;
|
||||||
|
package = pkgs.check-sieve;
|
||||||
|
entry = lib.getExe pkgs.check-sieve;
|
||||||
|
files = "\\.sieve$";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
checks.${system} = allTests;
|
|
||||||
packages.${system} = {
|
packages.${system} = {
|
||||||
inherit optionsDoc documentation;
|
inherit optionsDoc documentation;
|
||||||
};
|
};
|
||||||
devShells.${system}.default = pkgs.mkShell {
|
devShells.${system}.default = pkgs.mkShellNoCC {
|
||||||
inputsFrom = [ documentation ];
|
inputsFrom = [ documentation ];
|
||||||
packages = with pkgs; [
|
packages =
|
||||||
clamav
|
with pkgs;
|
||||||
];
|
[
|
||||||
|
glab
|
||||||
|
]
|
||||||
|
++ self.checks.${system}.pre-commit.enabledPackages;
|
||||||
|
shellHook = self.checks.${system}.pre-commit.shellHook;
|
||||||
};
|
};
|
||||||
devShell.${system} = self.devShells.${system}.default; # compatibility
|
devShell.${system} = self.devShells.${system}.default; # compatibility
|
||||||
|
|
||||||
|
formatter.${system} = pkgs.nixfmt-tree;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,45 @@
|
|||||||
{ config, lib, pkgs, ... }:
|
|
||||||
{
|
{
|
||||||
assertions = lib.optionals config.mailserver.ldap.enable [
|
config,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
mailserverRelease = "25.11";
|
||||||
|
nixpkgsRelease = lib.trivial.release;
|
||||||
|
releaseMismatch =
|
||||||
|
config.mailserver.enableNixpkgsReleaseCheck && mailserverRelease != nixpkgsRelease;
|
||||||
|
in
|
||||||
|
|
||||||
|
{
|
||||||
|
warnings = lib.optional releaseMismatch ''
|
||||||
|
You are using
|
||||||
|
|
||||||
|
NixOS Mailserver version ${mailserverRelease} and
|
||||||
|
Nixpkgs version ${nixpkgsRelease}.
|
||||||
|
|
||||||
|
Using mismatched versions is likely to cause compatibility issues
|
||||||
|
and may require migrations that make an eventual rollback tricky.
|
||||||
|
|
||||||
|
It is therefore highly recommended to use a release of
|
||||||
|
NixOS mailserver that corresponds with your chosen release of Nixpkgs.
|
||||||
|
|
||||||
|
If you insist then you can disable this warning by adding
|
||||||
|
|
||||||
|
mailserver.enableNixpkgsReleaseCheck = false;
|
||||||
|
|
||||||
|
to your configuration.
|
||||||
|
'';
|
||||||
|
|
||||||
|
# We guard all assertions by requiring mailserver to be actually enabled
|
||||||
|
assertions = lib.optionals config.mailserver.enable (
|
||||||
|
[
|
||||||
|
{
|
||||||
|
assertion = config.mailserver.stateVersion != null;
|
||||||
|
message = "The `mailserver.stateVersion` option is not set. Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html to determine the proper value to initialize it at.";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
++ lib.optionals config.mailserver.ldap.enable [
|
||||||
{
|
{
|
||||||
assertion = config.mailserver.loginAccounts == { };
|
assertion = config.mailserver.loginAccounts == { };
|
||||||
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts";
|
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts";
|
||||||
@@ -9,9 +48,38 @@
|
|||||||
assertion = config.mailserver.extraVirtualAliases == { };
|
assertion = config.mailserver.extraVirtualAliases == { };
|
||||||
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases";
|
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.extraVirtualAliases";
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
++
|
||||||
|
lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail")
|
||||||
|
[
|
||||||
{
|
{
|
||||||
assertion = config.mailserver.forwards == {};
|
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2;
|
||||||
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.forwards";
|
message = ''
|
||||||
|
Issue: The dovecot homedir for LDAP users was previously not respecting `mailserver.mailDirectory`.
|
||||||
|
Remediation:
|
||||||
|
- Stop the `dovecot2.service`
|
||||||
|
- Move `/var/vmail/ldap` below your `mailserver.mailDirectory`
|
||||||
|
- Increase the `stateVersion` to 2.
|
||||||
|
|
||||||
|
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-ldap-home-directory-migration for more information.
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
];
|
]
|
||||||
|
++ [
|
||||||
|
{
|
||||||
|
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 3;
|
||||||
|
message = ''
|
||||||
|
Issue: The dovecot mail location for all users has changed and need to be migrated.
|
||||||
|
|
||||||
|
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration for the required remediation steps.
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
]
|
||||||
|
++ lib.optionals (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";
|
||||||
|
}
|
||||||
|
]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,28 +14,44 @@
|
|||||||
# 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,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver.borgbackup;
|
cfg = config.mailserver.borgbackup;
|
||||||
|
|
||||||
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
|
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
|
||||||
autoFragment =
|
autoFragment =
|
||||||
if cfg.compression.auto && cfg.compression.method == null
|
if cfg.compression.auto && cfg.compression.method == null then
|
||||||
then throw "compression.method must be set when using auto."
|
throw "compression.method must be set when using auto."
|
||||||
else lib.optional cfg.compression.auto "auto";
|
else
|
||||||
|
lib.optional cfg.compression.auto "auto";
|
||||||
levelFragment =
|
levelFragment =
|
||||||
if cfg.compression.level != null && cfg.compression.method == null
|
if cfg.compression.level != null && cfg.compression.method == null then
|
||||||
then throw "compression.method must be set when using compression.level."
|
throw "compression.method must be set when using compression.level."
|
||||||
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
|
else
|
||||||
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
|
lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
|
||||||
|
compressionFragment = lib.concatStringsSep "," (
|
||||||
|
lib.flatten [
|
||||||
|
autoFragment
|
||||||
|
methodFragment
|
||||||
|
levelFragment
|
||||||
|
]
|
||||||
|
);
|
||||||
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
|
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
|
||||||
|
|
||||||
encryptionFragment = cfg.encryption.method;
|
encryptionFragment = cfg.encryption.method;
|
||||||
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
|
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
|
||||||
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
|
passphraseFragment = lib.optionalString (cfg.encryption.method != "none") (
|
||||||
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
|
if cfg.encryption.passphraseFile != null then
|
||||||
else throw "passphraseFile must be set when using encryption.");
|
''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
|
||||||
|
else
|
||||||
|
throw "passphraseFile must be set when using encryption."
|
||||||
|
);
|
||||||
|
|
||||||
locations = lib.escapeShellArgs cfg.locations;
|
locations = lib.escapeShellArgs cfg.locations;
|
||||||
name = lib.escapeShellArg cfg.name;
|
name = lib.escapeShellArg cfg.name;
|
||||||
@@ -55,7 +71,8 @@ let
|
|||||||
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
|
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
|
||||||
${cmdPostexec}
|
${cmdPostexec}
|
||||||
'';
|
'';
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
|
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
borgbackup
|
borgbackup
|
||||||
|
|||||||
@@ -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, options, ... }:
|
{ config, lib, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|||||||
@@ -14,43 +14,63 @@
|
|||||||
# 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,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
# cert :: PATH
|
# cert :: PATH
|
||||||
certificatePath = if cfg.certificateScheme == "manual"
|
certificatePath =
|
||||||
then cfg.certificateFile
|
if cfg.certificateScheme == "manual" then
|
||||||
else if cfg.certificateScheme == "selfsigned"
|
cfg.certificateFile
|
||||||
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
else if cfg.certificateScheme == "selfsigned" then
|
||||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
|
"${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
||||||
then "${config.security.acme.certs.${cfg.fqdn}.directory}/fullchain.pem"
|
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
|
||||||
else throw "unknown certificate scheme";
|
"${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
|
||||||
|
else
|
||||||
|
throw "unknown certificate scheme";
|
||||||
|
|
||||||
# key :: PATH
|
# key :: PATH
|
||||||
keyPath = if cfg.certificateScheme == "manual"
|
keyPath =
|
||||||
then cfg.keyFile
|
if cfg.certificateScheme == "manual" then
|
||||||
else if cfg.certificateScheme == "selfsigned"
|
cfg.keyFile
|
||||||
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
else if cfg.certificateScheme == "selfsigned" then
|
||||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
|
"${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
||||||
then "${config.security.acme.certs.${cfg.fqdn}.directory}/key.pem"
|
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
|
||||||
else throw "unknown certificate scheme";
|
"${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
|
||||||
|
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;
|
||||||
in
|
in
|
||||||
lib.mapAttrs (name: value:
|
lib.mapAttrs (
|
||||||
|
name: value:
|
||||||
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
|
# Appends the LDAP bind password to files to avoid writing this
|
||||||
# password into the Nix store.
|
# password into the Nix store.
|
||||||
appendLdapBindPwd = {
|
appendLdapBindPwd =
|
||||||
name, file, prefix, passwordFile, destination
|
{
|
||||||
}: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
name,
|
||||||
|
file,
|
||||||
|
prefix,
|
||||||
|
suffix ? "",
|
||||||
|
passwordFile,
|
||||||
|
destination,
|
||||||
|
}:
|
||||||
|
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -61,8 +81,9 @@ in
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
cat ${file} > ${destination}
|
cat ${file} > ${destination}
|
||||||
echo -n "${prefix}" >> ${destination}
|
echo -n '${prefix}' >> ${destination}
|
||||||
cat ${passwordFile} >> ${destination}
|
cat ${passwordFile} | tr -d '\n' >> ${destination}
|
||||||
|
echo -n '${suffix}' >> ${destination}
|
||||||
chmod 600 ${destination}
|
chmod 600 ${destination}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
{ config, lib, ... }:
|
|
||||||
{
|
|
||||||
mailserver.policydSPFExtraConfig = lib.mkIf config.mailserver.debug "debugLevel = 4";
|
|
||||||
}
|
|
||||||
@@ -14,9 +14,22 @@
|
|||||||
# 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,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
with (import ./common.nix { inherit config pkgs lib; });
|
with (import ./common.nix {
|
||||||
|
inherit
|
||||||
|
config
|
||||||
|
options
|
||||||
|
pkgs
|
||||||
|
lib
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
@@ -26,40 +39,27 @@ let
|
|||||||
userdbFile = "${passwdDir}/userdb";
|
userdbFile = "${passwdDir}/userdb";
|
||||||
# This file contains the ldap bind password
|
# This file contains the ldap bind password
|
||||||
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
|
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
|
||||||
bool2int = x: if x then "1" else "0";
|
boolToYesNo = x: if x then "yes" else "no";
|
||||||
|
listToLine = lib.concatStringsSep " ";
|
||||||
|
listToMultiAttrs =
|
||||||
|
keyPrefix: attrs:
|
||||||
|
lib.listToAttrs (
|
||||||
|
lib.imap1 (n: x: {
|
||||||
|
name = "${keyPrefix}${if n == 1 then "" else toString n}";
|
||||||
|
value = x;
|
||||||
|
}) attrs
|
||||||
|
);
|
||||||
|
|
||||||
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
||||||
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
|
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
|
||||||
|
|
||||||
# maildir in format "/${domain}/${user}"
|
# https://doc.dovecot.org/2.3/configuration_manual/home_directories_for_virtual_users/#ways-to-set-up-home-directory
|
||||||
|
# Mail directory below the home directory
|
||||||
dovecotMaildir =
|
dovecotMaildir =
|
||||||
"maildir:${cfg.mailDirectory}/%d/%n${maildirLayoutAppendix}${maildirUTF8FolderNames}"
|
"maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}"
|
||||||
+ (lib.optionalString (cfg.indexDir != null)
|
+ (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}");
|
||||||
":INDEX=${cfg.indexDir}/%d/%n"
|
|
||||||
);
|
|
||||||
|
|
||||||
postfixCfg = config.services.postfix;
|
postfixCfg = config.services.postfix;
|
||||||
dovecot2Cfg = config.services.dovecot2;
|
|
||||||
|
|
||||||
stateDir = "/var/lib/dovecot";
|
|
||||||
|
|
||||||
pipeBin = pkgs.stdenv.mkDerivation {
|
|
||||||
name = "pipe_bin";
|
|
||||||
src = ./dovecot/pipe_bin;
|
|
||||||
buildInputs = with pkgs; [ makeWrapper coreutils bash rspamd ];
|
|
||||||
buildCommand = ''
|
|
||||||
mkdir -p $out/pipe/bin
|
|
||||||
cp $src/* $out/pipe/bin/
|
|
||||||
chmod a+x $out/pipe/bin/*
|
|
||||||
patchShebangs $out/pipe/bin
|
|
||||||
|
|
||||||
for file in $out/pipe/bin/*; do
|
|
||||||
wrapProgram $file \
|
|
||||||
--set PATH "${pkgs.coreutils}/bin:${pkgs.rspamd}/bin"
|
|
||||||
done
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
ldapConfig = pkgs.writeTextFile {
|
ldapConfig = pkgs.writeTextFile {
|
||||||
name = "dovecot-ldap.conf.ext.template";
|
name = "dovecot-ldap.conf.ext.template";
|
||||||
@@ -76,7 +76,7 @@ let
|
|||||||
auth_bind = yes
|
auth_bind = yes
|
||||||
base = ${cfg.ldap.searchBase}
|
base = ${cfg.ldap.searchBase}
|
||||||
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
|
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
|
||||||
${lib.optionalString (cfg.ldap.dovecot.userAttrs != "") ''
|
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
|
||||||
user_attrs = ${cfg.ldap.dovecot.userAttrs}
|
user_attrs = ${cfg.ldap.dovecot.userAttrs}
|
||||||
''}
|
''}
|
||||||
user_filter = ${cfg.ldap.dovecot.userFilter}
|
user_filter = ${cfg.ldap.dovecot.userFilter}
|
||||||
@@ -90,7 +90,8 @@ let
|
|||||||
setPwdInLdapConfFile = appendLdapBindPwd {
|
setPwdInLdapConfFile = appendLdapBindPwd {
|
||||||
name = "ldap-conf-file";
|
name = "ldap-conf-file";
|
||||||
file = ldapConfig;
|
file = ldapConfig;
|
||||||
prefix = "dnpass = ";
|
prefix = ''dnpass = "'';
|
||||||
|
suffix = ''"'';
|
||||||
passwordFile = cfg.ldap.bind.passwordFile;
|
passwordFile = cfg.ldap.bind.passwordFile;
|
||||||
destination = ldapConfFile;
|
destination = ldapConfFile;
|
||||||
};
|
};
|
||||||
@@ -108,7 +109,9 @@ let
|
|||||||
# Prevent world-readable password files, even temporarily.
|
# Prevent world-readable password files, even temporarily.
|
||||||
umask 077
|
umask 077
|
||||||
|
|
||||||
for f in ${builtins.toString (lib.mapAttrsToList (name: value: passwordFiles."${name}") cfg.loginAccounts)}; do
|
for f in ${
|
||||||
|
builtins.toString (lib.mapAttrsToList (name: _: 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!"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -116,35 +119,61 @@ let
|
|||||||
done
|
done
|
||||||
|
|
||||||
cat <<EOF > ${passwdFile}
|
cat <<EOF > ${passwdFile}
|
||||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
${lib.concatStringsSep "\n" (
|
||||||
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
lib.mapAttrsToList (
|
||||||
) cfg.loginAccounts)}
|
name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
||||||
|
) cfg.loginAccounts
|
||||||
|
)}
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat <<EOF > ${userdbFile}
|
cat <<EOF > ${userdbFile}
|
||||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
${lib.concatStringsSep "\n" (
|
||||||
|
lib.mapAttrsToList (
|
||||||
|
name: value:
|
||||||
"${name}:::::::"
|
"${name}:::::::"
|
||||||
+ (if lib.isString value.quota
|
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
|
||||||
then "userdb_quota_rule=*:storage=${value.quota}"
|
) cfg.loginAccounts
|
||||||
else "")
|
)}
|
||||||
) cfg.loginAccounts)}
|
|
||||||
EOF
|
EOF
|
||||||
'';
|
'';
|
||||||
|
|
||||||
junkMailboxes = builtins.attrNames (lib.filterAttrs (n: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes);
|
junkMailboxes = builtins.attrNames (
|
||||||
|
lib.filterAttrs (_: v: v ? "specialUse" && v.specialUse == "Junk") cfg.mailboxes
|
||||||
|
);
|
||||||
junkMailboxNumber = builtins.length junkMailboxes;
|
junkMailboxNumber = builtins.length junkMailboxes;
|
||||||
# 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: (
|
mkLdapSearchScope =
|
||||||
if scope == "sub" then "subtree"
|
scope:
|
||||||
else if scope == "one" then "onelevel"
|
(
|
||||||
else scope
|
if scope == "sub" then
|
||||||
|
"subtree"
|
||||||
|
else if scope == "one" then
|
||||||
|
"onelevel"
|
||||||
|
else
|
||||||
|
scope
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ftsPluginSettings = {
|
||||||
|
fts = "flatcurve";
|
||||||
|
fts_languages = listToLine cfg.fullTextSearch.languages;
|
||||||
|
fts_tokenizers = listToLine [
|
||||||
|
"generic"
|
||||||
|
"email-address"
|
||||||
|
];
|
||||||
|
fts_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian
|
||||||
|
fts_flatcurve_substring_search = boolToYesNo cfg.fullTextSearch.substringSearch;
|
||||||
|
fts_filters = listToLine cfg.fullTextSearch.filters;
|
||||||
|
fts_header_excludes = listToLine cfg.fullTextSearch.headerExcludes;
|
||||||
|
fts_autoindex = boolToYesNo cfg.fullTextSearch.autoIndex;
|
||||||
|
fts_enforced = cfg.fullTextSearch.enforced;
|
||||||
|
}
|
||||||
|
// (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude);
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = lib.mkIf cfg.enable {
|
||||||
assertions = [
|
assertions = [
|
||||||
{
|
{
|
||||||
assertion = junkMailboxNumber == 1;
|
assertion = junkMailboxNumber == 1;
|
||||||
@@ -152,31 +181,64 @@ in
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
warnings =
|
||||||
|
lib.optional
|
||||||
|
(
|
||||||
|
(builtins.length cfg.fullTextSearch.languages > 1)
|
||||||
|
&& (builtins.elem "stopwords" cfg.fullTextSearch.filters)
|
||||||
|
)
|
||||||
|
''
|
||||||
|
Using stopwords in `mailserver.fullTextSearch.filters` with multiple
|
||||||
|
languages in `mailserver.fullTextSearch.languages` configured WILL
|
||||||
|
cause some searches to fail.
|
||||||
|
|
||||||
|
The recommended solution is to NOT use the stopword filter when
|
||||||
|
multiple languages are present in the configuration.
|
||||||
|
'';
|
||||||
|
|
||||||
# for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
|
# 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,
|
# the global config and tries to open shared libraries configured in there,
|
||||||
# which are usually not compatible.
|
# which are usually not compatible.
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
pkgs.dovecot_pigeonhole
|
pkgs.dovecot_pigeonhole
|
||||||
];
|
]
|
||||||
|
++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
|
||||||
|
|
||||||
|
# For compatibility with python imaplib
|
||||||
|
environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
|
||||||
|
|
||||||
services.dovecot2 = {
|
services.dovecot2 = {
|
||||||
enable = true;
|
enable = true;
|
||||||
enableImap = enableImap || enableImapSsl;
|
enableImap = cfg.enableImap || cfg.enableImapSsl;
|
||||||
enablePop3 = enablePop3 || enablePop3Ssl;
|
enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl;
|
||||||
enablePAM = false;
|
enablePAM = false;
|
||||||
enableQuota = true;
|
enableQuota = true;
|
||||||
mailGroup = vmailGroupName;
|
mailGroup = cfg.vmailGroupName;
|
||||||
mailUser = vmailUserName;
|
mailUser = cfg.vmailUserName;
|
||||||
mailLocation = dovecotMaildir;
|
mailLocation = dovecotMaildir;
|
||||||
sslServerCert = certificatePath;
|
sslServerCert = certificatePath;
|
||||||
sslServerKey = keyPath;
|
sslServerKey = keyPath;
|
||||||
|
enableDHE = lib.mkDefault false;
|
||||||
enableLmtp = true;
|
enableLmtp = true;
|
||||||
modules = [ pkgs.dovecot_pigeonhole ] ++ (lib.optional cfg.fullTextSearch.enable pkgs.dovecot_fts_xapian );
|
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [
|
||||||
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [ "fts" "fts_xapian" ];
|
"fts"
|
||||||
|
"fts_flatcurve"
|
||||||
|
];
|
||||||
protocols = lib.optional cfg.enableManageSieve "sieve";
|
protocols = lib.optional cfg.enableManageSieve "sieve";
|
||||||
|
|
||||||
sieveScripts = {
|
pluginSettings = {
|
||||||
after = builtins.toFile "spam.sieve" ''
|
sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve";
|
||||||
|
sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve";
|
||||||
|
sieve_default_name = "default";
|
||||||
|
}
|
||||||
|
// (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings);
|
||||||
|
|
||||||
|
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" {
|
||||||
@@ -184,13 +246,35 @@ in
|
|||||||
stop;
|
stop;
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
pipeBins = map lib.getExe [
|
||||||
|
(pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
|
||||||
|
(pkgs.writeShellScriptBin "rspamd-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 = ''
|
||||||
#Extra Config
|
#Extra Config
|
||||||
${lib.optionalString debug ''
|
${lib.optionalString cfg.debug.dovecot ''
|
||||||
mail_debug = yes
|
mail_debug = yes
|
||||||
auth_debug = yes
|
auth_debug = yes
|
||||||
verbose_ssl = yes
|
verbose_ssl = yes
|
||||||
@@ -199,42 +283,62 @@ in
|
|||||||
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
|
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
|
||||||
service imap-login {
|
service imap-login {
|
||||||
inet_listener imap {
|
inet_listener imap {
|
||||||
${if cfg.enableImap then ''
|
${
|
||||||
|
if cfg.enableImap then
|
||||||
|
''
|
||||||
port = 143
|
port = 143
|
||||||
'' else ''
|
''
|
||||||
|
else
|
||||||
|
''
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
||||||
port = 0
|
port = 0
|
||||||
''}
|
''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inet_listener imaps {
|
inet_listener imaps {
|
||||||
${if cfg.enableImapSsl then ''
|
${
|
||||||
|
if cfg.enableImapSsl then
|
||||||
|
''
|
||||||
port = 993
|
port = 993
|
||||||
ssl = yes
|
ssl = yes
|
||||||
'' else ''
|
''
|
||||||
|
else
|
||||||
|
''
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
||||||
port = 0
|
port = 0
|
||||||
''}
|
''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
''}
|
''}
|
||||||
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
|
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
|
||||||
service pop3-login {
|
service pop3-login {
|
||||||
inet_listener pop3 {
|
inet_listener pop3 {
|
||||||
${if cfg.enablePop3 then ''
|
${
|
||||||
|
if cfg.enablePop3 then
|
||||||
|
''
|
||||||
port = 110
|
port = 110
|
||||||
'' else ''
|
''
|
||||||
|
else
|
||||||
|
''
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
||||||
port = 0
|
port = 0
|
||||||
''}
|
''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
inet_listener pop3s {
|
inet_listener pop3s {
|
||||||
${if cfg.enablePop3Ssl then ''
|
${
|
||||||
|
if cfg.enablePop3Ssl then
|
||||||
|
''
|
||||||
port = 995
|
port = 995
|
||||||
ssl = yes
|
ssl = yes
|
||||||
'' else ''
|
''
|
||||||
|
else
|
||||||
|
''
|
||||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
||||||
port = 0
|
port = 0
|
||||||
''}
|
''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
''}
|
''}
|
||||||
@@ -244,14 +348,21 @@ in
|
|||||||
mail_plugins = $mail_plugins imap_sieve
|
mail_plugins = $mail_plugins imap_sieve
|
||||||
}
|
}
|
||||||
|
|
||||||
|
service imap {
|
||||||
|
vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB
|
||||||
|
}
|
||||||
|
|
||||||
protocol pop3 {
|
protocol pop3 {
|
||||||
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
|
||||||
}
|
}
|
||||||
|
|
||||||
mail_access_groups = ${vmailGroupName}
|
mail_access_groups = ${cfg.vmailGroupName}
|
||||||
|
|
||||||
|
# https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7
|
||||||
ssl = required
|
ssl = required
|
||||||
ssl_min_protocol = TLSv1.2
|
ssl_min_protocol = TLSv1
|
||||||
ssl_prefer_server_ciphers = yes
|
ssl_prefer_server_ciphers = no
|
||||||
|
ssl_curve_list = X25519MLKEM768:X25519:prime256v1:secp384r1
|
||||||
|
|
||||||
service lmtp {
|
service lmtp {
|
||||||
unix_listener dovecot-lmtp {
|
unix_listener dovecot-lmtp {
|
||||||
@@ -259,6 +370,17 @@ in
|
|||||||
mode = 0600
|
mode = 0600
|
||||||
user = ${postfixCfg.user}
|
user = ${postfixCfg.user}
|
||||||
}
|
}
|
||||||
|
vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB
|
||||||
|
}
|
||||||
|
|
||||||
|
service quota-status {
|
||||||
|
inet_listener {
|
||||||
|
port = 0
|
||||||
|
}
|
||||||
|
unix_listener quota-status {
|
||||||
|
user = postfix
|
||||||
|
}
|
||||||
|
vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB
|
||||||
}
|
}
|
||||||
|
|
||||||
recipient_delimiter = ${cfg.recipientDelimiter}
|
recipient_delimiter = ${cfg.recipientDelimiter}
|
||||||
@@ -276,7 +398,10 @@ in
|
|||||||
userdb {
|
userdb {
|
||||||
driver = passwd-file
|
driver = passwd-file
|
||||||
args = ${userdbFile}
|
args = ${userdbFile}
|
||||||
default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}
|
default_fields = \
|
||||||
|
home=${cfg.mailDirectory}/%{domain}/%{username} \
|
||||||
|
uid=${builtins.toString cfg.vmailUID} \
|
||||||
|
gid=${builtins.toString cfg.vmailUID}
|
||||||
}
|
}
|
||||||
|
|
||||||
${lib.optionalString cfg.ldap.enable ''
|
${lib.optionalString cfg.ldap.enable ''
|
||||||
@@ -288,7 +413,14 @@ in
|
|||||||
userdb {
|
userdb {
|
||||||
driver = ldap
|
driver = ldap
|
||||||
args = ${ldapConfFile}
|
args = ${ldapConfFile}
|
||||||
default_fields = home=/var/vmail/ldap/%u uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID}
|
default_fields = \
|
||||||
|
home=${cfg.mailDirectory}/ldap/%{user} \
|
||||||
|
uid=${toString cfg.vmailUID} \
|
||||||
|
gid=${toString cfg.vmailUID} \
|
||||||
|
mail=maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}${
|
||||||
|
lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/ldap/%{user}"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
''}
|
''}
|
||||||
|
|
||||||
@@ -307,90 +439,27 @@ in
|
|||||||
inbox = yes
|
inbox = yes
|
||||||
}
|
}
|
||||||
|
|
||||||
plugin {
|
|
||||||
sieve_plugins = sieve_imapsieve sieve_extprograms
|
|
||||||
sieve = file:${cfg.sieveDirectory}/%u/scripts;active=${cfg.sieveDirectory}/%u/active.sieve
|
|
||||||
sieve_default = file:${cfg.sieveDirectory}/%u/default.sieve
|
|
||||||
sieve_default_name = default
|
|
||||||
|
|
||||||
# From elsewhere to Spam folder
|
|
||||||
imapsieve_mailbox1_name = ${junkMailboxName}
|
|
||||||
imapsieve_mailbox1_causes = COPY,APPEND
|
|
||||||
imapsieve_mailbox1_before = file:${stateDir}/imap_sieve/report-spam.sieve
|
|
||||||
|
|
||||||
# From Spam folder to elsewhere
|
|
||||||
imapsieve_mailbox2_name = *
|
|
||||||
imapsieve_mailbox2_from = ${junkMailboxName}
|
|
||||||
imapsieve_mailbox2_causes = COPY
|
|
||||||
imapsieve_mailbox2_before = file:${stateDir}/imap_sieve/report-ham.sieve
|
|
||||||
|
|
||||||
sieve_pipe_bin_dir = ${pipeBin}/pipe/bin
|
|
||||||
|
|
||||||
sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.environment
|
|
||||||
}
|
|
||||||
|
|
||||||
${lib.optionalString cfg.fullTextSearch.enable ''
|
|
||||||
plugin {
|
|
||||||
plugin = fts fts_xapian
|
|
||||||
fts = xapian
|
|
||||||
fts_xapian = partial=${toString cfg.fullTextSearch.minSize} full=${toString cfg.fullTextSearch.maxSize} attachments=${bool2int cfg.fullTextSearch.indexAttachments} verbose=${bool2int cfg.debug}
|
|
||||||
|
|
||||||
fts_autoindex = ${if cfg.fullTextSearch.autoIndex then "yes" else "no"}
|
|
||||||
|
|
||||||
${lib.strings.concatImapStringsSep "\n" (n: x: "fts_autoindex_exclude${if n==1 then "" else toString n} = ${x}") cfg.fullTextSearch.autoIndexExclude}
|
|
||||||
|
|
||||||
fts_enforced = ${cfg.fullTextSearch.enforced}
|
|
||||||
}
|
|
||||||
|
|
||||||
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
|
|
||||||
service indexer-worker {
|
service indexer-worker {
|
||||||
|
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
|
||||||
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)}
|
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)}
|
||||||
|
''}
|
||||||
}
|
}
|
||||||
''}
|
|
||||||
''}
|
|
||||||
|
|
||||||
lda_mailbox_autosubscribe = yes
|
lda_mailbox_autosubscribe = yes
|
||||||
lda_mailbox_autocreate = yes
|
lda_mailbox_autocreate = yes
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.dovecot2 = {
|
systemd.services.dovecot = {
|
||||||
preStart = ''
|
preStart = ''
|
||||||
${genPasswdScript}
|
${genPasswdScript}
|
||||||
rm -rf '${stateDir}/imap_sieve'
|
''
|
||||||
mkdir '${stateDir}/imap_sieve'
|
+ (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
|
||||||
cp -p "${./dovecot/imap_sieve}"/*.sieve '${stateDir}/imap_sieve/'
|
|
||||||
for k in "${stateDir}/imap_sieve"/*.sieve ; do
|
|
||||||
${pkgs.dovecot_pigeonhole}/bin/sievec "$k"
|
|
||||||
done
|
|
||||||
chown -R '${dovecot2Cfg.mailUser}:${dovecot2Cfg.mailGroup}' '${stateDir}/imap_sieve'
|
|
||||||
'' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]);
|
systemd.services.postfix.restartTriggers = [
|
||||||
|
genPasswdScript
|
||||||
systemd.services.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable) {
|
]
|
||||||
description = "Optimize dovecot indices for fts_xapian";
|
++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]);
|
||||||
requisite = [ "dovecot2.service" ];
|
|
||||||
after = [ "dovecot2.service" ];
|
|
||||||
startAt = cfg.fullTextSearch.maintenance.onCalendar;
|
|
||||||
serviceConfig = {
|
|
||||||
Type = "oneshot";
|
|
||||||
ExecStart = "${pkgs.dovecot}/bin/doveadm fts optimize -A";
|
|
||||||
PrivateDevices = true;
|
|
||||||
PrivateNetwork = true;
|
|
||||||
ProtectKernelTunables = true;
|
|
||||||
ProtectKernelModules = true;
|
|
||||||
ProtectControlGroups = true;
|
|
||||||
ProtectHome = true;
|
|
||||||
ProtectSystem = true;
|
|
||||||
PrivateTmp = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
systemd.timers.dovecot-fts-xapian-optimize = lib.mkIf (cfg.fullTextSearch.enable && cfg.fullTextSearch.maintenance.enable && cfg.fullTextSearch.maintenance.randomizedDelaySec != 0) {
|
|
||||||
timerConfig = {
|
|
||||||
RandomizedDelaySec = cfg.fullTextSearch.maintenance.randomizedDelaySec;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,4 @@ if environment :matches "imap.user" "*" {
|
|||||||
set "username" "${1}";
|
set "username" "${1}";
|
||||||
}
|
}
|
||||||
|
|
||||||
pipe :copy "sa-learn-ham.sh" [ "${username}" ];
|
pipe :copy "rspamd-learn-ham.sh" [ "${username}" ];
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ if environment :matches "imap.user" "*" {
|
|||||||
set "username" "${1}";
|
set "username" "${1}";
|
||||||
}
|
}
|
||||||
|
|
||||||
pipe :copy "sa-learn-spam.sh" [ "${username}" ];
|
pipe :copy "rspamd-learn-spam.sh" [ "${username}" ];
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -o errexit
|
|
||||||
exec rspamc -h /run/rspamd/worker-controller.sock learn_ham
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
set -o errexit
|
|
||||||
exec rspamc -h /run/rspamd/worker-controller.sock learn_spam
|
|
||||||
@@ -14,15 +14,26 @@
|
|||||||
# 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,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = lib.mkIf cfg.enable {
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages =
|
||||||
dovecot opendkim openssh postfix rspamd
|
with pkgs;
|
||||||
] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []);
|
[
|
||||||
|
dovecot
|
||||||
|
openssh
|
||||||
|
postfix
|
||||||
|
rspamd
|
||||||
|
]
|
||||||
|
++ (if cfg.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;
|
||||||
@@ -24,4 +24,3 @@ in
|
|||||||
services.kresd.enable = true;
|
services.kresd.enable = true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -20,18 +20,20 @@ let
|
|||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf (enable && openFirewall) {
|
config = lib.mkIf (cfg.enable && cfg.openFirewall) {
|
||||||
|
|
||||||
networking.firewall = {
|
networking.firewall = {
|
||||||
allowedTCPPorts = [ 25 ]
|
allowedTCPPorts = [
|
||||||
++ lib.optional enableSubmission 587
|
25
|
||||||
++ lib.optional enableSubmissionSsl 465
|
]
|
||||||
++ lib.optional enableImap 143
|
++ lib.optional cfg.enableSubmission 587
|
||||||
++ lib.optional enableImapSsl 993
|
++ lib.optional cfg.enableSubmissionSsl 465
|
||||||
++ lib.optional enablePop3 110
|
++ lib.optional cfg.enableImap 143
|
||||||
++ lib.optional enablePop3Ssl 995
|
++ lib.optional cfg.enableImapSsl 993
|
||||||
++ lib.optional enableManageSieve 4190
|
++ lib.optional cfg.enablePop3 110
|
||||||
++ lib.optional (certificateScheme == "acme-nginx") 80;
|
++ lib.optional cfg.enablePop3Ssl 995
|
||||||
|
++ lib.optional cfg.enableManageSieve 4190
|
||||||
|
++ lib.optional (cfg.certificateScheme == "acme-nginx") 80;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,17 +14,30 @@
|
|||||||
# 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,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
with (import ./common.nix {
|
||||||
|
inherit
|
||||||
with (import ./common.nix { inherit config; });
|
config
|
||||||
|
options
|
||||||
|
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 == "acme" || cfg.certificateScheme == "acme-nginx")) {
|
config =
|
||||||
|
lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"))
|
||||||
|
{
|
||||||
services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
|
services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
|
||||||
enable = true;
|
enable = true;
|
||||||
virtualHosts."${cfg.fqdn}" = {
|
virtualHosts."${cfg.fqdn}" = {
|
||||||
@@ -32,13 +45,15 @@ in
|
|||||||
serverAliases = cfg.certificateDomains;
|
serverAliases = cfg.certificateDomains;
|
||||||
forceSSL = true;
|
forceSSL = true;
|
||||||
enableACME = true;
|
enableACME = true;
|
||||||
acmeRoot = acmeRoot;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
security.acme.certs."${cfg.fqdn}".reloadServices = [
|
security.acme.certs."${cfg.acmeCertificateName}" = {
|
||||||
|
extraDomainNames = lib.mkIf (cfg.certificateScheme == "acme") cfg.certificateDomains;
|
||||||
|
reloadServices = [
|
||||||
"postfix.service"
|
"postfix.service"
|
||||||
"dovecot2.service"
|
"dovecot.service"
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2017 Brian Olsen
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
{ config, lib, pkgs, ... }:
|
|
||||||
|
|
||||||
with lib;
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.mailserver;
|
|
||||||
|
|
||||||
dkimUser = config.services.opendkim.user;
|
|
||||||
dkimGroup = config.services.opendkim.group;
|
|
||||||
|
|
||||||
createDomainDkimCert = dom:
|
|
||||||
let
|
|
||||||
dkim_key = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key";
|
|
||||||
dkim_txt = "${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.txt";
|
|
||||||
in
|
|
||||||
''
|
|
||||||
if [ ! -f "${dkim_key}" ]
|
|
||||||
then
|
|
||||||
${pkgs.opendkim}/bin/opendkim-genkey -s "${cfg.dkimSelector}" \
|
|
||||||
-d "${dom}" \
|
|
||||||
--bits="${toString cfg.dkimKeyBits}" \
|
|
||||||
--directory="${cfg.dkimKeyDirectory}"
|
|
||||||
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.private" "${dkim_key}"
|
|
||||||
mv "${cfg.dkimKeyDirectory}/${cfg.dkimSelector}.txt" "${dkim_txt}"
|
|
||||||
chmod 644 "${dkim_txt}"
|
|
||||||
echo "Generated key for domain ${dom} selector ${cfg.dkimSelector}"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
createAllCerts = lib.concatStringsSep "\n" (map createDomainDkimCert cfg.domains);
|
|
||||||
|
|
||||||
keyTable = pkgs.writeText "opendkim-KeyTable"
|
|
||||||
(lib.concatStringsSep "\n" (lib.flip map cfg.domains
|
|
||||||
(dom: "${dom} ${dom}:${cfg.dkimSelector}:${cfg.dkimKeyDirectory}/${dom}.${cfg.dkimSelector}.key")));
|
|
||||||
signingTable = pkgs.writeText "opendkim-SigningTable"
|
|
||||||
(lib.concatStringsSep "\n" (lib.flip map cfg.domains (dom: "${dom} ${dom}")));
|
|
||||||
|
|
||||||
dkim = config.services.opendkim;
|
|
||||||
args = [ "-f" "-l" ] ++ lib.optionals (dkim.configFile != null) [ "-x" dkim.configFile ];
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = mkIf (cfg.dkimSigning && cfg.enable) {
|
|
||||||
services.opendkim = {
|
|
||||||
enable = true;
|
|
||||||
selector = cfg.dkimSelector;
|
|
||||||
keyPath = cfg.dkimKeyDirectory;
|
|
||||||
domains = "csl:${builtins.concatStringsSep "," cfg.domains}";
|
|
||||||
configFile = pkgs.writeText "opendkim.conf" (''
|
|
||||||
Canonicalization ${cfg.dkimHeaderCanonicalization}/${cfg.dkimBodyCanonicalization}
|
|
||||||
UMask 0002
|
|
||||||
Socket ${dkim.socket}
|
|
||||||
KeyTable file:${keyTable}
|
|
||||||
SigningTable file:${signingTable}
|
|
||||||
'' + (lib.optionalString cfg.debug ''
|
|
||||||
Syslog yes
|
|
||||||
SyslogSuccess yes
|
|
||||||
LogWhy yes
|
|
||||||
''));
|
|
||||||
};
|
|
||||||
|
|
||||||
users.users = optionalAttrs (config.services.postfix.user == "postfix") {
|
|
||||||
postfix.extraGroups = [ "${dkimGroup}" ];
|
|
||||||
};
|
|
||||||
systemd.services.opendkim = {
|
|
||||||
preStart = lib.mkForce createAllCerts;
|
|
||||||
serviceConfig = {
|
|
||||||
ExecStart = lib.mkForce "${pkgs.opendkim}/bin/opendkim ${escapeShellArgs args}";
|
|
||||||
PermissionsStartOnly = lib.mkForce false;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d '${cfg.dkimKeyDirectory}' - ${dkimUser} ${dkimGroup} - -"
|
|
||||||
];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
|
||||||
|
|
||||||
with lib;
|
|
||||||
|
|
||||||
let
|
|
||||||
cfg = config.mailserver;
|
|
||||||
in
|
|
||||||
{
|
|
||||||
config = mkIf (cfg.enable && cfg.rebootAfterKernelUpgrade.enable) {
|
|
||||||
systemd.services.nixos-upgrade.serviceConfig.ExecStartPost = pkgs.writeScript "post-upgrade-check" ''
|
|
||||||
#!${pkgs.stdenv.shell}
|
|
||||||
|
|
||||||
# Checks whether the "current" kernel is different from the booted kernel
|
|
||||||
# and then triggers a reboot so that the "current" kernel will be the booted one.
|
|
||||||
# This is just an educated guess. If the links do not differ the kernels might still be different, according to spacefrogg in #nixos.
|
|
||||||
|
|
||||||
current=$(readlink -f /run/current-system/kernel)
|
|
||||||
booted=$(readlink -f /run/booted-system/kernel)
|
|
||||||
|
|
||||||
if [ "$current" == "$booted" ]; then
|
|
||||||
echo "kernel version seems unchanged, skipping reboot" | systemd-cat --priority 4 --identifier "post-upgrade-check";
|
|
||||||
else
|
|
||||||
echo "kernel path changed, possibly a new version" | systemd-cat --priority 2 --identifier "post-upgrade-check"
|
|
||||||
echo "$booted" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
|
|
||||||
echo "$current" | systemd-cat --priority 2 --identifier "post-upgrade-kernel-check"
|
|
||||||
${cfg.rebootAfterKernelUpgrade.method}
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -14,45 +14,84 @@
|
|||||||
# 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,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
with (import ./common.nix { inherit config pkgs lib; });
|
with (import ./common.nix {
|
||||||
|
inherit
|
||||||
|
config
|
||||||
|
options
|
||||||
|
lib
|
||||||
|
pkgs
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (lib.strings) concatStringsSep;
|
inherit (lib.strings) concatStringsSep;
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
|
iniFormat = pkgs.formats.iniWithGlobalSection { };
|
||||||
|
|
||||||
# Merge several lookup tables. A lookup table is a attribute set where
|
# Merge several lookup tables. A lookup table is a attribute set where
|
||||||
# - the key is an address (user@example.com) or a domain (@example.com)
|
# - the key is an address (user@example.com) or a domain (@example.com)
|
||||||
# - the value is a list of addresses
|
# - the value is a list of addresses
|
||||||
mergeLookupTables = tables: lib.zipAttrsWith (n: v: lib.flatten v) tables;
|
mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables;
|
||||||
|
|
||||||
# valiases_postfix :: Map String [String]
|
# valiases_postfix :: Map String [String]
|
||||||
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
valiases_postfix = mergeLookupTables (
|
||||||
(name: value:
|
lib.flatten (
|
||||||
let to = name;
|
lib.mapAttrsToList (
|
||||||
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
|
name: value:
|
||||||
cfg.loginAccounts));
|
let
|
||||||
regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
to = name;
|
||||||
(name: value:
|
in
|
||||||
let to = name;
|
map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name)
|
||||||
in map (from: {"${from}" = to;}) value.aliasesRegexp)
|
) 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 (
|
||||||
(name: value:
|
lib.flatten (
|
||||||
let to = name;
|
lib.mapAttrsToList (
|
||||||
in map (from: {"@${from}" = to;}) value.catchAll)
|
name: value:
|
||||||
cfg.loginAccounts));
|
let
|
||||||
|
to = name;
|
||||||
|
in
|
||||||
|
map (from: { "@${from}" = to; }) value.catchAll
|
||||||
|
) cfg.loginAccounts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
# all_valiases_postfix :: Map String [String]
|
# all_valiases_postfix :: Map String [String]
|
||||||
all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix];
|
all_valiases_postfix = mergeLookupTables [
|
||||||
|
valiases_postfix
|
||||||
|
extra_valiases_postfix
|
||||||
|
];
|
||||||
|
|
||||||
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
|
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
|
||||||
attrsToLookupTable = aliases: let
|
attrsToLookupTable =
|
||||||
|
aliases:
|
||||||
|
let
|
||||||
lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases;
|
lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases;
|
||||||
in mergeLookupTables lookupTables;
|
in
|
||||||
|
mergeLookupTables lookupTables;
|
||||||
|
|
||||||
# extra_valiases_postfix :: Map String [String]
|
# extra_valiases_postfix :: Map String [String]
|
||||||
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
|
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
|
||||||
@@ -61,37 +100,49 @@ let
|
|||||||
forwards = attrsToLookupTable cfg.forwards;
|
forwards = attrsToLookupTable cfg.forwards;
|
||||||
|
|
||||||
# lookupTableToString :: Map String [String] -> String
|
# lookupTableToString :: Map String [String] -> String
|
||||||
lookupTableToString = attrs: let
|
lookupTableToString =
|
||||||
|
attrs:
|
||||||
|
let
|
||||||
valueToString = value: lib.concatStringsSep ", " value;
|
valueToString = value: lib.concatStringsSep ", " value;
|
||||||
in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
|
in
|
||||||
|
lib.concatStringsSep "\n" (
|
||||||
|
lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs
|
||||||
|
);
|
||||||
|
|
||||||
# valiases_file :: Path
|
# valiases_file :: Path
|
||||||
valiases_file = let
|
valiases_file =
|
||||||
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
|
let
|
||||||
in builtins.toFile "valias" content;
|
content = lookupTableToString (mergeLookupTables [
|
||||||
|
all_valiases_postfix
|
||||||
|
catchAllPostfix
|
||||||
|
]);
|
||||||
|
in
|
||||||
|
builtins.toFile "valias" content;
|
||||||
|
|
||||||
regex_valiases_file = let
|
regex_valiases_file =
|
||||||
|
let
|
||||||
content = lookupTableToString regex_valiases_postfix;
|
content = lookupTableToString regex_valiases_postfix;
|
||||||
in builtins.toFile "regex_valias" content;
|
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}")
|
lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)
|
||||||
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
|
);
|
||||||
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
|
denied_recipients_file = builtins.toFile "denied_recipients" (
|
||||||
|
lib.concatStringsSep "\n" denied_recipients_postfix
|
||||||
|
);
|
||||||
|
|
||||||
reject_senders_postfix = (map
|
reject_senders_postfix = map (sender: "${sender} REJECT") cfg.rejectSender;
|
||||||
(sender:
|
reject_senders_file = builtins.toFile "reject_senders" (
|
||||||
"${sender} REJECT")
|
lib.concatStringsSep "\n" reject_senders_postfix
|
||||||
(cfg.rejectSender));
|
);
|
||||||
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
|
|
||||||
|
|
||||||
reject_recipients_postfix = (map
|
reject_recipients_postfix = map (recipient: "${recipient} REJECT") cfg.rejectRecipients;
|
||||||
(recipient:
|
|
||||||
"${recipient} REJECT")
|
|
||||||
(cfg.rejectRecipients));
|
|
||||||
# rejectRecipients :: [ Path ]
|
# rejectRecipients :: [ Path ]
|
||||||
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ;
|
reject_recipients_file = builtins.toFile "reject_recipients" (
|
||||||
|
lib.concatStringsSep "\n" reject_recipients_postfix
|
||||||
|
);
|
||||||
|
|
||||||
# vhosts_file :: Path
|
# vhosts_file :: Path
|
||||||
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
|
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
|
||||||
@@ -103,9 +154,12 @@ 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);
|
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.
|
||||||
# See https://thomas-leister.de/mailserver-debian-stretch/
|
# See https://thomas-leister.de/mailserver-debian-stretch/
|
||||||
# Uses "pcre" style regex.
|
# Uses "pcre" style regex.
|
||||||
@@ -115,28 +169,22 @@ let
|
|||||||
/^X-Mailer:/ IGNORE
|
/^X-Mailer:/ IGNORE
|
||||||
/^User-Agent:/ IGNORE
|
/^User-Agent:/ IGNORE
|
||||||
/^X-Enigmail:/ IGNORE
|
/^X-Enigmail:/ IGNORE
|
||||||
'' + lib.optionalString cfg.rewriteMessageId ''
|
''
|
||||||
|
+ lib.optionalString cfg.rewriteMessageId ''
|
||||||
|
|
||||||
# Replaces the user submitted hostname with the server's FQDN to hide the
|
# Replaces the user submitted hostname with the server's FQDN to hide the
|
||||||
# user's host or network.
|
# user's host or network.
|
||||||
|
|
||||||
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
|
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
|
||||||
'');
|
''
|
||||||
|
);
|
||||||
|
|
||||||
inetSocket = addr: port: "inet:[${toString port}@${addr}]";
|
smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
||||||
unixSocket = sock: "unix:${sock}";
|
|
||||||
|
|
||||||
smtpdMilters =
|
|
||||||
(lib.optional cfg.dkimSigning "unix:/run/opendkim/opendkim.sock")
|
|
||||||
++ [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
|
||||||
|
|
||||||
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}";
|
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
|
||||||
|
|
||||||
submissionOptions =
|
submissionOptions = {
|
||||||
{
|
|
||||||
smtpd_tls_security_level = "encrypt";
|
smtpd_tls_security_level = "encrypt";
|
||||||
smtpd_sasl_auth_enable = "yes";
|
smtpd_sasl_auth_enable = "yes";
|
||||||
smtpd_sasl_type = "dovecot";
|
smtpd_sasl_type = "dovecot";
|
||||||
@@ -144,7 +192,9 @@ let
|
|||||||
smtpd_sasl_security_options = "noanonymous";
|
smtpd_sasl_security_options = "noanonymous";
|
||||||
smtpd_sasl_local_domain = "$myhostname";
|
smtpd_sasl_local_domain = "$myhostname";
|
||||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||||
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${lib.optionalString (regex_valiases_postfix != {}) ",pcre:/etc/postfix/regex_vaccounts"}";
|
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${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";
|
||||||
@@ -193,20 +243,55 @@ let
|
|||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
# SMTP TLS error reporting (RFC 8460)
|
||||||
|
services.tlsrpt = {
|
||||||
|
inherit (cfg.tlsrpt) enable;
|
||||||
|
configurePostfix = true;
|
||||||
|
reportd.settings = {
|
||||||
|
organization_name = cfg.systemName;
|
||||||
|
contact_info = "${cfg.systemContact}";
|
||||||
|
sender_address = "noreply-tlsrpt@${cfg.systemDomain}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# SMTP client policy mapping for DANE (RFC 6698) and MTA-STS (RFC 8461)
|
||||||
|
services.postfix-tlspol = {
|
||||||
|
enable = true;
|
||||||
|
configurePostfix = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
# Sender Rewriting Scheme (https://www.libsrs2.net/srs/srs.pdf)
|
||||||
|
services.postsrsd = {
|
||||||
|
inherit (cfg.srs) enable;
|
||||||
|
configurePostfix = true;
|
||||||
|
settings = {
|
||||||
|
domains = lib.unique (
|
||||||
|
[
|
||||||
|
cfg.fqdn
|
||||||
|
cfg.sendingFqdn
|
||||||
|
cfg.systemDomain
|
||||||
|
]
|
||||||
|
++ cfg.domains
|
||||||
|
);
|
||||||
|
separator = "=";
|
||||||
|
srs-domain = cfg.srs.domain;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
|
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
|
||||||
preStart = ''
|
preStart = ''
|
||||||
${appendPwdInVirtualMailboxMap}
|
${appendPwdInVirtualMailboxMap}
|
||||||
${appendPwdInSenderLoginMap}
|
${appendPwdInSenderLoginMap}
|
||||||
'';
|
'';
|
||||||
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
|
restartTriggers = [
|
||||||
|
appendPwdInVirtualMailboxMap
|
||||||
|
appendPwdInSenderLoginMap
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
services.postfix = {
|
services.postfix = {
|
||||||
enable = true;
|
enable = true;
|
||||||
hostname = "${sendingFqdn}";
|
|
||||||
networksStyle = "host";
|
|
||||||
mapFiles."valias" = valiases_file;
|
mapFiles."valias" = valiases_file;
|
||||||
mapFiles."regex_valias" = regex_valiases_file;
|
mapFiles."regex_valias" = regex_valiases_file;
|
||||||
mapFiles."vaccounts" = vaccounts_file;
|
mapFiles."vaccounts" = vaccounts_file;
|
||||||
@@ -214,36 +299,43 @@ in
|
|||||||
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;
|
||||||
sslCert = certificatePath;
|
|
||||||
sslKey = keyPath;
|
|
||||||
enableSubmission = cfg.enableSubmission;
|
enableSubmission = cfg.enableSubmission;
|
||||||
enableSubmissions = cfg.enableSubmissionSsl;
|
enableSubmissions = cfg.enableSubmissionSsl;
|
||||||
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
|
virtual = lookupTableToString (mergeLookupTables [
|
||||||
|
all_valiases_postfix
|
||||||
|
catchAllPostfix
|
||||||
|
forwards
|
||||||
|
]);
|
||||||
|
|
||||||
config = {
|
settings.main = {
|
||||||
# Extra Config
|
myhostname = cfg.sendingFqdn;
|
||||||
mydestination = "";
|
mydestination = ""; # disable local mail delivery
|
||||||
recipient_delimiter = cfg.recipientDelimiter;
|
recipient_delimiter = cfg.recipientDelimiter;
|
||||||
smtpd_banner = "${fqdn} ESMTP NO UCE";
|
smtpd_banner = "${cfg.fqdn} ESMTP NO UCE";
|
||||||
disable_vrfy_command = true;
|
disable_vrfy_command = true;
|
||||||
message_size_limit = toString cfg.messageSizeLimit;
|
message_size_limit = cfg.messageSizeLimit;
|
||||||
|
|
||||||
# virtual mail system
|
# virtual mail system
|
||||||
virtual_uid_maps = "static:5000";
|
virtual_uid_maps = "static:5000";
|
||||||
virtual_gid_maps = "static:5000";
|
virtual_gid_maps = "static:5000";
|
||||||
virtual_mailbox_base = mailDirectory;
|
virtual_mailbox_base = cfg.mailDirectory;
|
||||||
virtual_mailbox_domains = vhosts_file;
|
virtual_mailbox_domains = vhosts_file;
|
||||||
virtual_mailbox_maps = [
|
virtual_mailbox_maps = [
|
||||||
(mappedFile "valias")
|
(mappedFile "valias")
|
||||||
] ++ lib.optionals (cfg.ldap.enable) [
|
]
|
||||||
|
++ lib.optionals cfg.ldap.enable [
|
||||||
"ldap:${ldapVirtualMailboxMapFile}"
|
"ldap:${ldapVirtualMailboxMapFile}"
|
||||||
] ++ lib.optionals (regex_valiases_postfix != {}) [
|
]
|
||||||
|
++ lib.optionals (regex_valiases_postfix != { }) [
|
||||||
(mappedRegexFile "regex_valias")
|
(mappedRegexFile "regex_valias")
|
||||||
];
|
];
|
||||||
virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [
|
virtual_alias_maps = lib.mkAfter (
|
||||||
|
lib.optionals (regex_valiases_postfix != { }) [
|
||||||
(mappedRegexFile "regex_valias")
|
(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";
|
||||||
|
|
||||||
@@ -252,91 +344,125 @@ in
|
|||||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||||
smtpd_sasl_auth_enable = true;
|
smtpd_sasl_auth_enable = true;
|
||||||
smtpd_relay_restrictions = [
|
smtpd_relay_restrictions = [
|
||||||
"permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination"
|
"permit_mynetworks"
|
||||||
|
"permit_sasl_authenticated"
|
||||||
|
"reject_unauth_destination"
|
||||||
];
|
];
|
||||||
|
|
||||||
policy-spf_time_limit = "3600s";
|
|
||||||
|
|
||||||
# reject selected senders
|
# reject selected senders
|
||||||
smtpd_sender_restrictions = [
|
smtpd_sender_restrictions = [
|
||||||
"check_sender_access ${mappedFile "reject_senders"}"
|
"check_sender_access ${mappedFile "reject_senders"}"
|
||||||
];
|
];
|
||||||
|
|
||||||
# quota and spf checking
|
|
||||||
smtpd_recipient_restrictions = [
|
smtpd_recipient_restrictions = [
|
||||||
|
# reject selected recipients
|
||||||
"check_recipient_access ${mappedFile "denied_recipients"}"
|
"check_recipient_access ${mappedFile "denied_recipients"}"
|
||||||
"check_recipient_access ${mappedFile "reject_recipients"}"
|
"check_recipient_access ${mappedFile "reject_recipients"}"
|
||||||
"check_policy_service inet:localhost:12340"
|
# quota checking
|
||||||
"check_policy_service unix:private/policy-spf"
|
"check_policy_service unix:/run/dovecot2/quota-status"
|
||||||
];
|
];
|
||||||
|
|
||||||
# TLS settings, inspired by https://github.com/jeaye/nix-files
|
# The X509 private key followed by the corresponding certificate
|
||||||
# Submission by mail clients is handled in submissionOptions
|
smtpd_tls_chain_files = [
|
||||||
|
"${keyPath}"
|
||||||
|
"${certificatePath}"
|
||||||
|
];
|
||||||
|
|
||||||
|
# TLS for incoming mail is optional
|
||||||
smtpd_tls_security_level = "may";
|
smtpd_tls_security_level = "may";
|
||||||
|
|
||||||
# strong might suffice and is computationally less expensive
|
# But required for authentication attempts
|
||||||
smtpd_tls_eecdh_grade = "ultra";
|
smtpd_tls_auth_only = true;
|
||||||
|
|
||||||
# Disable obselete protocols
|
# TLS versions supported for the SMTP server
|
||||||
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
smtpd_tls_protocols = ">=TLSv1";
|
||||||
smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
smtpd_tls_mandatory_protocols = ">=TLSv1";
|
||||||
smtpd_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
|
||||||
smtp_tls_mandatory_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, !TLSv1, !SSLv2, !SSLv3";
|
|
||||||
|
|
||||||
smtp_tls_ciphers = "high";
|
# Require ciphersuites that OpenSSL classifies as "High"
|
||||||
smtpd_tls_ciphers = "high";
|
smtpd_tls_ciphers = "high";
|
||||||
smtp_tls_mandatory_ciphers = "high";
|
|
||||||
smtpd_tls_mandatory_ciphers = "high";
|
smtpd_tls_mandatory_ciphers = "high";
|
||||||
|
|
||||||
# Disable deprecated ciphers
|
# Exclude cipher suites with undesirable properties
|
||||||
smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
smtpd_tls_exclude_ciphers = "SHA1, eNULL, aNULL";
|
||||||
smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
smtpd_tls_mandatory_exclude_ciphers = "SHA1, eNULL, aNULL";
|
||||||
smtp_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
|
||||||
smtp_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
|
||||||
|
|
||||||
tls_preempt_cipherlist = true;
|
# Enable DNSSEC/DANE support for outgoing SMTP connections
|
||||||
|
# https://www.postfix.org/postconf.5.html#smtp_tls_security_level
|
||||||
|
smtp_dns_support_level = "dnssec";
|
||||||
|
smtp_tls_security_level = "dane";
|
||||||
|
|
||||||
|
# TLS versions supported for the SMTP client
|
||||||
|
smtp_tls_protocols = ">=TLSv1.2";
|
||||||
|
smtp_tls_mandatory_protocols = ">=TLSv1.2";
|
||||||
|
|
||||||
|
# Require ciphersuites that OpenSSL classifies as "High"
|
||||||
|
smtp_tls_ciphers = "high";
|
||||||
|
smtp_tls_mandatory_ciphers = "high";
|
||||||
|
|
||||||
|
# Exclude ciphersuites with undesirable properties
|
||||||
|
smtp_tls_exclude_ciphers = "SHA1, eNULL, aNULL";
|
||||||
|
smtp_tls_mandatory_exclude_ciphers = "SHA1, eNULL, aNULL";
|
||||||
|
|
||||||
|
# Restrict and prioritize the following curves in the given order
|
||||||
|
# Excludes curves that have no widespread support, so we don't bloat the handshake needlessly.
|
||||||
|
# https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves
|
||||||
|
tls_config_file =
|
||||||
|
let
|
||||||
|
mkGroupString = groups: concatStringsSep " / " (map (concatStringsSep ":") groups);
|
||||||
|
in
|
||||||
|
iniFormat.generate "postfix-openssl.cnf" {
|
||||||
|
globalSection.postfix = "postfix_settings";
|
||||||
|
sections = {
|
||||||
|
postfix_settings.ssl_conf = "postfix_ssl_settings";
|
||||||
|
postfix_ssl_settings.system_default = "baseline_postfix_settings";
|
||||||
|
baseline_postfix_settings.Groups = mkGroupString [
|
||||||
|
[ "*X25519MLKEM768" ]
|
||||||
|
[ "*X25519" ]
|
||||||
|
[
|
||||||
|
"P-256"
|
||||||
|
"P-384"
|
||||||
|
]
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
tls_config_name = "postfix";
|
||||||
|
|
||||||
|
# Algorithm selection happens through `tls_config_file` instead.
|
||||||
|
tls_eecdh_auto_curves = [ ];
|
||||||
|
tls_ffdhe_auto_groups = [ ];
|
||||||
|
|
||||||
|
# As long as all cipher suites are considered safe, let the client use its preferred cipher
|
||||||
|
tls_preempt_cipherlist = false;
|
||||||
|
|
||||||
# Allowing AUTH on a non encrypted connection poses a security risk
|
|
||||||
smtpd_tls_auth_only = true;
|
|
||||||
# Log only a summary message on TLS handshake completion
|
# Log only a summary message on TLS handshake completion
|
||||||
|
smtp_tls_loglevel = "1";
|
||||||
smtpd_tls_loglevel = "1";
|
smtpd_tls_loglevel = "1";
|
||||||
|
|
||||||
# Configure a non blocking source of randomness
|
|
||||||
tls_random_source = "dev:/dev/urandom";
|
|
||||||
|
|
||||||
smtpd_milters = smtpdMilters;
|
smtpd_milters = smtpdMilters;
|
||||||
non_smtpd_milters = lib.mkIf cfg.dkimSigning ["unix:/run/opendkim/opendkim.sock"];
|
non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
||||||
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_authen}";
|
||||||
|
|
||||||
# Fix for https://www.postfix.org/smtp-smuggling.html
|
|
||||||
smtpd_forbid_bare_newline = cfg.smtpdForbidBareNewline;
|
|
||||||
smtpd_forbid_bare_newline_exclusions = "$mynetworks";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
submissionOptions = submissionOptions;
|
submissionOptions = submissionOptions;
|
||||||
submissionsOptions = submissionOptions;
|
submissionsOptions = submissionOptions;
|
||||||
|
|
||||||
masterConfig = {
|
settings.master = {
|
||||||
"lmtp" = {
|
"lmtp" = {
|
||||||
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
|
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
|
||||||
# D => Delivered-To, O => X-Original-To, R => Return-Path
|
# D => Delivered-To, O => X-Original-To, R => Return-Path
|
||||||
args = [ "flags=O" ];
|
args = [ "flags=O" ];
|
||||||
};
|
};
|
||||||
"policy-spf" = {
|
|
||||||
type = "unix";
|
|
||||||
privileged = true;
|
|
||||||
chroot = false;
|
|
||||||
command = "spawn";
|
|
||||||
args = [ "user=nobody" "argv=${pkgs.pypolicyd-spf}/bin/policyd-spf" "${policyd-spf}"];
|
|
||||||
};
|
|
||||||
"submission-header-cleanup" = {
|
"submission-header-cleanup" = {
|
||||||
type = "unix";
|
type = "unix";
|
||||||
private = false;
|
private = false;
|
||||||
chroot = false;
|
chroot = false;
|
||||||
maxproc = 0;
|
maxproc = 0;
|
||||||
command = "cleanup";
|
command = "cleanup";
|
||||||
args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
|
args = [
|
||||||
|
"-o"
|
||||||
|
"header_checks=pcre:${submissionHeaderCleanupRules}"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,11 +14,19 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ config, pkgs, lib, ... }:
|
{
|
||||||
|
config,
|
||||||
with lib;
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
|
inherit (lib)
|
||||||
|
optionalString
|
||||||
|
mkIf
|
||||||
|
;
|
||||||
|
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
|
|
||||||
preexecDefined = cfg.backup.cmdPreexec != null;
|
preexecDefined = cfg.backup.cmdPreexec != null;
|
||||||
@@ -38,7 +46,8 @@ let
|
|||||||
${cfg.backup.cmdPostexec}
|
${cfg.backup.cmdPostexec}
|
||||||
'';
|
'';
|
||||||
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
|
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
config = mkIf (cfg.enable && cfg.backup.enable) {
|
config = mkIf (cfg.enable && cfg.backup.enable) {
|
||||||
services.rsnapshot = {
|
services.rsnapshot = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
|||||||
@@ -14,7 +14,12 @@
|
|||||||
# 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,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
@@ -22,27 +27,77 @@ let
|
|||||||
postfixCfg = config.services.postfix;
|
postfixCfg = config.services.postfix;
|
||||||
rspamdCfg = config.services.rspamd;
|
rspamdCfg = config.services.rspamd;
|
||||||
rspamdSocket = "rspamd.service";
|
rspamdSocket = "rspamd.service";
|
||||||
|
|
||||||
|
rspamdUser = config.services.rspamd.user;
|
||||||
|
rspamdGroup = config.services.rspamd.group;
|
||||||
|
|
||||||
|
createDkimKeypair =
|
||||||
|
domain:
|
||||||
|
let
|
||||||
|
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
|
||||||
|
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
|
||||||
|
in
|
||||||
|
pkgs.writeShellScript "dkim-keygen-${domain}" ''
|
||||||
|
if [ ! -f "${privateKey}" ]
|
||||||
|
then
|
||||||
|
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
|
||||||
|
--domain "${domain}" \
|
||||||
|
--selector "${cfg.dkimSelector}" \
|
||||||
|
--type "${cfg.dkimKeyType}" \
|
||||||
|
--bits ${toString cfg.dkimKeyBits} \
|
||||||
|
--privkey "${privateKey}" > "${publicKey}"
|
||||||
|
chmod 0644 "${publicKey}"
|
||||||
|
echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
|
||||||
|
dkimDomains = lib.unique (cfg.domains ++ (lib.optionals cfg.srs.enable [ cfg.srs.domain ]));
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = lib.mkIf cfg.enable {
|
||||||
|
environment.systemPackages = lib.mkBefore [
|
||||||
|
(pkgs.runCommand "rspamc-wrapped"
|
||||||
|
{
|
||||||
|
nativeBuildInputs = with pkgs; [ makeWrapper ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \
|
||||||
|
--add-flags "-h /run/rspamd/worker-controller.sock"
|
||||||
|
''
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
services.rspamd = {
|
services.rspamd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
inherit debug;
|
debug = cfg.debug.rspamd;
|
||||||
locals = {
|
locals = {
|
||||||
"milter_headers.conf" = { text = ''
|
"milter_headers.conf" = {
|
||||||
extended_spam_headers = yes;
|
text = ''
|
||||||
''; };
|
extended_spam_headers = true;
|
||||||
"redis.conf" = { text = ''
|
'';
|
||||||
servers = "${cfg.redis.address}:${toString cfg.redis.port}";
|
};
|
||||||
'' + (lib.optionalString (cfg.redis.password != null) ''
|
"redis.conf" = {
|
||||||
|
text = ''
|
||||||
|
servers = "${
|
||||||
|
if cfg.redis.port == null then
|
||||||
|
cfg.redis.address
|
||||||
|
else
|
||||||
|
"${cfg.redis.address}:${toString cfg.redis.port}"
|
||||||
|
}";
|
||||||
|
''
|
||||||
|
+ (lib.optionalString (cfg.redis.password != null) ''
|
||||||
password = "${cfg.redis.password}";
|
password = "${cfg.redis.password}";
|
||||||
''); };
|
'');
|
||||||
"classifier-bayes.conf" = { text = ''
|
};
|
||||||
|
"classifier-bayes.conf" = {
|
||||||
|
text = ''
|
||||||
cache {
|
cache {
|
||||||
backend = "redis";
|
backend = "redis";
|
||||||
}
|
}
|
||||||
''; };
|
'';
|
||||||
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
|
};
|
||||||
|
"antivirus.conf" = lib.mkIf cfg.virusScanning {
|
||||||
|
text = ''
|
||||||
clamav {
|
clamav {
|
||||||
action = "reject";
|
action = "reject";
|
||||||
symbol = "CLAM_VIRUS";
|
symbol = "CLAM_VIRUS";
|
||||||
@@ -51,38 +106,52 @@ in
|
|||||||
servers = "/run/clamav/clamd.ctl";
|
servers = "/run/clamav/clamd.ctl";
|
||||||
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
|
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
|
||||||
}
|
}
|
||||||
''; };
|
'';
|
||||||
"dkim_signing.conf" = { text = ''
|
};
|
||||||
# Disable outbound email signing, we use opendkim for this
|
"dkim_signing.conf" = {
|
||||||
enabled = false;
|
text = ''
|
||||||
''; };
|
enabled = ${lib.boolToString cfg.dkimSigning};
|
||||||
"dmarc.conf" = { text = ''
|
path = "${cfg.dkimKeyDirectory}/$domain.$selector.key";
|
||||||
|
selector = "${cfg.dkimSelector}";
|
||||||
|
# Allow for usernames w/o domain part
|
||||||
|
allow_username_mismatch = true;
|
||||||
|
# Don't normalize DKIM key selection for subdomains
|
||||||
|
use_esld = false;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
"dmarc.conf" = {
|
||||||
|
text = ''
|
||||||
${lib.optionalString cfg.dmarcReporting.enable ''
|
${lib.optionalString cfg.dmarcReporting.enable ''
|
||||||
reporting {
|
reporting {
|
||||||
enabled = true;
|
enabled = true;
|
||||||
email = "${cfg.dmarcReporting.email}";
|
email = "noreply-dmarc@${cfg.systemDomain}";
|
||||||
domain = "${cfg.dmarcReporting.domain}";
|
domain = "${cfg.systemDomain}";
|
||||||
org_name = "${cfg.dmarcReporting.organizationName}";
|
org_name = "${cfg.systemName}";
|
||||||
from_name = "${cfg.dmarcReporting.fromName}";
|
from_name = "${cfg.systemName}";
|
||||||
msgid_from = "dmarc-rua";
|
msgid_from = "${cfg.systemDomain}";
|
||||||
|
${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) ''
|
||||||
|
exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
|
||||||
|
''}
|
||||||
}''}
|
}''}
|
||||||
''; };
|
'';
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
overrides = {
|
overrides = {
|
||||||
"milter_headers.conf" = {
|
"options.inc" = {
|
||||||
text = ''
|
text = ''
|
||||||
extended_spam_headers = true;
|
local_addrs = [::1/128, 127.0.0.0/8]
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
workers.rspamd_proxy = {
|
workers.rspamd_proxy = {
|
||||||
type = "rspamd_proxy";
|
type = "rspamd_proxy";
|
||||||
bindSockets = [{
|
bindSockets = [
|
||||||
|
{
|
||||||
socket = "/run/rspamd/rspamd-milter.sock";
|
socket = "/run/rspamd/rspamd-milter.sock";
|
||||||
mode = "0664";
|
mode = "0664";
|
||||||
}];
|
}
|
||||||
|
];
|
||||||
count = 1; # Do not spawn too many processes of this type
|
count = 1; # Do not spawn too many processes of this type
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
milter = yes; # Enable milter mode
|
milter = yes; # Enable milter mode
|
||||||
@@ -97,10 +166,12 @@ in
|
|||||||
workers.controller = {
|
workers.controller = {
|
||||||
type = "controller";
|
type = "controller";
|
||||||
count = 1;
|
count = 1;
|
||||||
bindSockets = [{
|
bindSockets = [
|
||||||
|
{
|
||||||
socket = "/run/rspamd/worker-controller.sock";
|
socket = "/run/rspamd/worker-controller.sock";
|
||||||
mode = "0666";
|
mode = "0666";
|
||||||
}];
|
}
|
||||||
|
];
|
||||||
includes = [ ];
|
includes = [ ];
|
||||||
extraConfig = ''
|
extraConfig = ''
|
||||||
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
|
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
|
||||||
@@ -109,23 +180,46 @@ in
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
services.redis.servers.rspamd = {
|
services.redis.servers.rspamd.enable = lib.mkDefault cfg.redis.configureLocally;
|
||||||
enable = lib.mkDefault true;
|
|
||||||
port = lib.mkDefault 6380;
|
systemd.tmpfiles.settings."10-rspamd.conf" = {
|
||||||
|
"${cfg.dkimKeyDirectory}" = {
|
||||||
|
d = {
|
||||||
|
# Create /var/dkim owned by rspamd user/group
|
||||||
|
user = rspamdUser;
|
||||||
|
group = rspamdGroup;
|
||||||
|
};
|
||||||
|
Z = {
|
||||||
|
# Recursively adjust permissions in /var/dkim
|
||||||
|
user = rspamdUser;
|
||||||
|
group = rspamdGroup;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.rspamd = {
|
systemd.services.rspamd = {
|
||||||
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||||
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
|
||||||
|
serviceConfig = lib.mkMerge [
|
||||||
|
{
|
||||||
|
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
|
||||||
|
}
|
||||||
|
(lib.optionalAttrs cfg.dkimSigning {
|
||||||
|
ExecStartPre = map createDkimKeypair dkimDomains;
|
||||||
|
ReadWritePaths = [ cfg.dkimKeyDirectory ];
|
||||||
|
})
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
|
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
|
||||||
# Explicitly select yesterday's date to work around broken
|
# Explicitly select yesterday's date to work around broken
|
||||||
# default behaviour when called without a date.
|
# default behaviour when called without a date.
|
||||||
# https://github.com/rspamd/rspamd/issues/4062
|
# https://github.com/rspamd/rspamd/issues/4062
|
||||||
script = ''
|
script = toString [
|
||||||
${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
|
(lib.getExe' pkgs.rspamd "rspamadm")
|
||||||
'';
|
"dmarc_report"
|
||||||
|
"$(date -d 'yesterday' '+%Y%m%d')"
|
||||||
|
];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
User = "${config.services.rspamd.user}";
|
User = "${config.services.rspamd.user}";
|
||||||
Group = "${config.services.rspamd.group}";
|
Group = "${config.services.rspamd.group}";
|
||||||
@@ -151,10 +245,17 @@ in
|
|||||||
ProcSubset = "pid";
|
ProcSubset = "pid";
|
||||||
ProtectSystem = "strict";
|
ProtectSystem = "strict";
|
||||||
RemoveIPC = true;
|
RemoveIPC = true;
|
||||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
RestrictAddressFamilies = [
|
||||||
|
"AF_INET"
|
||||||
|
"AF_INET6"
|
||||||
|
"AF_UNIX"
|
||||||
|
];
|
||||||
RestrictNamespaces = true;
|
RestrictNamespaces = true;
|
||||||
RestrictRealtime = true;
|
RestrictRealtime = true;
|
||||||
RestrictSUIDSGID = true;
|
RestrictSUIDSGID = true;
|
||||||
|
SupplementaryGroups = lib.optionals cfg.redis.configureLocally [
|
||||||
|
config.services.redis.servers.rspamd.group
|
||||||
|
];
|
||||||
SystemCallArchitectures = "native";
|
SystemCallArchitectures = "native";
|
||||||
SystemCallFilter = [
|
SystemCallFilter = [
|
||||||
"@system-service"
|
"@system-service"
|
||||||
@@ -164,7 +265,7 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
|
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
|
||||||
description = "Daily delivery of aggregated DMARC reports";
|
description = "Daily delivery of aggregated DMARC reports";
|
||||||
wantedBy = [
|
wantedBy = [
|
||||||
"timers.target"
|
"timers.target"
|
||||||
@@ -185,4 +286,3 @@ in
|
|||||||
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,22 @@
|
|||||||
# 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,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
with (import ./common.nix {
|
||||||
|
inherit
|
||||||
|
config
|
||||||
|
options
|
||||||
|
lib
|
||||||
|
pkgs
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
let
|
let
|
||||||
cfg = config.mailserver;
|
cfg = config.mailserver;
|
||||||
@@ -25,11 +40,14 @@ let
|
|||||||
[ "mailserver-selfsigned-certificate.service" ]
|
[ "mailserver-selfsigned-certificate.service" ]
|
||||||
else
|
else
|
||||||
[ "acme-finished-${cfg.fqdn}.target" ];
|
[ "acme-finished-${cfg.fqdn}.target" ];
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
config = with cfg; lib.mkIf enable {
|
config = lib.mkIf cfg.enable {
|
||||||
# Create self signed certificate
|
# Create self signed certificate
|
||||||
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") {
|
systemd.services.mailserver-selfsigned-certificate =
|
||||||
|
lib.mkIf (cfg.certificateScheme == "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
|
||||||
@@ -53,21 +71,22 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
# Create maildir folder before dovecot startup
|
# Create maildir folder before dovecot startup
|
||||||
systemd.services.dovecot2 = {
|
systemd.services.dovecot = {
|
||||||
wants = certificatesDeps;
|
wants = certificatesDeps;
|
||||||
after = certificatesDeps;
|
after = certificatesDeps;
|
||||||
preStart = let
|
preStart =
|
||||||
|
let
|
||||||
directories = lib.strings.escapeShellArgs (
|
directories = lib.strings.escapeShellArgs (
|
||||||
[ mailDirectory ]
|
[ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
|
||||||
++ lib.optional (cfg.indexDir != null) cfg.indexDir
|
|
||||||
);
|
);
|
||||||
in ''
|
in
|
||||||
|
''
|
||||||
# Create mail directory and set permissions. See
|
# Create mail directory and set permissions. See
|
||||||
# <http://wiki2.dovecot.org/SharedMailboxes/Permissions>.
|
# <https://doc.dovecot.org/main/core/config/shared_mailboxes.html#filesystem-permissions-1>.
|
||||||
# Prevent world-readable paths, even temporarily.
|
# Prevent world-readable paths, even temporarily.
|
||||||
umask 007
|
umask 007
|
||||||
mkdir -p ${directories}
|
mkdir -p ${directories}
|
||||||
chgrp "${vmailGroupName}" ${directories}
|
chgrp "${cfg.vmailGroupName}" ${directories}
|
||||||
chmod 02770 ${directories}
|
chmod 02770 ${directories}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
@@ -75,11 +94,12 @@ in
|
|||||||
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
||||||
systemd.services.postfix = {
|
systemd.services.postfix = {
|
||||||
wants = certificatesDeps;
|
wants = certificatesDeps;
|
||||||
after = [ "dovecot2.service" ]
|
after = [
|
||||||
++ lib.optional cfg.dkimSigning "opendkim.service"
|
"dovecot.service"
|
||||||
|
]
|
||||||
|
++ lib.optional cfg.dkimSigning "rspamd.service"
|
||||||
++ certificatesDeps;
|
++ certificatesDeps;
|
||||||
requires = [ "dovecot2.service" ]
|
requires = [ "dovecot.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service";
|
||||||
++ lib.optional cfg.dkimSigning "opendkim.service";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,22 @@
|
|||||||
# 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,
|
||||||
|
options,
|
||||||
|
pkgs,
|
||||||
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
with (import ./common.nix {
|
||||||
|
inherit
|
||||||
|
config
|
||||||
|
options
|
||||||
|
lib
|
||||||
|
pkgs
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
with config.mailserver;
|
with config.mailserver;
|
||||||
|
|
||||||
@@ -28,7 +43,6 @@ let
|
|||||||
group = vmailGroupName;
|
group = vmailGroupName;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
|
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
|
|
||||||
@@ -46,8 +60,10 @@ let
|
|||||||
|
|
||||||
# Copy user's sieve script to the correct location (if it exists). If it
|
# Copy user's sieve script to the correct location (if it exists). If it
|
||||||
# is null, remove the file.
|
# is null, remove the file.
|
||||||
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
|
${lib.concatMapStringsSep "\n" (
|
||||||
if lib.isString sieveScript then ''
|
{ name, sieveScript }:
|
||||||
|
if lib.isString sieveScript then
|
||||||
|
''
|
||||||
if (! test -d "${sieveDirectory}/${name}"); then
|
if (! test -d "${sieveDirectory}/${name}"); then
|
||||||
mkdir -p "${sieveDirectory}/${name}"
|
mkdir -p "${sieveDirectory}/${name}"
|
||||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
|
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
|
||||||
@@ -57,34 +73,41 @@ let
|
|||||||
${sieveScript}
|
${sieveScript}
|
||||||
EOF
|
EOF
|
||||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
|
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
|
||||||
'' else ''
|
''
|
||||||
|
else
|
||||||
|
''
|
||||||
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
|
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
|
||||||
rm "${sieveDirectory}/${name}/default.sieve"
|
rm "${sieveDirectory}/${name}/default.sieve"
|
||||||
fi
|
fi
|
||||||
if (test -f "${sieveDirectory}/${name}.svbin"); then
|
if (test -f "${sieveDirectory}/${name}.svbin"); then
|
||||||
rm "${sieveDirectory}/${name}/default.svbin"
|
rm "${sieveDirectory}/${name}/default.svbin"
|
||||||
fi
|
fi
|
||||||
'') (map (user: { inherit (user) name sieveScript; })
|
''
|
||||||
(lib.attrValues loginAccounts))}
|
) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))}
|
||||||
'';
|
'';
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
config = lib.mkIf enable {
|
config = lib.mkIf enable {
|
||||||
# assert that all accounts provide a password
|
# assert that all accounts provide a password
|
||||||
assertions = (map (acct: {
|
assertions = map (acct: {
|
||||||
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
|
assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null;
|
||||||
message = "${acct.name} must provide either a hashed password or a password hash file";
|
message = "${acct.name} must provide either a hashed password or a password hash file";
|
||||||
}) (lib.attrValues loginAccounts));
|
}) (lib.attrValues loginAccounts);
|
||||||
|
|
||||||
# warn for accounts that specify both password and file
|
# warn for accounts that specify both password and file
|
||||||
warnings = (map
|
warnings =
|
||||||
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
|
map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
|
||||||
(lib.filter
|
(
|
||||||
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null))
|
lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) (
|
||||||
(lib.attrValues loginAccounts)));
|
lib.attrValues loginAccounts
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
# set the vmail gid to a specific value
|
# set the vmail gid to a specific value
|
||||||
users.groups = {
|
users.groups = {
|
||||||
"${vmailGroupName}" = { gid = vmailUID; };
|
"${vmailGroupName}" = {
|
||||||
|
gid = vmailUID;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
# define all users
|
# define all users
|
||||||
@@ -94,7 +117,7 @@ in {
|
|||||||
|
|
||||||
systemd.services.activate-virtual-mail-users = {
|
systemd.services.activate-virtual-mail-users = {
|
||||||
wantedBy = [ "multi-user.target" ];
|
wantedBy = [ "multi-user.target" ];
|
||||||
before = [ "dovecot2.service" ];
|
before = [ "dovecot.service" ];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = virtualMailUsersActivationScript;
|
ExecStart = virtualMailUsersActivationScript;
|
||||||
};
|
};
|
||||||
|
|||||||
146
migrations/nixos-mailserver-migration-03.py
Normal file
146
migrations/nixos-mailserver-migration-03.py
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
#!/usr/bin/env nix-shell
|
||||||
|
#!nix-shell -i python3 -p python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
from enum import Enum
|
||||||
|
from pathlib import Path
|
||||||
|
from pwd import getpwnam
|
||||||
|
|
||||||
|
|
||||||
|
class FolderLayout(Enum):
|
||||||
|
Default = 1
|
||||||
|
Folder = 2
|
||||||
|
|
||||||
|
|
||||||
|
def check_user(vmail_root: Path):
|
||||||
|
owner = vmail_root.owner()
|
||||||
|
owner_uid = getpwnam(owner).pw_uid
|
||||||
|
|
||||||
|
if os.geteuid() == owner_uid:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(
|
||||||
|
f"Trying to switch effective user id to {owner_uid} ({owner})",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
os.seteuid(owner_uid)
|
||||||
|
return
|
||||||
|
except PermissionError:
|
||||||
|
print(
|
||||||
|
f"Failed switching to virtual mail user. Please run this script under it, for example by using `sudo -u {owner}`)",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def is_maildir_related(path: Path, layout: FolderLayout) -> bool:
|
||||||
|
if path.name in [
|
||||||
|
"subscriptions",
|
||||||
|
# https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-uid-mapping
|
||||||
|
"dovecot-uidlist",
|
||||||
|
# https://doc.dovecot.org/2.3/admin_manual/mailbox_formats/maildir/#imap-keywords
|
||||||
|
"dovecot-keywords",
|
||||||
|
]:
|
||||||
|
return True
|
||||||
|
if not path.is_dir():
|
||||||
|
return False
|
||||||
|
if path.name in ["cur", "new", "tmp"]:
|
||||||
|
return True
|
||||||
|
if layout is FolderLayout.Default and path.name.startswith("."):
|
||||||
|
return True
|
||||||
|
if layout is FolderLayout.Folder:
|
||||||
|
if path.name in ["mail"]:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def mkdir(dst: Path, dry_run: bool = True):
|
||||||
|
print(f'mkdir "{dst}"')
|
||||||
|
if not dry_run:
|
||||||
|
# u+rwx, setgid
|
||||||
|
dst.mkdir(mode=0o2700)
|
||||||
|
|
||||||
|
|
||||||
|
def move(src: Path, dst: Path, dry_run: bool = True):
|
||||||
|
print(f'mv "{src}" "{dst}"')
|
||||||
|
if not dry_run:
|
||||||
|
src.rename(dst)
|
||||||
|
|
||||||
|
|
||||||
|
def delete(dst: Path, dry_run: bool = True):
|
||||||
|
if not dst.exists():
|
||||||
|
return
|
||||||
|
|
||||||
|
if dst.is_dir():
|
||||||
|
print(f'rm --recursive "{dst}"')
|
||||||
|
if not dry_run:
|
||||||
|
shutil.rmtree(dst)
|
||||||
|
else:
|
||||||
|
print(f'rm "{dst}"')
|
||||||
|
if not dry_run:
|
||||||
|
dst.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def main(vmail_root: Path, layout: FolderLayout, dry_run: bool = True):
|
||||||
|
maildirs = {path.parent for path in vmail_root.glob("*/*/cur")}
|
||||||
|
maybe_delete = []
|
||||||
|
|
||||||
|
# The old maildir will be the new home directory
|
||||||
|
for homedir in maildirs:
|
||||||
|
maildir = homedir / "mail"
|
||||||
|
mkdir(maildir, dry_run)
|
||||||
|
|
||||||
|
for path in homedir.iterdir():
|
||||||
|
if is_maildir_related(path, layout):
|
||||||
|
move(path, maildir / path.name, dry_run)
|
||||||
|
else:
|
||||||
|
maybe_delete.append(path)
|
||||||
|
|
||||||
|
# Files that are part of the previous home directory, but now obsolete
|
||||||
|
for path in [
|
||||||
|
vmail_root / ".dovecot.lda-dupes",
|
||||||
|
vmail_root / ".dovecot.lda-dupes.locks",
|
||||||
|
]:
|
||||||
|
delete(path, dry_run)
|
||||||
|
|
||||||
|
# The remaining files are likely obsolete, but should still be checked with care
|
||||||
|
for path in maybe_delete:
|
||||||
|
print(f"# rm {str(path)}")
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
print("\nNo changes were made.")
|
||||||
|
print("Run the script with `--execute` to apply the listed changes.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="""
|
||||||
|
NixOS Mailserver Migration #3: Dovecot mail directory migration
|
||||||
|
(https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#dovecot-mail-directory-migration)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"vmail_root", type=Path, help="Path to the `mailserver.mailDirectory`"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--layout",
|
||||||
|
choices=["default", "folder"],
|
||||||
|
required=True,
|
||||||
|
help="Folder layout: 'default' unless `mailserver.useFsLayout` was enabled, then'folder'",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--execute", action="store_true", help="Actually perform changes"
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
layout = FolderLayout.Default if args.layout == "default" else FolderLayout.Folder
|
||||||
|
|
||||||
|
check_user(args.vmail_root)
|
||||||
|
main(args.vmail_root, layout, not args.execute)
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
network.description = "mail server";
|
|
||||||
|
|
||||||
mailserver =
|
|
||||||
{ config, pkgs, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
../default.nix
|
|
||||||
];
|
|
||||||
|
|
||||||
mailserver = {
|
|
||||||
enable = true;
|
|
||||||
fqdn = "mail.example.com";
|
|
||||||
domains = [ "example.com" "example2.com" ];
|
|
||||||
loginAccounts = {
|
|
||||||
"user1@example.com" = {
|
|
||||||
hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
extraVirtualAliases = {
|
|
||||||
"info@example.com" = "user1@example.com";
|
|
||||||
"postmaster@example.com" = "user1@example.com";
|
|
||||||
"abuse@example.com" = "user1@example.com";
|
|
||||||
"user1@example2.com" = "user1@example.com";
|
|
||||||
"info@example2.com" = "user1@example.com";
|
|
||||||
"postmaster@example2.com" = "user1@example.com";
|
|
||||||
"abuse@example2.com" = "user1@example.com";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
mailserver =
|
|
||||||
{ config, pkgs, ... }:
|
|
||||||
{ deployment.targetEnv = "virtualbox";
|
|
||||||
deployment.virtualbox.memorySize = 1024; # megabytes
|
|
||||||
deployment.virtualbox.vcpu = 2; # number of cpus
|
|
||||||
deployment.virtualbox.headless = true;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
5
pyproject.toml
Normal file
5
pyproject.toml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
[tool.ruff.lint]
|
||||||
|
extend-select = ["ISC"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.flake8-implicit-str-concat]
|
||||||
|
allow-multiline = false
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
from textwrap import indent
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
header = """
|
header = """
|
||||||
# Mailserver options
|
# Mailserver options
|
||||||
@@ -21,62 +23,90 @@ template = """
|
|||||||
f = open(sys.argv[1])
|
f = open(sys.argv[1])
|
||||||
options = json.load(f)
|
options = json.load(f)
|
||||||
|
|
||||||
groups = ["mailserver.loginAccounts",
|
groups = [
|
||||||
|
"mailserver.loginAccounts",
|
||||||
"mailserver.certificate",
|
"mailserver.certificate",
|
||||||
"mailserver.dkim",
|
"mailserver.dkim",
|
||||||
|
"mailserver.srs",
|
||||||
"mailserver.dmarcReporting",
|
"mailserver.dmarcReporting",
|
||||||
"mailserver.fullTextSearch",
|
"mailserver.fullTextSearch",
|
||||||
"mailserver.redis",
|
"mailserver.redis",
|
||||||
"mailserver.ldap",
|
"mailserver.ldap",
|
||||||
"mailserver.monitoring",
|
"mailserver.monitoring",
|
||||||
"mailserver.backup",
|
"mailserver.backup",
|
||||||
"mailserver.borgbackup"]
|
"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):
|
def md_literal(value: str) -> str:
|
||||||
if isinstance(opt['description'], dict) and '_type' in opt['description']: # mdDoc
|
return f"`{value}`"
|
||||||
description = opt['description']['text']
|
|
||||||
|
|
||||||
|
def md_codefence(value: str, language: str = "nix") -> str:
|
||||||
|
return indent(
|
||||||
|
f"\n```{language}\n{value}\n```",
|
||||||
|
prefix=2 * " ",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def render_option_value(option: Mapping[str, Any], key: str) -> str:
|
||||||
|
if key not in option:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
if isinstance(option[key], dict) and "_type" in option[key]:
|
||||||
|
if option[key]["_type"] == "literalExpression":
|
||||||
|
# multi-line codeblock
|
||||||
|
if "\n" in option[key]["text"]:
|
||||||
|
text = option[key]["text"].rstrip("\n")
|
||||||
|
value = md_codefence(text)
|
||||||
|
# inline codeblock
|
||||||
else:
|
else:
|
||||||
description = opt['description']
|
value = md_literal(option[key]["text"])
|
||||||
print(template.format(
|
# literal markdown
|
||||||
key=opt['name'],
|
elif option[key]["_type"] == "literalMD":
|
||||||
|
value = option[key]["text"]
|
||||||
|
else:
|
||||||
|
assert RuntimeError(f"Unhandled option type {option[key]['_type']}")
|
||||||
|
else:
|
||||||
|
text = str(option[key])
|
||||||
|
if text == "":
|
||||||
|
value = md_literal('""')
|
||||||
|
elif "\n" in text:
|
||||||
|
value = md_codefence(text.rstrip("\n"))
|
||||||
|
else:
|
||||||
|
value = md_literal(text)
|
||||||
|
|
||||||
|
return f"- {key}: {value}" # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
def print_option(option):
|
||||||
|
if (
|
||||||
|
isinstance(option["description"], dict) and "_type" in option["description"]
|
||||||
|
): # mdDoc
|
||||||
|
description = option["description"]["text"]
|
||||||
|
else:
|
||||||
|
description = option["description"]
|
||||||
|
print(
|
||||||
|
template.format(
|
||||||
|
key=option["name"],
|
||||||
description=description or "",
|
description=description or "",
|
||||||
type="- type: ```{}```".format(opt['type']),
|
type=f"- type: {md_literal(option['type'])}",
|
||||||
default=render_option_value(opt, 'default'),
|
default=render_option_value(option, "defaultText")
|
||||||
example=render_option_value(opt, 'example')))
|
if "defaultText" in option
|
||||||
|
else render_option_value(option, "default"),
|
||||||
|
example=render_option_value(option, "example"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
print(header)
|
print(header)
|
||||||
for opt in options:
|
for opt in options:
|
||||||
if any([opt['name'].startswith(c) for c in groups]):
|
if any([opt["name"].startswith(c) for c in groups]):
|
||||||
continue
|
continue
|
||||||
print_option(opt)
|
print_option(opt)
|
||||||
|
|
||||||
for c in groups:
|
for c in groups:
|
||||||
print('## `{}`'.format(c))
|
print(f"## `{c}`\n")
|
||||||
print()
|
|
||||||
for opt in options:
|
for opt in options:
|
||||||
if opt['name'].startswith(c):
|
if opt["name"].startswith(c):
|
||||||
print_option(opt)
|
print_option(opt)
|
||||||
|
|||||||
@@ -1,31 +1,45 @@
|
|||||||
import smtplib, sys
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
|
||||||
import uuid
|
|
||||||
import imaplib
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import email
|
import email
|
||||||
|
import email.utils
|
||||||
|
import imaplib
|
||||||
|
import smtplib
|
||||||
import time
|
import time
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
RETRY = 100
|
RETRY = 100
|
||||||
|
|
||||||
def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls):
|
|
||||||
print("Sending mail with subject '{}'".format(subject))
|
|
||||||
message = "\n".join([
|
|
||||||
"From: {from_addr}",
|
|
||||||
"To: {to_addr}",
|
|
||||||
"Subject: {subject}",
|
|
||||||
"",
|
|
||||||
"This validates our mail server can send to Gmail :/"]).format(
|
|
||||||
from_addr=from_addr,
|
|
||||||
to_addr=to_addr,
|
|
||||||
subject=subject)
|
|
||||||
|
|
||||||
|
def _send_mail(
|
||||||
|
smtp_host,
|
||||||
|
smtp_port,
|
||||||
|
smtp_username,
|
||||||
|
from_addr,
|
||||||
|
from_pwd,
|
||||||
|
to_addr,
|
||||||
|
subject,
|
||||||
|
starttls,
|
||||||
|
ssl,
|
||||||
|
):
|
||||||
|
print(f"Sending mail with subject '{subject}'")
|
||||||
|
message = "\n".join(
|
||||||
|
[
|
||||||
|
f"From: {from_addr}",
|
||||||
|
f"To: {to_addr}",
|
||||||
|
f"Subject: {subject}",
|
||||||
|
f"Message-ID: {uuid.uuid4()}@mail-check.py",
|
||||||
|
f"Date: {email.utils.formatdate()}",
|
||||||
|
"",
|
||||||
|
"This validates our mail server can send to Gmail :/",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
retry = RETRY
|
retry = RETRY
|
||||||
|
smtp_class = smtplib.SMTP_SSL if ssl else smtplib.SMTP
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
with smtplib.SMTP(smtp_host, port=smtp_port) as smtp:
|
with smtp_class(smtp_host, port=smtp_port) as smtp:
|
||||||
try:
|
try:
|
||||||
if starttls:
|
if starttls:
|
||||||
smtp.starttls()
|
smtp.starttls()
|
||||||
@@ -37,7 +51,9 @@ def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr
|
|||||||
except smtplib.SMTPResponseException as e:
|
except smtplib.SMTPResponseException as e:
|
||||||
if e.smtp_code == 451: # service unavailable error
|
if e.smtp_code == 451: # service unavailable error
|
||||||
print(e)
|
print(e)
|
||||||
elif e.smtp_code == 454: # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later')
|
elif (
|
||||||
|
e.smtp_code == 454
|
||||||
|
): # smtplib.SMTPResponseException: (454, b'4.3.0 Try again later')
|
||||||
print(e)
|
print(e)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
@@ -55,6 +71,7 @@ def _send_mail(smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr
|
|||||||
print("Retry attempts exhausted")
|
print("Retry attempts exhausted")
|
||||||
exit(5)
|
exit(5)
|
||||||
|
|
||||||
|
|
||||||
def _read_mail(
|
def _read_mail(
|
||||||
imap_host,
|
imap_host,
|
||||||
imap_port,
|
imap_port,
|
||||||
@@ -63,8 +80,9 @@ def _read_mail(
|
|||||||
subject,
|
subject,
|
||||||
ignore_dkim_spf,
|
ignore_dkim_spf,
|
||||||
show_body=False,
|
show_body=False,
|
||||||
delete=True):
|
delete=True,
|
||||||
print("Reading mail from %s" % imap_username)
|
):
|
||||||
|
print(f"Reading mail from {imap_username}")
|
||||||
|
|
||||||
message = None
|
message = None
|
||||||
|
|
||||||
@@ -74,49 +92,62 @@ def _read_mail(
|
|||||||
|
|
||||||
today = datetime.today()
|
today = datetime.today()
|
||||||
cutoff = today - timedelta(days=1)
|
cutoff = today - timedelta(days=1)
|
||||||
dt = cutoff.strftime('%d-%b-%Y')
|
dt = cutoff.strftime("%d-%b-%Y")
|
||||||
for _ in range(0, RETRY):
|
for _ in range(0, RETRY):
|
||||||
print("Retrying")
|
print("Retrying")
|
||||||
obj.select()
|
obj.select()
|
||||||
typ, data = obj.search(None, '(SINCE %s) (SUBJECT "%s")'%(dt, subject))
|
_, data = obj.search(None, f'(SINCE {dt}) (SUBJECT "{subject}")')
|
||||||
if data == [b'']:
|
if data == [b""]:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
uids = data[0].decode("utf-8").split(" ")
|
uids = data[0].decode("utf-8").split(" ")
|
||||||
if len(uids) != 1:
|
if len(uids) != 1:
|
||||||
print("Warning: %d messages have been found with subject containing %s " % (len(uids), subject))
|
print(
|
||||||
|
f"Warning: {len(uids)} messages have been found with subject containing {subject}"
|
||||||
|
)
|
||||||
|
|
||||||
# FIXME: we only consider the first matching message...
|
# FIXME: we only consider the first matching message...
|
||||||
uid = uids[0]
|
uid = uids[0]
|
||||||
_, raw = obj.fetch(uid, '(RFC822)')
|
_, raw = obj.fetch(uid, "(RFC822)")
|
||||||
if delete:
|
if delete:
|
||||||
obj.store(uid, '+FLAGS', '\\Deleted')
|
obj.store(uid, "+FLAGS", "\\Deleted")
|
||||||
obj.expunge()
|
obj.expunge()
|
||||||
message = email.message_from_bytes(raw[0][1])
|
assert raw[0] and raw[0][1]
|
||||||
print("Message with subject '%s' has been found" % message['subject'])
|
message = email.message_from_bytes(cast(bytes, raw[0][1]))
|
||||||
|
print(f"Message with subject '{message['subject']}' has been found")
|
||||||
if show_body:
|
if show_body:
|
||||||
for m in message.get_payload():
|
if message.is_multipart():
|
||||||
if m.get_content_type() == 'text/plain':
|
for part in message.walk():
|
||||||
print("Body:\n%s" % m.get_payload(decode=True).decode('utf-8'))
|
ctype = part.get_content_type()
|
||||||
|
if ctype == "text/plain":
|
||||||
|
body = cast(bytes, part.get_payload(decode=True)).decode()
|
||||||
|
print(f"Body:\n{body}")
|
||||||
|
else:
|
||||||
|
print(f"Body with content type {ctype} not printed")
|
||||||
|
else:
|
||||||
|
body = cast(bytes, message.get_payload(decode=True)).decode()
|
||||||
|
print(f"Body:\n{body}")
|
||||||
break
|
break
|
||||||
|
|
||||||
if message is None:
|
if message is None:
|
||||||
print("Error: no message with subject '%s' has been found in INBOX of %s" % (subject, imap_username))
|
print(
|
||||||
|
f"Error: no message with subject '{subject}' has been found in INBOX of {imap_username}"
|
||||||
|
)
|
||||||
exit(1)
|
exit(1)
|
||||||
|
|
||||||
if ignore_dkim_spf:
|
if ignore_dkim_spf:
|
||||||
return
|
return
|
||||||
|
|
||||||
# gmail set this standardized header
|
# gmail set this standardized header
|
||||||
if 'ARC-Authentication-Results' in message:
|
if "ARC-Authentication-Results" in message:
|
||||||
if "dkim=pass" in message['ARC-Authentication-Results']:
|
if "dkim=pass" in message["ARC-Authentication-Results"]:
|
||||||
print("DKIM ok")
|
print("DKIM ok")
|
||||||
else:
|
else:
|
||||||
print("Error: no DKIM validation found in message:")
|
print("Error: no DKIM validation found in message:")
|
||||||
print(message.as_string())
|
print(message.as_string())
|
||||||
exit(2)
|
exit(2)
|
||||||
if "spf=pass" in message['ARC-Authentication-Results']:
|
if "spf=pass" in message["ARC-Authentication-Results"]:
|
||||||
print("SPF ok")
|
print("SPF ok")
|
||||||
else:
|
else:
|
||||||
print("Error: no SPF validation found in message:")
|
print("Error: no SPF validation found in message:")
|
||||||
@@ -126,71 +157,110 @@ def _read_mail(
|
|||||||
print("DKIM and SPF verification failed")
|
print("DKIM and SPF verification failed")
|
||||||
exit(4)
|
exit(4)
|
||||||
|
|
||||||
|
|
||||||
def send_and_read(args):
|
def send_and_read(args):
|
||||||
src_pwd = None
|
src_pwd = None
|
||||||
if args.src_password_file is not None:
|
if args.src_password_file is not None:
|
||||||
src_pwd = args.src_password_file.readline().rstrip()
|
src_pwd = args.src_password_file.readline().rstrip()
|
||||||
dst_pwd = args.dst_password_file.readline().rstrip()
|
dst_pwd = args.dst_password_file.readline().rstrip()
|
||||||
|
|
||||||
if args.imap_username != '':
|
if args.imap_username != "":
|
||||||
imap_username = args.imap_username
|
imap_username = args.imap_username
|
||||||
else:
|
else:
|
||||||
imap_username = args.to_addr
|
imap_username = args.to_addr
|
||||||
|
|
||||||
subject = "{}".format(uuid.uuid4())
|
subject = f"{uuid.uuid4()}"
|
||||||
|
|
||||||
_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,
|
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,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
starttls=args.smtp_starttls)
|
starttls=args.smtp_starttls,
|
||||||
|
ssl=args.smtp_ssl,
|
||||||
|
)
|
||||||
|
|
||||||
_read_mail(imap_host=args.imap_host,
|
_read_mail(
|
||||||
|
imap_host=args.imap_host,
|
||||||
imap_port=args.imap_port,
|
imap_port=args.imap_port,
|
||||||
imap_username=imap_username,
|
imap_username=imap_username,
|
||||||
to_pwd=dst_pwd,
|
to_pwd=dst_pwd,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
ignore_dkim_spf=args.ignore_dkim_spf)
|
ignore_dkim_spf=args.ignore_dkim_spf,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def read(args):
|
def read(args):
|
||||||
_read_mail(imap_host=args.imap_host,
|
_read_mail(
|
||||||
|
imap_host=args.imap_host,
|
||||||
imap_port=args.imap_port,
|
imap_port=args.imap_port,
|
||||||
to_addr=args.imap_username,
|
imap_username=args.imap_username,
|
||||||
to_pwd=args.imap_password,
|
to_pwd=args.imap_password,
|
||||||
subject=args.subject,
|
subject=args.subject,
|
||||||
ignore_dkim_spf=args.ignore_dkim_spf,
|
ignore_dkim_spf=args.ignore_dkim_spf,
|
||||||
show_body=args.show_body,
|
show_body=args.show_body,
|
||||||
delete=False)
|
delete=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
subparsers = parser.add_subparsers()
|
subparsers = parser.add_subparsers()
|
||||||
|
|
||||||
parser_send_and_read = subparsers.add_parser('send-and-read', description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.")
|
parser_send_and_read = subparsers.add_parser(
|
||||||
parser_send_and_read.add_argument('--smtp-host', type=str)
|
"send-and-read",
|
||||||
parser_send_and_read.add_argument('--smtp-port', type=str, default=25)
|
description="Send a email with a subject containing a random UUID and then try to read this email from the recipient INBOX.",
|
||||||
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("--smtp-host", type=str)
|
||||||
parser_send_and_read.add_argument('--from-addr', type=str)
|
parser_send_and_read.add_argument("--smtp-port", type=str, default=25)
|
||||||
parser_send_and_read.add_argument('--imap-host', required=True, type=str)
|
parser_send_and_read.add_argument("--smtp-starttls", action="store_true")
|
||||||
parser_send_and_read.add_argument('--imap-port', type=str, default=993)
|
parser_send_and_read.add_argument("--smtp-ssl", action="store_true")
|
||||||
parser_send_and_read.add_argument('--to-addr', type=str, required=True)
|
parser_send_and_read.add_argument(
|
||||||
parser_send_and_read.add_argument('--imap-username', type=str, default='', help="username used for imap login. If not specified, the to-addr value is used")
|
"--smtp-username",
|
||||||
parser_send_and_read.add_argument('--src-password-file', type=argparse.FileType('r'))
|
type=str,
|
||||||
parser_send_and_read.add_argument('--dst-password-file', required=True, type=argparse.FileType('r'))
|
default="",
|
||||||
parser_send_and_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail")
|
help="username used for smtp login. If not specified, the from-addr value is used",
|
||||||
|
)
|
||||||
|
parser_send_and_read.add_argument("--from-addr", type=str)
|
||||||
|
parser_send_and_read.add_argument("--imap-host", required=True, type=str)
|
||||||
|
parser_send_and_read.add_argument("--imap-port", type=str, default=993)
|
||||||
|
parser_send_and_read.add_argument("--to-addr", type=str, required=True)
|
||||||
|
parser_send_and_read.add_argument(
|
||||||
|
"--imap-username",
|
||||||
|
type=str,
|
||||||
|
default="",
|
||||||
|
help="username used for imap login. If not specified, the to-addr value is used",
|
||||||
|
)
|
||||||
|
parser_send_and_read.add_argument("--src-password-file", type=argparse.FileType("r"))
|
||||||
|
parser_send_and_read.add_argument(
|
||||||
|
"--dst-password-file", required=True, type=argparse.FileType("r")
|
||||||
|
)
|
||||||
|
parser_send_and_read.add_argument(
|
||||||
|
"--ignore-dkim-spf",
|
||||||
|
action="store_true",
|
||||||
|
help="to ignore the dkim and spf verification on the read mail",
|
||||||
|
)
|
||||||
parser_send_and_read.set_defaults(func=send_and_read)
|
parser_send_and_read.set_defaults(func=send_and_read)
|
||||||
|
|
||||||
parser_read = subparsers.add_parser('read', description="Search for an email with a subject containing 'subject' in the INBOX.")
|
parser_read = subparsers.add_parser(
|
||||||
parser_read.add_argument('--imap-host', type=str, default="localhost")
|
"read",
|
||||||
parser_read.add_argument('--imap-port', type=str, default=993)
|
description="Search for an email with a subject containing 'subject' in the INBOX.",
|
||||||
parser_read.add_argument('--imap-username', required=True, type=str)
|
)
|
||||||
parser_read.add_argument('--imap-password', required=True, type=str)
|
parser_read.add_argument("--imap-host", type=str, default="localhost")
|
||||||
parser_read.add_argument('--ignore-dkim-spf', action='store_true', help="to ignore the dkim and spf verification on the read mail")
|
parser_read.add_argument("--imap-port", type=str, default=993)
|
||||||
parser_read.add_argument('--show-body', action='store_true', help="print mail text/plain payload")
|
parser_read.add_argument("--imap-username", required=True, type=str)
|
||||||
parser_read.add_argument('subject', type=str)
|
parser_read.add_argument("--imap-password", required=True, type=str)
|
||||||
|
parser_read.add_argument(
|
||||||
|
"--ignore-dkim-spf",
|
||||||
|
action="store_true",
|
||||||
|
help="to ignore the dkim and spf verification on the read mail",
|
||||||
|
)
|
||||||
|
parser_read.add_argument(
|
||||||
|
"--show-body", action="store_true", help="print mail text/plain payload"
|
||||||
|
)
|
||||||
|
parser_read.add_argument("subject", type=str)
|
||||||
parser_read.set_defaults(func=read)
|
parser_read.set_defaults(func=read)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|||||||
11
shell.nix
11
shell.nix
@@ -1,10 +1,9 @@
|
|||||||
(import
|
(import (
|
||||||
(
|
let
|
||||||
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
|
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||||
|
in
|
||||||
fetchTarball {
|
fetchTarball {
|
||||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||||
}
|
}
|
||||||
)
|
) { src = ./.; }).shellNix
|
||||||
{ src = ./.; }
|
|
||||||
).shellNix
|
|
||||||
|
|||||||
@@ -14,12 +14,18 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ pkgs ? import <nixpkgs> {}, blobs}:
|
{
|
||||||
|
lib,
|
||||||
|
blobs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
pkgs.nixosTest {
|
{
|
||||||
name = "clamav";
|
name = "clamav";
|
||||||
|
|
||||||
nodes = {
|
nodes = {
|
||||||
server = { config, pkgs, lib, ... }:
|
server =
|
||||||
|
{ pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../default.nix
|
../default.nix
|
||||||
@@ -28,6 +34,8 @@ pkgs.nixosTest {
|
|||||||
|
|
||||||
virtualisation.memorySize = 1500;
|
virtualisation.memorySize = 1500;
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [ netcat ];
|
||||||
|
|
||||||
services.rsyslogd = {
|
services.rsyslogd = {
|
||||||
enable = true;
|
enable = true;
|
||||||
defaultConfig = ''
|
defaultConfig = ''
|
||||||
@@ -63,7 +71,10 @@ pkgs.nixosTest {
|
|||||||
mailserver = {
|
mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
fqdn = "mail.example.com";
|
fqdn = "mail.example.com";
|
||||||
domains = [ "example.com" "example2.com" ];
|
domains = [
|
||||||
|
"example.com"
|
||||||
|
"example2.com"
|
||||||
|
];
|
||||||
virusScanning = true;
|
virusScanning = true;
|
||||||
|
|
||||||
loginAccounts = {
|
loginAccounts = {
|
||||||
@@ -83,21 +94,28 @@ pkgs.nixosTest {
|
|||||||
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
client = { nodes, config, pkgs, ... }: let
|
client =
|
||||||
serverIP = nodes.server.config.networking.primaryIPAddress;
|
{ nodes, pkgs, ... }:
|
||||||
clientIP = nodes.client.config.networking.primaryIPAddress;
|
let
|
||||||
|
serverIP = nodes.server.networking.primaryIPAddress;
|
||||||
|
clientIP = nodes.client.networking.primaryIPAddress;
|
||||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
echo grep '${clientIP}' "$@" >&2
|
echo grep '${clientIP}' "$@" >&2
|
||||||
exec grep '${clientIP}' "$@"
|
exec grep '${clientIP}' "$@"
|
||||||
'';
|
'';
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./lib/config.nix
|
./lib/config.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
fetchmail msmtp procmail findutils grep-ip
|
fetchmail
|
||||||
|
msmtp
|
||||||
|
procmail
|
||||||
|
findutils
|
||||||
|
grep-ip
|
||||||
];
|
];
|
||||||
environment.etc = {
|
environment.etc = {
|
||||||
"root/.fetchmailrc" = {
|
"root/.fetchmailrc" = {
|
||||||
@@ -180,8 +198,7 @@ pkgs.nixosTest {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = { nodes, ... }:
|
testScript = ''
|
||||||
''
|
|
||||||
start_all()
|
start_all()
|
||||||
|
|
||||||
server.wait_for_unit("multi-user.target")
|
server.wait_for_unit("multi-user.target")
|
||||||
@@ -189,10 +206,10 @@ pkgs.nixosTest {
|
|||||||
|
|
||||||
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
|
# TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
|
||||||
server.wait_until_succeeds(
|
server.wait_until_succeeds(
|
||||||
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
||||||
)
|
)
|
||||||
server.wait_until_succeeds(
|
server.wait_until_succeeds(
|
||||||
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
|
||||||
)
|
)
|
||||||
|
|
||||||
client.execute("cp -p /etc/root/.* ~/")
|
client.execute("cp -p /etc/root/.* ~/")
|
||||||
@@ -222,7 +239,7 @@ pkgs.nixosTest {
|
|||||||
|
|
||||||
with subtest("virus scan email"):
|
with subtest("virus scan email"):
|
||||||
client.succeed(
|
client.succeed(
|
||||||
'set +o pipefail; msmtp -a user2 user1\@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
|
'set +o pipefail; msmtp -a user2 user1@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
|
||||||
)
|
)
|
||||||
server.succeed("journalctl -u rspamd | grep -i eicar")
|
server.succeed("journalctl -u rspamd | grep -i eicar")
|
||||||
# give the mail server some time to process the mail
|
# give the mail server some time to process the mail
|
||||||
|
|||||||
@@ -14,18 +14,20 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ pkgs ? import <nixpkgs> {}, ...}:
|
{
|
||||||
|
|
||||||
pkgs.nixosTest {
|
|
||||||
name = "external";
|
name = "external";
|
||||||
|
|
||||||
nodes = {
|
nodes = {
|
||||||
server = { config, pkgs, ... }:
|
server =
|
||||||
|
{ pkgs, ... }:
|
||||||
{
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../default.nix
|
../default.nix
|
||||||
./lib/config.nix
|
./lib/config.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
|
environment.systemPackages = with pkgs; [ netcat ];
|
||||||
|
|
||||||
virtualisation.memorySize = 1024;
|
virtualisation.memorySize = 1024;
|
||||||
|
|
||||||
services.rsyslogd = {
|
services.rsyslogd = {
|
||||||
@@ -35,19 +37,17 @@ pkgs.nixosTest {
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
mailserver = {
|
mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
debug = true;
|
debug.dovecot = true; # enabled for sieve script logging
|
||||||
fqdn = "mail.example.com";
|
fqdn = "mail.example.com";
|
||||||
domains = [ "example.com" "example2.com" ];
|
domains = [
|
||||||
|
"example.com"
|
||||||
|
"example2.com"
|
||||||
|
];
|
||||||
rewriteMessageId = true;
|
rewriteMessageId = true;
|
||||||
dkimKeyBits = 1535;
|
dkimKeyBits = 1535;
|
||||||
dmarcReporting = {
|
dmarcReporting.enable = true;
|
||||||
enable = true;
|
|
||||||
domain = "example.com";
|
|
||||||
organizationName = "ACME Corp";
|
|
||||||
};
|
|
||||||
|
|
||||||
loginAccounts = {
|
loginAccounts = {
|
||||||
"user1@example.com" = {
|
"user1@example.com" = {
|
||||||
@@ -70,7 +70,10 @@ pkgs.nixosTest {
|
|||||||
|
|
||||||
extraVirtualAliases = {
|
extraVirtualAliases = {
|
||||||
"single-alias@example.com" = "user1@example.com";
|
"single-alias@example.com" = "user1@example.com";
|
||||||
"multi-alias@example.com" = [ "user1@example.com" "user2@example.com" ];
|
"multi-alias@example.com" = [
|
||||||
|
"user1@example.com"
|
||||||
|
"user2@example.com"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
enableImap = true;
|
enableImap = true;
|
||||||
@@ -79,16 +82,18 @@ pkgs.nixosTest {
|
|||||||
enable = true;
|
enable = true;
|
||||||
autoIndex = true;
|
autoIndex = true;
|
||||||
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201
|
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201
|
||||||
autoIndexExclude = [ (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") ];
|
autoIndexExclude = [
|
||||||
|
(if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk")
|
||||||
|
];
|
||||||
enforced = "yes";
|
enforced = "yes";
|
||||||
# fts-xapian warns when memory is low, which makes the test fail
|
|
||||||
memoryLimit = 100000;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
client = { nodes, config, pkgs, ... }: let
|
client =
|
||||||
serverIP = nodes.server.config.networking.primaryIPAddress;
|
{ nodes, pkgs, ... }:
|
||||||
clientIP = nodes.client.config.networking.primaryIPAddress;
|
let
|
||||||
|
serverIP = nodes.server.networking.primaryIPAddress;
|
||||||
|
clientIP = nodes.client.networking.primaryIPAddress;
|
||||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
||||||
#!${pkgs.stdenv.shell}
|
#!${pkgs.stdenv.shell}
|
||||||
echo grep '${clientIP}' "$@" >&2
|
echo grep '${clientIP}' "$@" >&2
|
||||||
@@ -173,12 +178,21 @@ pkgs.nixosTest {
|
|||||||
assert needle in repr(response)
|
assert needle in repr(response)
|
||||||
imap.close()
|
imap.close()
|
||||||
'';
|
'';
|
||||||
in {
|
in
|
||||||
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./lib/config.nix
|
./lib/config.nix
|
||||||
];
|
];
|
||||||
environment.systemPackages = with pkgs; [
|
environment.systemPackages = with pkgs; [
|
||||||
fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham search
|
fetchmail
|
||||||
|
msmtp
|
||||||
|
procmail
|
||||||
|
findutils
|
||||||
|
grep-ip
|
||||||
|
check-mail-id
|
||||||
|
test-imap-spam
|
||||||
|
test-imap-ham
|
||||||
|
search
|
||||||
];
|
];
|
||||||
environment.etc = {
|
environment.etc = {
|
||||||
"root/.fetchmailrc" = {
|
"root/.fetchmailrc" = {
|
||||||
@@ -272,7 +286,7 @@ pkgs.nixosTest {
|
|||||||
To: Chuck <chuck@example.com>
|
To: Chuck <chuck@example.com>
|
||||||
Cc:
|
Cc:
|
||||||
Bcc:
|
Bcc:
|
||||||
Subject: This is a test Email from postmaster\@example.com to chuck
|
Subject: This is a test Email from postmaster@example.com to chuck
|
||||||
Reply-To:
|
Reply-To:
|
||||||
|
|
||||||
Hello Chuck,
|
Hello Chuck,
|
||||||
@@ -286,7 +300,7 @@ pkgs.nixosTest {
|
|||||||
To: User1 <user1@example.com>
|
To: User1 <user1@example.com>
|
||||||
Cc:
|
Cc:
|
||||||
Bcc:
|
Bcc:
|
||||||
Subject: This is a test Email from single-alias\@example.com to user1
|
Subject: This is a test Email from single-alias@example.com to user1
|
||||||
Reply-To:
|
Reply-To:
|
||||||
|
|
||||||
Hello User1,
|
Hello User1,
|
||||||
@@ -301,7 +315,7 @@ pkgs.nixosTest {
|
|||||||
To: Multi Alias <multi-alias@example.com>
|
To: Multi Alias <multi-alias@example.com>
|
||||||
Cc:
|
Cc:
|
||||||
Bcc:
|
Bcc:
|
||||||
Subject: This is a test Email from user2\@example.com to multi-alias
|
Subject: This is a test Email from user2@example.com to multi-alias
|
||||||
Reply-To:
|
Reply-To:
|
||||||
|
|
||||||
Hello Multi Alias,
|
Hello Multi Alias,
|
||||||
@@ -341,8 +355,7 @@ pkgs.nixosTest {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript = { nodes, ... }:
|
testScript = ''
|
||||||
''
|
|
||||||
start_all()
|
start_all()
|
||||||
|
|
||||||
server.wait_for_unit("multi-user.target")
|
server.wait_for_unit("multi-user.target")
|
||||||
@@ -350,7 +363,7 @@ pkgs.nixosTest {
|
|||||||
|
|
||||||
# TODO put this blocking into the systemd units?
|
# TODO put this blocking into the systemd units?
|
||||||
server.wait_until_succeeds(
|
server.wait_until_succeeds(
|
||||||
"set +e; timeout 1 ${nodes.server.nixpkgs.pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
||||||
)
|
)
|
||||||
|
|
||||||
client.execute("cp -p /etc/root/.* ~/")
|
client.execute("cp -p /etc/root/.* ~/")
|
||||||
@@ -367,7 +380,7 @@ pkgs.nixosTest {
|
|||||||
with subtest("submission port send mail"):
|
with subtest("submission port send mail"):
|
||||||
# send email from user2 to user1
|
# send email from user2 to user1
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"
|
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2"
|
||||||
)
|
)
|
||||||
# give the mail server some time to process the mail
|
# give the mail server some time to process the mail
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
@@ -395,20 +408,20 @@ pkgs.nixosTest {
|
|||||||
client.execute("rm ~/mail/*")
|
client.execute("rm ~/mail/*")
|
||||||
# send email from user2 to user1
|
# send email from user2 to user1
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email2 >&2"
|
"msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email2 >&2"
|
||||||
)
|
)
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
client.succeed("fetchmail --nosslcertck -v")
|
client.succeed("fetchmail --nosslcertck -v")
|
||||||
client.succeed("cat ~/mail/* >&2")
|
client.succeed("cat ~/mail/* >&2")
|
||||||
# make sure it is dkim signed
|
# make sure it is dkim signed
|
||||||
client.succeed("grep DKIM ~/mail/*")
|
client.succeed("grep DKIM-Signature: ~/mail/*")
|
||||||
|
|
||||||
with subtest("aliases"):
|
with subtest("aliases"):
|
||||||
client.execute("rm ~/mail/*")
|
client.execute("rm ~/mail/*")
|
||||||
# send email from chuck to postmaster
|
# send email from chuck to postmaster
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster\@example.com < /etc/root/email2 >&2"
|
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster@example.com < /etc/root/email2 >&2"
|
||||||
)
|
)
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
@@ -418,7 +431,7 @@ pkgs.nixosTest {
|
|||||||
client.execute("rm ~/mail/*")
|
client.execute("rm ~/mail/*")
|
||||||
# send email from chuck to non exsitent account
|
# send email from chuck to non exsitent account
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol\@example.com < /etc/root/email2 >&2"
|
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol@example.com < /etc/root/email2 >&2"
|
||||||
)
|
)
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
@@ -427,7 +440,7 @@ pkgs.nixosTest {
|
|||||||
client.execute("rm ~/mail/*")
|
client.execute("rm ~/mail/*")
|
||||||
# send email from user1 to chuck
|
# send email from user1 to chuck
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck\@example.com < /etc/root/email2 >&2"
|
"msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck@example.com < /etc/root/email2 >&2"
|
||||||
)
|
)
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
# fetchmail returns EXIT_CODE 1 when no new mail
|
# fetchmail returns EXIT_CODE 1 when no new mail
|
||||||
@@ -438,7 +451,7 @@ pkgs.nixosTest {
|
|||||||
client.execute("rm ~/mail/*")
|
client.execute("rm ~/mail/*")
|
||||||
# send email from single-alias to user1
|
# send email from single-alias to user1
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email4 >&2"
|
"msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email4 >&2"
|
||||||
)
|
)
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
@@ -447,7 +460,7 @@ pkgs.nixosTest {
|
|||||||
client.execute("rm ~/mail/*")
|
client.execute("rm ~/mail/*")
|
||||||
# send email from user1 to multi-alias (user{1,2}@example.com)
|
# send email from user1 to multi-alias (user{1,2}@example.com)
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias\@example.com < /etc/root/email5 >&2"
|
"msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias@example.com < /etc/root/email5 >&2"
|
||||||
)
|
)
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
@@ -458,7 +471,7 @@ pkgs.nixosTest {
|
|||||||
client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
|
client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
|
||||||
|
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota\@example.com < /etc/root/email2 >&2"
|
"msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota@example.com < /etc/root/email2 >&2"
|
||||||
)
|
)
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
# fetchmail returns EXIT_CODE 0 when it retrieves mail
|
||||||
@@ -467,23 +480,23 @@ pkgs.nixosTest {
|
|||||||
with subtest("imap sieve junk trainer"):
|
with subtest("imap sieve junk trainer"):
|
||||||
# send email from user2 to user1
|
# send email from user2 to user1
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email1 >&2"
|
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2"
|
||||||
)
|
)
|
||||||
# give the mail server some time to process the mail
|
# give the mail server some time to process the mail
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
|
|
||||||
client.succeed("imap-mark-spam >&2")
|
client.succeed("imap-mark-spam >&2")
|
||||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-spam.sh >&2")
|
server.wait_until_succeeds("journalctl -u dovecot -u dovecot2 | grep -i rspamd-learn-spam.sh >&2")
|
||||||
client.succeed("imap-mark-ham >&2")
|
client.succeed("imap-mark-ham >&2")
|
||||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i sa-learn-ham.sh >&2")
|
server.wait_until_succeeds("journalctl -u dovecot -u dovecot2 | grep -i rspamd-learn-ham.sh >&2")
|
||||||
|
|
||||||
with subtest("full text search and indexation"):
|
with subtest("full text search and indexation"):
|
||||||
# send 2 email from user2 to user1
|
# send 2 email from user2 to user1
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email6 >&2"
|
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email6 >&2"
|
||||||
)
|
)
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1\@example.com < /etc/root/email7 >&2"
|
"msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email7 >&2"
|
||||||
)
|
)
|
||||||
# give the mail server some time to process the mail
|
# give the mail server some time to process the mail
|
||||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||||
@@ -493,23 +506,23 @@ pkgs.nixosTest {
|
|||||||
# should fail because this folder is not indexed
|
# should fail because this folder is not indexed
|
||||||
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 dovecot -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&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 dovecot -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2")
|
||||||
|
|
||||||
with subtest("dmarc reporting"):
|
with subtest("dmarc reporting"):
|
||||||
server.systemctl("start rspamd-dmarc-reporter.service")
|
server.systemctl("start rspamd-dmarc-reporter.service")
|
||||||
server.wait_until_succeeds("journalctl -eu rspamd-dmarc-reporter.service -o cat | grep -q 'No reports for '")
|
|
||||||
|
|
||||||
with subtest("no warnings or errors"):
|
with subtest("no warnings or errors"):
|
||||||
server.fail("journalctl -u postfix | grep -i error >&2")
|
server.fail("journalctl -u postfix | grep -i error >&2")
|
||||||
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 dovecot -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | 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 dovecot -u dovecot2 | \
|
||||||
|
grep -v 'Expunged message reappeared, giving a new UID' | \
|
||||||
|
grep -v 'Time moved forwards' | \
|
||||||
|
grep -i warning >&2"
|
||||||
)
|
)
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,10 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
|
||||||
{ pkgs ? import <nixpkgs> {}, ...}:
|
{
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
sendMail = pkgs.writeTextFile {
|
sendMail = pkgs.writeTextFile {
|
||||||
@@ -27,19 +30,27 @@ let
|
|||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
hashPassword = password: pkgs.runCommand
|
hashPassword =
|
||||||
"password-${password}-hashed"
|
password:
|
||||||
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } ''
|
pkgs.runCommand "password-${password}-hashed"
|
||||||
|
{
|
||||||
|
buildInputs = [ pkgs.mkpasswd ];
|
||||||
|
inherit password;
|
||||||
|
}
|
||||||
|
''
|
||||||
mkpasswd -sm bcrypt <<<"$password" > $out
|
mkpasswd -sm bcrypt <<<"$password" > $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
hashedPasswordFile = hashPassword "my-password";
|
hashedPasswordFile = hashPassword "my-password";
|
||||||
passwordFile = pkgs.writeText "password" "my-password";
|
passwordFile = pkgs.writeText "password" "my-password";
|
||||||
in
|
in
|
||||||
pkgs.nixosTest {
|
{
|
||||||
name = "internal";
|
name = "internal";
|
||||||
|
|
||||||
nodes = {
|
nodes = {
|
||||||
machine = { config, pkgs, ... }: {
|
machine =
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./../default.nix
|
./../default.nix
|
||||||
./lib/config.nix
|
./lib/config.nix
|
||||||
@@ -50,12 +61,21 @@ pkgs.nixosTest {
|
|||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
(pkgs.writeScriptBin "mail-check" ''
|
(pkgs.writeScriptBin "mail-check" ''
|
||||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||||
'')];
|
'')
|
||||||
|
]
|
||||||
|
++ (with pkgs; [
|
||||||
|
curl
|
||||||
|
openssl
|
||||||
|
netcat
|
||||||
|
]);
|
||||||
|
|
||||||
mailserver = {
|
mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
fqdn = "mail.example.com";
|
fqdn = "mail.example.com";
|
||||||
domains = [ "example.com" "domain.com" ];
|
domains = [
|
||||||
|
"example.com"
|
||||||
|
"domain.com"
|
||||||
|
];
|
||||||
localDnsResolver = false;
|
localDnsResolver = false;
|
||||||
|
|
||||||
loginAccounts = {
|
loginAccounts = {
|
||||||
@@ -79,18 +99,24 @@ pkgs.nixosTest {
|
|||||||
|
|
||||||
vmailGroupName = "vmail";
|
vmailGroupName = "vmail";
|
||||||
vmailUID = 5000;
|
vmailUID = 5000;
|
||||||
|
indexDir = "/var/lib/dovecot/indices";
|
||||||
|
|
||||||
enableImap = false;
|
enableImap = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
testScript = ''
|
testScript =
|
||||||
|
{
|
||||||
|
nodes,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
''
|
||||||
machine.start()
|
machine.start()
|
||||||
machine.wait_for_unit("multi-user.target")
|
machine.wait_for_unit("multi-user.target")
|
||||||
|
|
||||||
# Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205
|
# Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205
|
||||||
with subtest("mail forwarded can are locally kept"):
|
with subtest("mail forwarded can are locally kept"):
|
||||||
# A mail sent to user2@example.com is in the user1@example.com mailbox
|
# A mail sent to user2@example.com via explicit TLS is in the user1@example.com mailbox
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
" ".join(
|
" ".join(
|
||||||
[
|
[
|
||||||
@@ -108,13 +134,13 @@ pkgs.nixosTest {
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
# A mail sent to user2@example.com is in the user2@example.com mailbox
|
# A mail sent to user2@example.com via implicit TLS is in the user2@example.com mailbox
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
" ".join(
|
" ".join(
|
||||||
[
|
[
|
||||||
"mail-check send-and-read",
|
"mail-check send-and-read",
|
||||||
"--smtp-port 587",
|
"--smtp-port 465",
|
||||||
"--smtp-starttls",
|
"--smtp-ssl",
|
||||||
"--smtp-host localhost",
|
"--smtp-host localhost",
|
||||||
"--imap-host localhost",
|
"--imap-host localhost",
|
||||||
"--imap-username user2@example.com",
|
"--imap-username user2@example.com",
|
||||||
@@ -128,7 +154,7 @@ pkgs.nixosTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
with subtest("regex email alias are received"):
|
with subtest("regex email alias are received"):
|
||||||
# A mail sent to user2-regex-alias@domain.com is in the user2@example.com mailbox
|
# A mail sent to user2-regex-alias@domain.com via explicit TLS is in the user2@example.com mailbox
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
" ".join(
|
" ".join(
|
||||||
[
|
[
|
||||||
@@ -148,13 +174,14 @@ pkgs.nixosTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
with subtest("user can send from regex email alias"):
|
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
|
# A mail sent to user1@example.com from user2-regex-alias@domain.com by
|
||||||
|
# user2@example.com via implicit TLS is in the user1@example.com mailbox
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
" ".join(
|
" ".join(
|
||||||
[
|
[
|
||||||
"mail-check send-and-read",
|
"mail-check send-and-read",
|
||||||
"--smtp-port 587",
|
"--smtp-port 465",
|
||||||
"--smtp-starttls",
|
"--smtp-ssl",
|
||||||
"--smtp-host localhost",
|
"--smtp-host localhost",
|
||||||
"--imap-host localhost",
|
"--imap-host localhost",
|
||||||
"--smtp-username user2@example.com",
|
"--smtp-username user2@example.com",
|
||||||
@@ -170,26 +197,31 @@ pkgs.nixosTest {
|
|||||||
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")
|
||||||
|
|
||||||
|
with subtest("Check dovecot maildir and index locations"):
|
||||||
|
# If these paths change we need a migration
|
||||||
|
machine.succeed("doveadm user -f home user1@example.com | grep ${nodes.machine.mailserver.mailDirectory}/example.com/user1")
|
||||||
|
machine.succeed("doveadm user -f mail user1@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/example.com/user1'")
|
||||||
|
|
||||||
with subtest("mail to send only accounts is rejected"):
|
with subtest("mail to send only accounts is rejected"):
|
||||||
machine.wait_for_open_port(25)
|
machine.wait_for_open_port(25)
|
||||||
# TODO put this blocking into the systemd units
|
# TODO put this blocking into the systemd units
|
||||||
machine.wait_until_succeeds(
|
machine.wait_until_succeeds(
|
||||||
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 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} | nc localhost 25 | grep -q '554 5.5.0 Error'"
|
||||||
)
|
)
|
||||||
|
|
||||||
with subtest("rspamd controller serves web ui"):
|
with subtest("rspamd controller serves web ui"):
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
"set +o pipefail; ${pkgs.curl}/bin/curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'"
|
"set +o pipefail; curl --unix-socket /run/rspamd/worker-controller.sock http://localhost/ | grep -q '<body>'"
|
||||||
)
|
)
|
||||||
|
|
||||||
with subtest("imap port 143 is closed and imaps is serving SSL"):
|
with subtest("imap port 143 is closed and imaps is serving SSL"):
|
||||||
machine.wait_for_closed_port(143)
|
machine.wait_for_closed_port(143)
|
||||||
machine.wait_for_open_port(993)
|
machine.wait_for_open_port(993)
|
||||||
machine.succeed(
|
machine.succeed(
|
||||||
"echo | ${pkgs.openssl}/bin/openssl s_client -connect localhost:993 | grep 'New, TLS'"
|
"echo | openssl s_client -connect localhost:993 | grep 'New, TLS'"
|
||||||
)
|
)
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
{ pkgs ? import <nixpkgs> {}
|
|
||||||
, ...
|
|
||||||
}:
|
|
||||||
|
|
||||||
let
|
let
|
||||||
bindPassword = "unsafegibberish";
|
bindPassword = "unsafegibberish";
|
||||||
alicePassword = "testalice";
|
alicePassword = "testalice";
|
||||||
bobPassword = "testbob";
|
bobPassword = "testbob";
|
||||||
in
|
in
|
||||||
pkgs.nixosTest {
|
{
|
||||||
name = "ldap";
|
name = "ldap";
|
||||||
|
|
||||||
nodes = {
|
nodes = {
|
||||||
machine = { config, pkgs, ... }: {
|
machine =
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
imports = [
|
imports = [
|
||||||
./../default.nix
|
./../default.nix
|
||||||
./lib/config.nix
|
./lib/config.nix
|
||||||
@@ -20,13 +19,14 @@ pkgs.nixosTest {
|
|||||||
|
|
||||||
services.openssh = {
|
services.openssh = {
|
||||||
enable = true;
|
enable = true;
|
||||||
permitRootLogin = "yes";
|
settings.PermitRootLogin = "yes";
|
||||||
};
|
};
|
||||||
|
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
(pkgs.writeScriptBin "mail-check" ''
|
(pkgs.writeScriptBin "mail-check" ''
|
||||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||||
'')];
|
'')
|
||||||
|
];
|
||||||
|
|
||||||
environment.etc.bind-password.text = bindPassword;
|
environment.etc.bind-password.text = bindPassword;
|
||||||
|
|
||||||
@@ -90,6 +90,7 @@ pkgs.nixosTest {
|
|||||||
fqdn = "mail.example.com";
|
fqdn = "mail.example.com";
|
||||||
domains = [ "example.com" ];
|
domains = [ "example.com" ];
|
||||||
localDnsResolver = false;
|
localDnsResolver = false;
|
||||||
|
indexDir = "/var/lib/dovecot/indices";
|
||||||
|
|
||||||
ldap = {
|
ldap = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -104,6 +105,10 @@ pkgs.nixosTest {
|
|||||||
searchScope = "sub";
|
searchScope = "sub";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
forwards = {
|
||||||
|
"bob_fw@example.com" = "bob@example.com";
|
||||||
|
};
|
||||||
|
|
||||||
vmailGroupName = "vmail";
|
vmailGroupName = "vmail";
|
||||||
vmailUID = 5000;
|
vmailUID = 5000;
|
||||||
|
|
||||||
@@ -111,7 +116,12 @@ pkgs.nixosTest {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
testScript = ''
|
testScript =
|
||||||
|
{
|
||||||
|
nodes,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
''
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
|
|
||||||
@@ -147,7 +157,7 @@ pkgs.nixosTest {
|
|||||||
machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root 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'")
|
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'")
|
||||||
|
|
||||||
with subtest("Test account/mail address binding"):
|
with subtest("Test account/mail address binding via explicit TLS"):
|
||||||
machine.fail(" ".join([
|
machine.fail(" ".join([
|
||||||
"mail-check send-and-read",
|
"mail-check send-and-read",
|
||||||
"--smtp-port 587",
|
"--smtp-port 587",
|
||||||
@@ -164,11 +174,11 @@ pkgs.nixosTest {
|
|||||||
]))
|
]))
|
||||||
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'")
|
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user alice@example.com'")
|
||||||
|
|
||||||
with subtest("Test mail delivery"):
|
with subtest("Test mail delivery via implicit TLS"):
|
||||||
machine.succeed(" ".join([
|
machine.succeed(" ".join([
|
||||||
"mail-check send-and-read",
|
"mail-check send-and-read",
|
||||||
"--smtp-port 587",
|
"--smtp-port 465",
|
||||||
"--smtp-starttls",
|
"--smtp-ssl",
|
||||||
"--smtp-host localhost",
|
"--smtp-host localhost",
|
||||||
"--smtp-username alice@example.com",
|
"--smtp-username alice@example.com",
|
||||||
"--imap-host localhost",
|
"--imap-host localhost",
|
||||||
@@ -179,5 +189,43 @@ pkgs.nixosTest {
|
|||||||
"--dst-password-file <(echo '${bobPassword}')",
|
"--dst-password-file <(echo '${bobPassword}')",
|
||||||
"--ignore-dkim-spf"
|
"--ignore-dkim-spf"
|
||||||
]))
|
]))
|
||||||
|
|
||||||
|
with subtest("Test mail forwarding via explicit TLS works"):
|
||||||
|
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_fw@example.com",
|
||||||
|
"--src-password-file <(echo '${alicePassword}')",
|
||||||
|
"--dst-password-file <(echo '${bobPassword}')",
|
||||||
|
"--ignore-dkim-spf"
|
||||||
|
]))
|
||||||
|
|
||||||
|
with subtest("Test cannot send mail via implicit TLS from forwarded address"):
|
||||||
|
machine.fail(" ".join([
|
||||||
|
"mail-check send-and-read",
|
||||||
|
"--smtp-port 465",
|
||||||
|
"--smtp-ssl",
|
||||||
|
"--smtp-host localhost",
|
||||||
|
"--smtp-username bob@example.com",
|
||||||
|
"--imap-host localhost",
|
||||||
|
"--imap-username alice@example.com",
|
||||||
|
"--from-addr bob_fw@example.com",
|
||||||
|
"--to-addr alice@example.com",
|
||||||
|
"--src-password-file <(echo '${bobPassword}')",
|
||||||
|
"--dst-password-file <(echo '${alicePassword}')",
|
||||||
|
"--ignore-dkim-spf"
|
||||||
|
]))
|
||||||
|
machine.succeed("journalctl -u postfix | grep -q 'Sender address rejected: not owned by user bob@example.com'")
|
||||||
|
|
||||||
|
with subtest("Check dovecot mail and index locations"):
|
||||||
|
# If these paths change we need a migration
|
||||||
|
machine.succeed("doveadm user -f home bob@example.com | grep ${nodes.machine.mailserver.mailDirectory}/ldap/bob@example.com")
|
||||||
|
machine.succeed("doveadm user -f mail bob@example.com | grep 'maildir:~/mail:INDEX=${nodes.machine.mailserver.indexDir}/ldap/bob@example.com'")
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,28 @@
|
|||||||
{
|
{
|
||||||
security.dhparams.defaultBitSize = 1024; # minimum size required by dovecot
|
lib,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
{
|
||||||
|
# Testing eval failures that result from stateVersion assertion is out of scope
|
||||||
|
mailserver.stateVersion = 999;
|
||||||
|
|
||||||
|
# Keep testing submission with explicit TLS
|
||||||
|
mailserver.enableSubmission = true;
|
||||||
|
|
||||||
|
# Enable second CPU core
|
||||||
|
virtualisation.cores = lib.mkDefault 2;
|
||||||
|
|
||||||
|
services.rspamd = {
|
||||||
|
# Don't make tests block on DNS requests that will never succeed
|
||||||
|
locals."options.inc".text = ''
|
||||||
|
dns {
|
||||||
|
nameservers = ["127.0.0.1"];
|
||||||
|
timeout = 0.0s;
|
||||||
|
retransmits = 0;
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
# Relax `local_addrs` definition to default for tests, so mail doesn't get flagged as spam
|
||||||
|
overrides."options.inc".enable = false;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
# nixos-mailserver: a simple mail server
|
|
||||||
# Copyright (C) 2016-2018 Robin Raymond
|
|
||||||
#
|
|
||||||
# This program is free software: you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation, either version 3 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
|
|
||||||
import <nixpkgs/nixos/tests/make-test-python.nix> {
|
|
||||||
|
|
||||||
nodes.machine =
|
|
||||||
{ config, pkgs, ... }:
|
|
||||||
{
|
|
||||||
imports = [
|
|
||||||
./../default.nix
|
|
||||||
];
|
|
||||||
};
|
|
||||||
|
|
||||||
testScript =
|
|
||||||
''
|
|
||||||
machine.wait_for_unit("multi-user.target");
|
|
||||||
'';
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,34 @@
|
|||||||
# This tests is used to test features requiring several mail domains.
|
# This tests is used to test features requiring several mail domains.
|
||||||
|
|
||||||
{ pkgs ? import <nixpkgs> {}, ...}:
|
{
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
let
|
let
|
||||||
hashPassword = password: pkgs.runCommand
|
hashPassword =
|
||||||
"password-${password}-hashed"
|
password:
|
||||||
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; }
|
pkgs.runCommand "password-${password}-hashed"
|
||||||
|
{
|
||||||
|
buildInputs = [ pkgs.mkpasswd ];
|
||||||
|
inherit password;
|
||||||
|
}
|
||||||
''
|
''
|
||||||
mkpasswd -sm bcrypt <<<"$password" > $out
|
mkpasswd -sm bcrypt <<<"$password" > $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
password = pkgs.writeText "password" "password";
|
password = pkgs.writeText "password" "password";
|
||||||
|
|
||||||
domainGenerator = domain: { config, pkgs, ... }: {
|
domainGenerator =
|
||||||
imports = [../default.nix];
|
domain:
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
|
imports = [
|
||||||
|
../default.nix
|
||||||
|
./lib/config.nix
|
||||||
|
];
|
||||||
|
environment.systemPackages = with pkgs; [ netcat ];
|
||||||
virtualisation.memorySize = 1024;
|
virtualisation.memorySize = 1024;
|
||||||
mailserver = {
|
mailserver = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -30,36 +45,49 @@ let
|
|||||||
};
|
};
|
||||||
services.dnsmasq = {
|
services.dnsmasq = {
|
||||||
enable = true;
|
enable = true;
|
||||||
# Fixme: once nixos-22.11 has been removed, could be replaced by
|
settings.mx-host = [
|
||||||
# settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
|
"domain1.com,domain1,10"
|
||||||
extraConfig = ''
|
"domain2.com,domain2,10"
|
||||||
mx-host=domain1.com,domain1,10
|
];
|
||||||
mx-host=domain2.com,domain2,10
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# breaks the test, due to running into DNS timeouts
|
||||||
|
services.postfix-tlspol.configurePostfix = lib.mkForce false;
|
||||||
};
|
};
|
||||||
|
|
||||||
in
|
in
|
||||||
|
|
||||||
pkgs.nixosTest {
|
{
|
||||||
name = "multiple";
|
name = "multiple";
|
||||||
|
|
||||||
nodes = {
|
nodes = {
|
||||||
domain1 = {...}: {
|
domain1 =
|
||||||
|
{ ... }:
|
||||||
|
{
|
||||||
imports = [
|
imports = [
|
||||||
../default.nix
|
../default.nix
|
||||||
(domainGenerator "domain1.com")
|
(domainGenerator "domain1.com")
|
||||||
];
|
];
|
||||||
mailserver.forwards = {
|
mailserver.forwards = {
|
||||||
"non-local@domain1.com" = ["user@domain2.com" "user@domain1.com"];
|
"non-local@domain1.com" = [
|
||||||
"non@domain1.com" = ["user@domain2.com" "user@domain1.com"];
|
"user@domain2.com"
|
||||||
|
"user@domain1.com"
|
||||||
|
];
|
||||||
|
"non@domain1.com" = [
|
||||||
|
"user@domain2.com"
|
||||||
|
"user@domain1.com"
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
domain2 = domainGenerator "domain2.com";
|
domain2 = domainGenerator "domain2.com";
|
||||||
client = { config, pkgs, ... }: {
|
client =
|
||||||
|
{ pkgs, ... }:
|
||||||
|
{
|
||||||
environment.systemPackages = [
|
environment.systemPackages = [
|
||||||
(pkgs.writeScriptBin "mail-check" ''
|
(pkgs.writeScriptBin "mail-check" ''
|
||||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||||
'')];
|
'')
|
||||||
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
testScript = ''
|
testScript = ''
|
||||||
@@ -70,20 +98,20 @@ pkgs.nixosTest {
|
|||||||
|
|
||||||
# TODO put this blocking into the systemd units?
|
# TODO put this blocking into the systemd units?
|
||||||
domain1.wait_until_succeeds(
|
domain1.wait_until_succeeds(
|
||||||
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
||||||
)
|
)
|
||||||
domain2.wait_until_succeeds(
|
domain2.wait_until_succeeds(
|
||||||
"set +e; timeout 1 ${pkgs.netcat}/bin/nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
"set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
|
||||||
)
|
)
|
||||||
|
|
||||||
# user@domain1.com sends a mail to user@domain2.com
|
# user@domain1.com sends a mail to user@domain2.com via explicit TLS
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
|
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Send a mail to the address forwarded and check it is in the recipient mailbox
|
# Send a mail to the address forwarded via implicit TLS and check it is in the recipient mailbox
|
||||||
client.succeed(
|
client.succeed(
|
||||||
"mail-check send-and-read --smtp-port 587 --smtp-starttls --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr non-local@domain1.com --imap-username user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
|
"mail-check send-and-read --smtp-port 465 --smtp-ssl --smtp-host domain1 --from-addr user@domain1.com --imap-host domain2 --to-addr non-local@domain1.com --imap-username user@domain2.com --src-password-file ${password} --dst-password-file ${password} --ignore-dkim-spf"
|
||||||
)
|
)
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
sed -i -e "s/v[0-9]\+\.[0-9]\+\.[0-9]\+/$1/g" README.md
|
|
||||||
|
|
||||||
HASH=$(nix-prefetch-url "https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/archive/v2.3.0/nixos-mailserver-$1.tar.gz" --unpack)
|
|
||||||
|
|
||||||
sed -i -e "s/sha256 = \"[0-9a-z]\{52\}\"/sha256 = \"$HASH\"/g" README.md
|
|
||||||
Reference in New Issue
Block a user