Compare commits
148 Commits
havefun-25
...
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 | ||
|
|
8970ed0849 |
@@ -1,11 +1,11 @@
|
||||
{ nixpkgs, pulls, ... }:
|
||||
|
||||
let
|
||||
pkgs = import nixpkgs {};
|
||||
pkgs = import nixpkgs { };
|
||||
|
||||
prs = builtins.fromJSON (builtins.readFile pulls);
|
||||
prJobsets = pkgs.lib.mapAttrs (num: info:
|
||||
{ enabled = 1;
|
||||
prJobsets = pkgs.lib.mapAttrs (num: info: {
|
||||
enabled = 1;
|
||||
hidden = false;
|
||||
description = "PR ${num}: ${info.title}";
|
||||
checkinterval = 300;
|
||||
@@ -15,8 +15,7 @@ let
|
||||
keepnr = 1;
|
||||
type = 1;
|
||||
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
|
||||
}
|
||||
) prs;
|
||||
}) prs;
|
||||
mkFlakeJobset = branch: {
|
||||
description = "Build ${branch} branch of Simple NixOS MailServer";
|
||||
checkinterval = 300;
|
||||
@@ -32,8 +31,8 @@ let
|
||||
|
||||
desc = prJobsets // {
|
||||
"master" = mkFlakeJobset "master";
|
||||
"nixos-24.11" = mkFlakeJobset "nixos-24.11";
|
||||
"nixos-25.05" = mkFlakeJobset "nixos-25.05";
|
||||
"nixos-25.11" = mkFlakeJobset "nixos-25.11";
|
||||
};
|
||||
|
||||
log = {
|
||||
@@ -41,13 +40,14 @@ let
|
||||
jobsets = desc;
|
||||
};
|
||||
|
||||
in {
|
||||
jobsets = pkgs.runCommand "spec-jobsets.json" {} ''
|
||||
cat >$out <<EOF
|
||||
in
|
||||
{
|
||||
jobsets = pkgs.runCommand "spec-jobsets.json" { } ''
|
||||
cat >$out <<'EOF'
|
||||
${builtins.toJSON desc}
|
||||
EOF
|
||||
# This is to get nice .jobsets build logs on Hydra
|
||||
cat >tmp <<EOF
|
||||
cat >tmp <<'EOF'
|
||||
${builtins.toJSON log}
|
||||
EOF
|
||||
${pkgs.jq}/bin/jq . tmp
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
python: "3"
|
||||
apt_packages:
|
||||
- nix
|
||||
- curl
|
||||
- proot
|
||||
jobs:
|
||||
pre_install:
|
||||
- mkdir -p ~/.nix ~/.config/nix
|
||||
- echo "experimental-features = nix-command flakes" > ~/.config/nix/nix.conf
|
||||
- proot -b ~/.nix:/nix /bin/sh -c "nix build -L .#optionsDoc && cp -v result docs/options.md"
|
||||
- curl -L https://github.com/DavHau/nix-portable/releases/latest/download/nix-portable-$(uname -m) > ./nix-portable
|
||||
- chmod +x ./nix-portable
|
||||
- ./nix-portable nix build --print-build-logs .#optionsDoc
|
||||
- ./nix-portable nix store cat $(readlink result) > docs/options.md
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
13
README.md
13
README.md
@@ -8,14 +8,14 @@
|
||||
For each NixOS release, we publish a branch. You then have to use the
|
||||
SNM branch corresponding to your NixOS version.
|
||||
|
||||
* For NixOS 25.11
|
||||
* 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-25.11/)
|
||||
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.11/release-notes.html#nixos-25-11)
|
||||
* For NixOS 25.05
|
||||
* 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-25.05/)
|
||||
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-25.05/release-notes.html#nixos-25-05)
|
||||
* For NixOS 24.11
|
||||
* Use the [SNM branch `nixos-24.11`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/nixos-24.11)
|
||||
* [Documentation](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/)
|
||||
* [Release notes](https://nixos-mailserver.readthedocs.io/en/nixos-24.11/release-notes.html#nixos-24-11)
|
||||
* For NixOS unstable
|
||||
* Use the [SNM branch `master`](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/tree/master)
|
||||
* [Documentation](https://nixos-mailserver.readthedocs.io/en/latest/)
|
||||
@@ -29,6 +29,8 @@ SNM branch corresponding to your NixOS version.
|
||||
* [x] Submission TLS on port 465
|
||||
* [x] Submission StartTLS on port 587
|
||||
* [x] LMTP with Dovecot
|
||||
* [x] DANE and MTA-STS validation
|
||||
* [x] SMTP TLS Reports ([RFC 8460](https://www.rfc-editor.org/rfc/rfc8460))
|
||||
* Dovecot
|
||||
* [x] Maildir folders
|
||||
* [x] IMAP with TLS on port 993
|
||||
@@ -55,6 +57,8 @@ SNM branch corresponding to your NixOS version.
|
||||
* User Aliases
|
||||
* [x] Regular aliases
|
||||
* [x] Catch all aliases
|
||||
* Improve the Forwarding Experience
|
||||
* [x] [Sender Rewriting Scheme](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme)
|
||||
|
||||
### In the future
|
||||
|
||||
@@ -67,7 +71,6 @@ SNM branch corresponding to your NixOS version.
|
||||
* [ ] 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)
|
||||
* [ ] Support [SRS](https://en.wikipedia.org/wiki/Sender_Rewriting_Scheme) with [postsrsd](https://github.com/roehling/postsrsd)
|
||||
* User management
|
||||
* [ ] Allow local and LDAP user to coexist
|
||||
* OpenID Connect
|
||||
|
||||
438
default.nix
438
default.nix
@@ -14,17 +14,64 @@
|
||||
# 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;
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
literalExpression
|
||||
literalMD
|
||||
mkDefault
|
||||
mkEnableOption
|
||||
mkOption
|
||||
mkOptionType
|
||||
mkRemovedOptionModule
|
||||
mkRenamedOptionModule
|
||||
types
|
||||
warn
|
||||
;
|
||||
|
||||
cfg = config.mailserver;
|
||||
in
|
||||
{
|
||||
options.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 {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
@@ -37,17 +84,60 @@ in
|
||||
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 {
|
||||
type = types.listOf types.str;
|
||||
example = [ "example.com" ];
|
||||
default = [];
|
||||
default = [ ];
|
||||
description = "The domains that this mail server serves.";
|
||||
};
|
||||
|
||||
certificateDomains = mkOption {
|
||||
type = types.listOf types.str;
|
||||
example = [ "imap.example.com" "pop3.example.com" ];
|
||||
default = [];
|
||||
example = [
|
||||
"imap.example.com"
|
||||
"pop3.example.com"
|
||||
];
|
||||
default = [ ];
|
||||
description = ''
|
||||
({option}`mailserver.certificateScheme` == `acme-nginx`)
|
||||
|
||||
@@ -63,7 +153,10 @@ in
|
||||
};
|
||||
|
||||
loginAccounts = mkOption {
|
||||
type = types.attrsOf (types.submodule ({ name, ... }: {
|
||||
type = types.attrsOf (
|
||||
types.submodule (
|
||||
{ name, ... }:
|
||||
{
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
@@ -102,8 +195,11 @@ in
|
||||
|
||||
aliases = mkOption {
|
||||
type = with types; listOf types.str;
|
||||
example = ["abuse@example.com" "postmaster@example.com"];
|
||||
default = [];
|
||||
example = [
|
||||
"abuse@example.com"
|
||||
"postmaster@example.com"
|
||||
];
|
||||
default = [ ];
|
||||
description = ''
|
||||
A list of aliases of this login account.
|
||||
Note: Use list entries like "@example.com" to create a catchAll
|
||||
@@ -113,8 +209,8 @@ in
|
||||
|
||||
aliasesRegexp = mkOption {
|
||||
type = with types; listOf types.str;
|
||||
example = [''/^tom\..*@domain\.com$/''];
|
||||
default = [];
|
||||
example = [ ''/^tom\..*@domain\.com$/'' ];
|
||||
default = [ ];
|
||||
description = ''
|
||||
Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex).
|
||||
'';
|
||||
@@ -122,8 +218,11 @@ in
|
||||
|
||||
catchAll = mkOption {
|
||||
type = with types; listOf (enum cfg.domains);
|
||||
example = ["example.com" "example2.com"];
|
||||
default = [];
|
||||
example = [
|
||||
"example.com"
|
||||
"example2.com"
|
||||
];
|
||||
default = [ ];
|
||||
description = ''
|
||||
For which domains should this account act as a catch all?
|
||||
Note: Does not allow sending from all addresses of these domains.
|
||||
@@ -186,7 +285,9 @@ in
|
||||
};
|
||||
|
||||
config.name = mkDefault name;
|
||||
}));
|
||||
}
|
||||
)
|
||||
);
|
||||
example = {
|
||||
user1 = {
|
||||
hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
|
||||
@@ -204,7 +305,7 @@ in
|
||||
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
|
||||
```
|
||||
'';
|
||||
default = {};
|
||||
default = { };
|
||||
};
|
||||
|
||||
ldap = {
|
||||
@@ -234,7 +335,7 @@ in
|
||||
tlsCAFile = mkOption {
|
||||
type = types.path;
|
||||
default = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
|
||||
defaultText = lib.literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
|
||||
defaultText = literalMD "see [source](https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/blob/master/default.nix)";
|
||||
description = ''
|
||||
Certifificate trust anchors used to verify the LDAP server certificate.
|
||||
'';
|
||||
@@ -268,7 +369,11 @@ in
|
||||
};
|
||||
|
||||
searchScope = mkOption {
|
||||
type = types.enum [ "sub" "base" "one" ];
|
||||
type = types.enum [
|
||||
"sub"
|
||||
"base"
|
||||
"one"
|
||||
];
|
||||
default = "sub";
|
||||
description = ''
|
||||
Search scope below which users accounts are looked for.
|
||||
@@ -283,7 +388,7 @@ in
|
||||
LDAP attributes to be retrieved during userdb lookups.
|
||||
|
||||
See the users_attrs reference at
|
||||
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#user-attrs
|
||||
https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#user-attrs
|
||||
in the Dovecot manual.
|
||||
'';
|
||||
};
|
||||
@@ -296,7 +401,7 @@ in
|
||||
Filter for user lookups in Dovecot.
|
||||
|
||||
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.
|
||||
'';
|
||||
};
|
||||
@@ -308,7 +413,7 @@ in
|
||||
LDAP attributes to be retrieved during passdb lookups.
|
||||
|
||||
See the pass_attrs reference at
|
||||
https://doc.dovecot.org/configuration_manual/authentication/ldap_settings_auth/#pass-attrs
|
||||
https://doc.dovecot.org/2.3/configuration_manual/authentication/ldap_settings_auth/#pass-attrs
|
||||
in the Dovecot manual.
|
||||
'';
|
||||
};
|
||||
@@ -321,7 +426,7 @@ in
|
||||
Filter for password lookups in Dovecot.
|
||||
|
||||
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.
|
||||
'';
|
||||
};
|
||||
@@ -373,7 +478,7 @@ in
|
||||
to resynchronize).
|
||||
|
||||
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.
|
||||
'';
|
||||
example = "/var/lib/dovecot/indices";
|
||||
@@ -403,14 +508,22 @@ in
|
||||
autoIndexExclude = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [ ];
|
||||
example = [ "\\Trash" "SomeFolder" "Other/*" ];
|
||||
example = [
|
||||
"\\Trash"
|
||||
"SomeFolder"
|
||||
"Other/*"
|
||||
];
|
||||
description = ''
|
||||
Mailboxes to exclude from automatic indexing.
|
||||
'';
|
||||
};
|
||||
|
||||
enforced = mkOption {
|
||||
type = types.enum [ "yes" "no" "body" ];
|
||||
type = types.enum [
|
||||
"yes"
|
||||
"no"
|
||||
"body"
|
||||
];
|
||||
default = "no";
|
||||
description = ''
|
||||
Fail searches when no index is available. If set to
|
||||
@@ -423,7 +536,10 @@ in
|
||||
languages = mkOption {
|
||||
type = types.nonEmptyListOf types.str;
|
||||
default = [ "en" ];
|
||||
example = [ "en" "de" ];
|
||||
example = [
|
||||
"en"
|
||||
"de"
|
||||
];
|
||||
description = ''
|
||||
A list of languages that the full text search should detect.
|
||||
At least one language must be specified.
|
||||
@@ -472,7 +588,10 @@ in
|
||||
};
|
||||
|
||||
lmtpSaveToDetailMailbox = mkOption {
|
||||
type = types.enum ["yes" "no"];
|
||||
type = types.enum [
|
||||
"yes"
|
||||
"no"
|
||||
];
|
||||
default = "yes";
|
||||
description = ''
|
||||
If an email address is delimited by a "+", should it be filed into a
|
||||
@@ -498,17 +617,23 @@ in
|
||||
};
|
||||
|
||||
extraVirtualAliases = mkOption {
|
||||
type = let
|
||||
type =
|
||||
let
|
||||
loginAccount = mkOptionType {
|
||||
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 = {
|
||||
"info@example.com" = "user1@example.com";
|
||||
"postmaster@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 = ''
|
||||
Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that
|
||||
@@ -521,7 +646,7 @@ in
|
||||
example all mails for `multi@example.com` will be forwarded to both
|
||||
`user1@example.com` and `user2@example.com`.
|
||||
'';
|
||||
default = {};
|
||||
default = { };
|
||||
};
|
||||
|
||||
forwards = mkOption {
|
||||
@@ -538,28 +663,34 @@ in
|
||||
can't send mail as `user@example.com`. Also, this option
|
||||
allows to forward mails to external addresses.
|
||||
'';
|
||||
default = {};
|
||||
default = { };
|
||||
};
|
||||
|
||||
rejectSender = mkOption {
|
||||
type = types.listOf types.str;
|
||||
example = [ "example.com" "spammer@example.net" ];
|
||||
example = [
|
||||
"example.com"
|
||||
"spammer@example.net"
|
||||
];
|
||||
description = ''
|
||||
Reject emails from these addresses from unauthorized senders.
|
||||
Use if a spammer is using the same domain or the same sender over and over.
|
||||
'';
|
||||
default = [];
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
rejectRecipients = mkOption {
|
||||
type = types.listOf types.str;
|
||||
example = [ "sales@example.com" "info@example.com" ];
|
||||
example = [
|
||||
"sales@example.com"
|
||||
"info@example.com"
|
||||
];
|
||||
description = ''
|
||||
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
|
||||
not want to disable the catchall.
|
||||
'';
|
||||
default = [];
|
||||
default = [ ];
|
||||
};
|
||||
|
||||
vmailUID = mkOption {
|
||||
@@ -657,12 +788,30 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
certificateScheme = let
|
||||
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))}\"'."
|
||||
certificateScheme =
|
||||
let
|
||||
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));
|
||||
in mkOption {
|
||||
type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes);
|
||||
in
|
||||
mkOption {
|
||||
type =
|
||||
with types;
|
||||
coercedTo (enum [
|
||||
1
|
||||
2
|
||||
3
|
||||
]) translate (enum schemes);
|
||||
default = "selfsigned";
|
||||
description = ''
|
||||
The scheme to use for managing TLS certificates:
|
||||
@@ -715,6 +864,7 @@ in
|
||||
acmeCertificateName = mkOption {
|
||||
type = types.str;
|
||||
default = cfg.fqdn;
|
||||
defaultText = literalExpression "config.mailserver.fqdn";
|
||||
example = "example.com";
|
||||
description = ''
|
||||
({option}`mailserver.certificateScheme` == `acme`)
|
||||
@@ -727,9 +877,11 @@ in
|
||||
|
||||
enableImap = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to enable IMAP with STARTTLS on port 143.
|
||||
|
||||
The use of this port is deprecated per RFC 8314 4.1.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -751,9 +903,11 @@ in
|
||||
|
||||
enableSubmission = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
default = false;
|
||||
description = ''
|
||||
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.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -770,6 +924,8 @@ in
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to enable POP3 with STARTTLS on port on port 110.
|
||||
|
||||
The use of this port is deprecated per RFC 8314 4.1.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -835,7 +991,10 @@ in
|
||||
};
|
||||
|
||||
dkimKeyType = mkOption {
|
||||
type = types.enum [ "rsa" "ed25519" ];
|
||||
type = types.enum [
|
||||
"rsa"
|
||||
"ed25519"
|
||||
];
|
||||
default = "rsa";
|
||||
description = ''
|
||||
The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018).
|
||||
@@ -849,9 +1008,9 @@ in
|
||||
|
||||
dkimKeyBits = mkOption {
|
||||
type = types.int;
|
||||
default = 1024;
|
||||
default = 2048;
|
||||
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
|
||||
here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
|
||||
@@ -875,70 +1034,47 @@ in
|
||||
'';
|
||||
};
|
||||
|
||||
localpart = mkOption {
|
||||
type = types.str;
|
||||
default = "dmarc-noreply";
|
||||
example = "dmarc-report";
|
||||
description = ''
|
||||
The local part of the email address used for outgoing DMARC reports.
|
||||
'';
|
||||
};
|
||||
|
||||
domain = mkOption {
|
||||
type = types.enum (cfg.domains);
|
||||
example = "example.com";
|
||||
description = ''
|
||||
The domain from which outgoing DMARC reports are served.
|
||||
'';
|
||||
};
|
||||
|
||||
email = mkOption {
|
||||
type = types.str;
|
||||
default = with cfg.dmarcReporting; "${localpart}@${domain}";
|
||||
defaultText = literalExpression ''"''${localpart}@''${domain}"'';
|
||||
readOnly = true;
|
||||
description = ''
|
||||
The email address used for outgoing DMARC reports. Read-only.
|
||||
'';
|
||||
};
|
||||
|
||||
organizationName = mkOption {
|
||||
type = types.str;
|
||||
example = "ACME Corp.";
|
||||
description = ''
|
||||
The name of your organization used in the `org_name` attribute in
|
||||
DMARC reports.
|
||||
'';
|
||||
};
|
||||
|
||||
fromName = mkOption {
|
||||
type = types.str;
|
||||
default = cfg.dmarcReporting.organizationName;
|
||||
defaultText = literalMD "{option}`mailserver.dmarcReporting.organizationName`";
|
||||
description = ''
|
||||
The sender name for DMARC reports. Defaults to the organization name.
|
||||
'';
|
||||
};
|
||||
|
||||
excludeDomains = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
default = [ ];
|
||||
description = ''
|
||||
List of domains or eSLDs to be excluded from DMARC reports.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
debug = mkOption {
|
||||
tlsrpt.enable = mkEnableOption "delivery of SMTP TLS reports according to RFC 8460";
|
||||
|
||||
debug = {
|
||||
all = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = ''
|
||||
Whether to enable verbose logging for mailserver related services. This
|
||||
intended be used for development purposes only, you probably don't want
|
||||
to enable this unless you're hacking on nixos-mailserver.
|
||||
Whether to enable verbose logging for all mailserver related services.
|
||||
This intended be used for development purposes only, you probably
|
||||
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 {
|
||||
type = types.int;
|
||||
default = 100;
|
||||
@@ -965,12 +1101,42 @@ 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 = {
|
||||
configureLocally = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Whether to provision a local Redis instance.
|
||||
'';
|
||||
};
|
||||
|
||||
address = mkOption {
|
||||
type = types.str;
|
||||
# read the default from nixos' redis module
|
||||
default = config.services.redis.servers.rspamd.unixSocket;
|
||||
defaultText = lib.literalExpression "config.services.redis.servers.rspamd.unixSocket";
|
||||
defaultText = literalExpression "config.services.redis.servers.rspamd.unixSocket";
|
||||
description = ''
|
||||
Path, IP address or hostname that Rspamd should use to contact Redis.
|
||||
'';
|
||||
@@ -979,7 +1145,7 @@ in
|
||||
port = mkOption {
|
||||
type = with types; nullOr port;
|
||||
default = null;
|
||||
example = lib.literalExpression "config.services.redis.servers.rspamd.port";
|
||||
example = literalExpression "config.services.redis.servers.rspamd.port";
|
||||
description = ''
|
||||
Port that Rspamd should use to contact Redis.
|
||||
'';
|
||||
@@ -988,7 +1154,7 @@ in
|
||||
password = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
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 = ''
|
||||
Password that rspamd should use to contact redis, or null if not required.
|
||||
'';
|
||||
@@ -1005,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 {
|
||||
type = types.str;
|
||||
default = cfg.fqdn;
|
||||
defaultText = lib.literalMD "{option}`mailserver.fqdn`";
|
||||
defaultText = literalMD "{option}`mailserver.fqdn`";
|
||||
example = "myserver.example.com";
|
||||
description = ''
|
||||
The fully qualified domain name of the mail server used to
|
||||
@@ -1099,7 +1250,7 @@ in
|
||||
start program = "${pkgs.systemd}/bin/systemctl start 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 = ''
|
||||
The configuration used for monitoring via monit.
|
||||
Use a mail address that you actively check and set it via 'set alert ...'.
|
||||
@@ -1141,7 +1292,15 @@ in
|
||||
|
||||
compression = {
|
||||
method = mkOption {
|
||||
type = types.nullOr (types.enum ["none" "lz4" "zstd" "zlib" "lzma"]);
|
||||
type = types.nullOr (
|
||||
types.enum [
|
||||
"none"
|
||||
"lz4"
|
||||
"zstd"
|
||||
"zlib"
|
||||
"lzma"
|
||||
]
|
||||
);
|
||||
default = null;
|
||||
description = "Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4.";
|
||||
};
|
||||
@@ -1199,14 +1358,14 @@ in
|
||||
|
||||
locations = mkOption {
|
||||
type = types.listOf types.path;
|
||||
default = [cfg.mailDirectory];
|
||||
defaultText = lib.literalExpression "[ config.mailserver.mailDirectory ]";
|
||||
default = [ cfg.mailDirectory ];
|
||||
defaultText = literalExpression "[ config.mailserver.mailDirectory ]";
|
||||
description = "The locations that are to be backed up by borg.";
|
||||
};
|
||||
|
||||
extraArgumentsForInit = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = ["--critical"];
|
||||
default = [ "--critical" ];
|
||||
description = "Additional arguments to add to the borg init command line.";
|
||||
};
|
||||
|
||||
@@ -1301,29 +1460,29 @@ in
|
||||
};
|
||||
|
||||
imports = [
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] ''
|
||||
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] ''
|
||||
This option is not needed for fts-flatcurve
|
||||
'')
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] ''
|
||||
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] ''
|
||||
This option is not needed for fts-flatcurve
|
||||
'')
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] ''
|
||||
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] ''
|
||||
This option is not needed for fts-flatcurve
|
||||
'')
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] ''
|
||||
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] ''
|
||||
This option is not supported by fts-flatcurve
|
||||
'')
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] ''
|
||||
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] ''
|
||||
This option is not needed since fts-xapian 1.8.3
|
||||
'')
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] ''
|
||||
(mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] ''
|
||||
Text attachments are always indexed since fts-xapian 1.4.8
|
||||
'')
|
||||
(lib.mkRenamedOptionModule
|
||||
(mkRenamedOptionModule
|
||||
[ "mailserver" "rebootAfterKernelUpgrade" "enable" ]
|
||||
[ "system" "autoUpgrade" "allowReboot" ]
|
||||
)
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "method" ] ''
|
||||
(mkRemovedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "method" ] ''
|
||||
Use `system.autoUpgrade` instead.
|
||||
'')
|
||||
./mail-server/assertions.nix
|
||||
@@ -1340,15 +1499,32 @@ in
|
||||
./mail-server/rspamd.nix
|
||||
./mail-server/nginx.nix
|
||||
./mail-server/kresd.nix
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "policydSPFExtraConfig" ] ''
|
||||
(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.
|
||||
'')
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "dkimHeaderCanonicalization" ] ''
|
||||
(mkRemovedOptionModule [ "mailserver" "dkimHeaderCanonicalization" ] ''
|
||||
DKIM signing has been migrated to Rspamd, which always uses relaxed canonicalization.
|
||||
'')
|
||||
(lib.mkRemovedOptionModule [ "mailserver" "dkimBodyCanonicalization" ] ''
|
||||
(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`.
|
||||
'')
|
||||
];
|
||||
}
|
||||
|
||||
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
|
||||
``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
|
||||
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
|
||||
|
||||
@@ -64,3 +64,44 @@ To build the documentation, you need to enable `Nix Flakes
|
||||
|
||||
$ nix build .#documentation
|
||||
$ xdg-open result/index.html
|
||||
|
||||
|
||||
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.
|
||||
|
||||
If that is the case for your change, find the highest `stateVersion` that is
|
||||
being asserted on in `mail-server/assertions.nix`. Then pick the next number
|
||||
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.
|
||||
|
||||
.. 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.
|
||||
|
||||
Check https://nixos-mailserver.readthedocs.io/en/latest/migrations.html#specific-anchor-here for further details.
|
||||
'';
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
The setup guide should always reference the latest `stateVersion`, since we
|
||||
don't require any migration steps for new setups.
|
||||
|
||||
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.
|
||||
|
||||
@@ -14,23 +14,31 @@ Welcome to NixOS Mailserver's documentation!
|
||||
:maxdepth: 2
|
||||
|
||||
setup-guide
|
||||
advanced-configurations
|
||||
howto-develop
|
||||
faq
|
||||
release-notes
|
||||
options
|
||||
migrations
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Features
|
||||
|
||||
fts
|
||||
ldap
|
||||
srs
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 0
|
||||
:caption: How-to
|
||||
|
||||
backup-guide
|
||||
add-radicale
|
||||
add-roundcube
|
||||
rspamd-tuning
|
||||
fts
|
||||
flakes
|
||||
autodiscovery
|
||||
ldap
|
||||
|
||||
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,44 @@
|
||||
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
|
||||
-----------
|
||||
|
||||
|
||||
@@ -63,15 +63,16 @@ common ones.
|
||||
imports = [
|
||||
(builtins.fetchTarball {
|
||||
# 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-25.05/nixos-mailserver-nixos-25.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:
|
||||
# release="nixos-25.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";
|
||||
})
|
||||
];
|
||||
|
||||
mailserver = {
|
||||
enable = true;
|
||||
stateVersion = 3;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.com" ];
|
||||
|
||||
@@ -237,3 +238,8 @@ Besides that, you can send an email to
|
||||
score, and let `mxtoolbox.com <http://mxtoolbox.com/>`__ take a look at
|
||||
your setup, but if you followed the steps closely then everything should
|
||||
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.
|
||||
39
flake.lock
generated
39
flake.lock
generated
@@ -19,11 +19,11 @@
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1747046372,
|
||||
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
|
||||
"lastModified": 1761588595,
|
||||
"narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
|
||||
"rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -43,11 +43,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1742649964,
|
||||
"narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
|
||||
"lastModified": 1763319842,
|
||||
"narHash": "sha256-YG19IyrTdnVn0l3DvcUYm85u3PaqBt6tI6VvolcuHnA=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
|
||||
"rev": "7275fa67fbbb75891c16d9dee7d88e58aea2d761",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -79,32 +79,16 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1747179050,
|
||||
"narHash": "sha256-qhFMmDkeJX9KJwr5H32f1r7Prs7XbQWtO0h3V0a0rFY=",
|
||||
"lastModified": 1764020296,
|
||||
"narHash": "sha256-6zddwDs2n+n01l+1TG6PlyokDdXzu/oBmEejcH5L5+A=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "adaa24fbf46737f3f1b5497bf64bae750f82942e",
|
||||
"rev": "a320ce8e6e2cc6b4397eef214d202a50a4583829",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-25_05": {
|
||||
"locked": {
|
||||
"lastModified": 1747610100,
|
||||
"narHash": "sha256-rpR5ZPMkWzcnCcYYo3lScqfuzEw5Uyfh+R0EKZfroAc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "ca49c4304acf0973078db0a9d200fd2bae75676d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.05",
|
||||
"ref": "nixos-25.11-small",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -114,8 +98,7 @@
|
||||
"blobs": "blobs",
|
||||
"flake-compat": "flake-compat",
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-25_05": "nixpkgs-25_05"
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
85
flake.nix
85
flake.nix
@@ -12,29 +12,31 @@
|
||||
inputs.flake-compat.follows = "flake-compat";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
nixpkgs-25_05.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small";
|
||||
blobs = {
|
||||
url = "gitlab:simple-nixos-mailserver/blobs";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, blobs, git-hooks, nixpkgs, nixpkgs-25_05, ... }: let
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
blobs,
|
||||
git-hooks,
|
||||
nixpkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
lib = nixpkgs.lib;
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
releases = [
|
||||
{
|
||||
name = "unstable";
|
||||
name = "nixos-25.11";
|
||||
nixpkgs = nixpkgs;
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
}
|
||||
{
|
||||
name = "25.05";
|
||||
nixpkgs = nixpkgs-25_05;
|
||||
pkgs = nixpkgs-25_05.legacyPackages.${system};
|
||||
}
|
||||
];
|
||||
testNames = [
|
||||
"clamav"
|
||||
@@ -44,13 +46,16 @@
|
||||
"multiple"
|
||||
];
|
||||
|
||||
genTest = testName: release: let
|
||||
genTest =
|
||||
testName: release:
|
||||
let
|
||||
pkgs = release.pkgs;
|
||||
nixos-lib = import (release.nixpkgs + "/nixos/lib") {
|
||||
inherit (pkgs) lib;
|
||||
};
|
||||
in {
|
||||
name = "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
|
||||
in
|
||||
{
|
||||
name = "${testName}-${builtins.replaceStrings [ "." ] [ "_" ] release.name}";
|
||||
value = nixos-lib.runTest {
|
||||
hostPkgs = pkgs;
|
||||
imports = [ ./tests/${testName}.nix ];
|
||||
@@ -65,13 +70,13 @@
|
||||
# external-21_05 = <derivation>;
|
||||
# ...
|
||||
# }
|
||||
allTests = lib.listToAttrs (
|
||||
lib.flatten (map (t: map (r: genTest t r) releases) testNames));
|
||||
allTests = lib.listToAttrs (lib.flatten (map (t: map (r: genTest t r) releases) testNames));
|
||||
|
||||
mailserverModule = import ./.;
|
||||
|
||||
# Generate a MarkDown file describing the options of the NixOS mailserver module
|
||||
optionsDoc = let
|
||||
optionsDoc =
|
||||
let
|
||||
eval = lib.evalModules {
|
||||
modules = [
|
||||
mailserverModule
|
||||
@@ -79,21 +84,23 @@
|
||||
_module.check = false;
|
||||
mailserver = {
|
||||
fqdn = "mx.example.com";
|
||||
systemDomain = "example.com";
|
||||
domains = [
|
||||
"example.com"
|
||||
];
|
||||
dmarcReporting = {
|
||||
organizationName = "Example Corp";
|
||||
domain = "example.com";
|
||||
};
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
options = builtins.toFile "options.json" (builtins.toJSON
|
||||
(lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver")
|
||||
(lib.optionAttrSetToDocList eval.options)));
|
||||
in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } ''
|
||||
options = builtins.toFile "options.json" (
|
||||
builtins.toJSON (
|
||||
lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") (
|
||||
lib.optionAttrSetToDocList eval.options
|
||||
)
|
||||
)
|
||||
);
|
||||
in
|
||||
pkgs.runCommand "options.md" { buildInputs = [ pkgs.python3Minimal ]; } ''
|
||||
echo "Generating options.md from ${options}"
|
||||
python ${./scripts/generate-options.py} ${options} > $out
|
||||
echo $out
|
||||
@@ -101,15 +108,22 @@
|
||||
|
||||
documentation = pkgs.stdenv.mkDerivation {
|
||||
name = "documentation";
|
||||
src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"];
|
||||
buildInputs = [(
|
||||
pkgs.python3.withPackages (p: with p; [
|
||||
src = lib.sourceByRegex ./docs [
|
||||
"logo\\.png"
|
||||
"conf\\.py"
|
||||
"Makefile"
|
||||
".*\\.rst"
|
||||
];
|
||||
buildInputs = [
|
||||
(pkgs.python3.withPackages (
|
||||
p: with p; [
|
||||
sphinx
|
||||
sphinx_rtd_theme
|
||||
sphinx-rtd-theme
|
||||
myst-parser
|
||||
linkify-it-py
|
||||
])
|
||||
)];
|
||||
]
|
||||
))
|
||||
];
|
||||
buildPhase = ''
|
||||
cp ${optionsDoc} options.md
|
||||
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
|
||||
@@ -121,7 +135,8 @@
|
||||
'';
|
||||
};
|
||||
|
||||
in {
|
||||
in
|
||||
{
|
||||
nixosModules = rec {
|
||||
mailserver = mailserverModule;
|
||||
default = mailserver;
|
||||
@@ -153,6 +168,7 @@
|
||||
|
||||
# nix
|
||||
deadnix.enable = true;
|
||||
nixfmt-rfc-style.enable = true;
|
||||
|
||||
# python
|
||||
pyright.enable = true;
|
||||
@@ -183,11 +199,16 @@
|
||||
};
|
||||
devShells.${system}.default = pkgs.mkShellNoCC {
|
||||
inputsFrom = [ documentation ];
|
||||
packages = with pkgs; [
|
||||
packages =
|
||||
with pkgs;
|
||||
[
|
||||
glab
|
||||
] ++ self.checks.${system}.pre-commit.enabledPackages;
|
||||
]
|
||||
++ self.checks.${system}.pre-commit.enabledPackages;
|
||||
shellHook = self.checks.${system}.pre-commit.shellHook;
|
||||
};
|
||||
devShell.${system} = self.devShells.${system}.default; # compatibility
|
||||
|
||||
formatter.${system} = pkgs.nixfmt-tree;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,18 +1,85 @@
|
||||
{ config, lib, ... }:
|
||||
{
|
||||
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.loginAccounts == {};
|
||||
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 == { };
|
||||
message = "When the LDAP support is enable (mailserver.ldap.enable = true), it is not possible to define mailserver.loginAccounts";
|
||||
}
|
||||
{
|
||||
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";
|
||||
}
|
||||
] ++ lib.optionals (config.mailserver.enable && config.mailserver.certificateScheme != "acme") [
|
||||
]
|
||||
++
|
||||
lib.optionals (config.mailserver.ldap.enable && config.mailserver.mailDirectory != "/var/vmail")
|
||||
[
|
||||
{
|
||||
assertion = config.mailserver.stateVersion != null -> config.mailserver.stateVersion >= 2;
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.mailserver.borgbackup;
|
||||
|
||||
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
|
||||
autoFragment =
|
||||
if cfg.compression.auto && cfg.compression.method == null
|
||||
then throw "compression.method must be set when using auto."
|
||||
else lib.optional cfg.compression.auto "auto";
|
||||
if cfg.compression.auto && cfg.compression.method == null then
|
||||
throw "compression.method must be set when using auto."
|
||||
else
|
||||
lib.optional cfg.compression.auto "auto";
|
||||
levelFragment =
|
||||
if cfg.compression.level != null && cfg.compression.method == null
|
||||
then throw "compression.method must be set when using compression.level."
|
||||
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
|
||||
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]);
|
||||
if cfg.compression.level != null && cfg.compression.method == null then
|
||||
throw "compression.method must be set when using compression.level."
|
||||
else
|
||||
lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
|
||||
compressionFragment = lib.concatStringsSep "," (
|
||||
lib.flatten [
|
||||
autoFragment
|
||||
methodFragment
|
||||
levelFragment
|
||||
]
|
||||
);
|
||||
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
|
||||
|
||||
encryptionFragment = cfg.encryption.method;
|
||||
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
|
||||
passphraseFragment = lib.optionalString (cfg.encryption.method != "none")
|
||||
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
|
||||
else throw "passphraseFile must be set when using encryption.");
|
||||
passphraseFragment = lib.optionalString (cfg.encryption.method != "none") (
|
||||
if cfg.encryption.passphraseFile != null then
|
||||
''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
|
||||
else
|
||||
throw "passphraseFile must be set when using encryption."
|
||||
);
|
||||
|
||||
locations = lib.escapeShellArgs cfg.locations;
|
||||
name = lib.escapeShellArg cfg.name;
|
||||
@@ -55,7 +71,8 @@ let
|
||||
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
|
||||
${cmdPostexec}
|
||||
'';
|
||||
in {
|
||||
in
|
||||
{
|
||||
config = lib.mkIf (config.mailserver.enable && cfg.enable) {
|
||||
environment.systemPackages = with pkgs; [
|
||||
borgbackup
|
||||
|
||||
@@ -14,43 +14,63 @@
|
||||
# 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 }:
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
in
|
||||
{
|
||||
# cert :: PATH
|
||||
certificatePath = if cfg.certificateScheme == "manual"
|
||||
then cfg.certificateFile
|
||||
else if cfg.certificateScheme == "selfsigned"
|
||||
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
|
||||
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
|
||||
else throw "unknown certificate scheme";
|
||||
certificatePath =
|
||||
if cfg.certificateScheme == "manual" then
|
||||
cfg.certificateFile
|
||||
else if cfg.certificateScheme == "selfsigned" then
|
||||
"${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
|
||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
|
||||
"${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
|
||||
else
|
||||
throw "unknown certificate scheme";
|
||||
|
||||
# key :: PATH
|
||||
keyPath = if cfg.certificateScheme == "manual"
|
||||
then cfg.keyFile
|
||||
else if cfg.certificateScheme == "selfsigned"
|
||||
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"
|
||||
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
|
||||
else throw "unknown certificate scheme";
|
||||
keyPath =
|
||||
if cfg.certificateScheme == "manual" then
|
||||
cfg.keyFile
|
||||
else if cfg.certificateScheme == "selfsigned" then
|
||||
"${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
|
||||
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
|
||||
"${config.security.acme.certs.${cfg.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;
|
||||
in
|
||||
lib.mapAttrs (name: value:
|
||||
lib.mapAttrs (
|
||||
name: value:
|
||||
if value.hashedPasswordFile == null then
|
||||
builtins.toString (mkHashFile name value.hashedPassword)
|
||||
else value.hashedPasswordFile) cfg.loginAccounts;
|
||||
else
|
||||
value.hashedPasswordFile
|
||||
) cfg.loginAccounts;
|
||||
|
||||
# Appends the LDAP bind password to files to avoid writing this
|
||||
# password into the Nix store.
|
||||
appendLdapBindPwd = {
|
||||
name, file, prefix, suffix ? "", passwordFile, destination
|
||||
}: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
||||
appendLdapBindPwd =
|
||||
{
|
||||
name,
|
||||
file,
|
||||
prefix,
|
||||
suffix ? "",
|
||||
passwordFile,
|
||||
destination,
|
||||
}:
|
||||
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
|
||||
#!${pkgs.stdenv.shell}
|
||||
set -euo pipefail
|
||||
|
||||
|
||||
@@ -14,9 +14,22 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{ options, 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
|
||||
cfg = config.mailserver;
|
||||
@@ -28,20 +41,23 @@ let
|
||||
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
|
||||
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}";
|
||||
listToMultiAttrs =
|
||||
keyPrefix: attrs:
|
||||
lib.listToAttrs (
|
||||
lib.imap1 (n: x: {
|
||||
name = "${keyPrefix}${if n == 1 then "" else toString n}";
|
||||
value = x;
|
||||
}) attrs);
|
||||
}) attrs
|
||||
);
|
||||
|
||||
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
|
||||
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 =
|
||||
"maildir:${cfg.mailDirectory}/%{domain}/%{username}${maildirLayoutAppendix}${maildirUTF8FolderNames}"
|
||||
+ (lib.optionalString (cfg.indexDir != null)
|
||||
":INDEX=${cfg.indexDir}/%{domain}/%{username}"
|
||||
);
|
||||
"maildir:~/mail${maildirLayoutAppendix}${maildirUTF8FolderNames}"
|
||||
+ (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}");
|
||||
|
||||
postfixCfg = config.services.postfix;
|
||||
|
||||
@@ -93,7 +109,9 @@ let
|
||||
# Prevent world-readable password files, even temporarily.
|
||||
umask 077
|
||||
|
||||
for f in ${builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts)}; do
|
||||
for f in ${
|
||||
builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts)
|
||||
}; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "Expected password hash file $f does not exist!"
|
||||
exit 1
|
||||
@@ -101,51 +119,61 @@ let
|
||||
done
|
||||
|
||||
cat <<EOF > ${passwdFile}
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: _:
|
||||
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
||||
) cfg.loginAccounts)}
|
||||
${lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (
|
||||
name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
|
||||
) cfg.loginAccounts
|
||||
)}
|
||||
EOF
|
||||
|
||||
cat <<EOF > ${userdbFile}
|
||||
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
|
||||
${lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
"${name}:::::::"
|
||||
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
|
||||
) cfg.loginAccounts)}
|
||||
) cfg.loginAccounts
|
||||
)}
|
||||
EOF
|
||||
'';
|
||||
|
||||
junkMailboxes = builtins.attrNames (lib.filterAttrs (_: 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;
|
||||
# The assertion garantees there is exactly one Junk mailbox.
|
||||
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
|
||||
|
||||
mkLdapSearchScope = scope: (
|
||||
if scope == "sub" then "subtree"
|
||||
else if scope == "one" then "onelevel"
|
||||
else scope
|
||||
mkLdapSearchScope =
|
||||
scope:
|
||||
(
|
||||
if scope == "sub" then
|
||||
"subtree"
|
||||
else if scope == "one" then
|
||||
"onelevel"
|
||||
else
|
||||
scope
|
||||
);
|
||||
|
||||
dovecotModules = [
|
||||
pkgs.dovecot_pigeonhole
|
||||
] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
|
||||
# Remove and assume `false` after NixOS 25.05
|
||||
haveDovecotModulesOption = options.services.dovecot2 ? "modules" && (options.services.dovecot2.modules.visible or true);
|
||||
|
||||
ftsPluginSettings = {
|
||||
fts = "flatcurve";
|
||||
fts_languages = listToLine cfg.fullTextSearch.languages;
|
||||
fts_tokenizers = listToLine [ "generic" "email-address" ];
|
||||
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);
|
||||
}
|
||||
// (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude);
|
||||
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
config = lib.mkIf cfg.enable {
|
||||
assertions = [
|
||||
{
|
||||
assertion = junkMailboxNumber == 1;
|
||||
@@ -154,42 +182,43 @@ in
|
||||
];
|
||||
|
||||
warnings =
|
||||
(lib.optional (
|
||||
(builtins.length cfg.fullTextSearch.languages > 1) &&
|
||||
(builtins.elem "stopwords" cfg.fullTextSearch.filters)
|
||||
) ''
|
||||
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
|
||||
# the global config and tries to open shared libraries configured in there,
|
||||
# which are usually not compatible.
|
||||
environment.systemPackages = [
|
||||
pkgs.dovecot_pigeonhole
|
||||
] ++ lib.optionals (!haveDovecotModulesOption) dovecotModules;
|
||||
]
|
||||
++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
|
||||
|
||||
# For compatibility with python imaplib
|
||||
environment.etc = lib.mkIf (!haveDovecotModulesOption) {
|
||||
"dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
|
||||
};
|
||||
environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
|
||||
|
||||
services.dovecot2 = lib.mkMerge [{
|
||||
services.dovecot2 = {
|
||||
enable = true;
|
||||
enableImap = enableImap || enableImapSsl;
|
||||
enablePop3 = enablePop3 || enablePop3Ssl;
|
||||
enableImap = cfg.enableImap || cfg.enableImapSsl;
|
||||
enablePop3 = cfg.enablePop3 || cfg.enablePop3Ssl;
|
||||
enablePAM = false;
|
||||
enableQuota = true;
|
||||
mailGroup = vmailGroupName;
|
||||
mailUser = vmailUserName;
|
||||
mailGroup = cfg.vmailGroupName;
|
||||
mailUser = cfg.vmailUserName;
|
||||
mailLocation = dovecotMaildir;
|
||||
sslServerCert = certificatePath;
|
||||
sslServerKey = keyPath;
|
||||
enableDHE = lib.mkDefault false;
|
||||
enableLmtp = true;
|
||||
mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [
|
||||
"fts"
|
||||
@@ -201,7 +230,8 @@ in
|
||||
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);
|
||||
}
|
||||
// (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings);
|
||||
|
||||
sieve = {
|
||||
extensions = [
|
||||
@@ -218,17 +248,18 @@ in
|
||||
'';
|
||||
|
||||
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")
|
||||
(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" ];
|
||||
causes = [
|
||||
"COPY"
|
||||
"APPEND"
|
||||
];
|
||||
before = ./dovecot/imap_sieve/report-spam.sieve;
|
||||
}
|
||||
{
|
||||
@@ -243,7 +274,7 @@ in
|
||||
|
||||
extraConfig = ''
|
||||
#Extra Config
|
||||
${lib.optionalString debug ''
|
||||
${lib.optionalString cfg.debug.dovecot ''
|
||||
mail_debug = yes
|
||||
auth_debug = yes
|
||||
verbose_ssl = yes
|
||||
@@ -252,42 +283,62 @@ in
|
||||
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
|
||||
service imap-login {
|
||||
inet_listener imap {
|
||||
${if cfg.enableImap then ''
|
||||
${
|
||||
if cfg.enableImap then
|
||||
''
|
||||
port = 143
|
||||
'' else ''
|
||||
''
|
||||
else
|
||||
''
|
||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
||||
port = 0
|
||||
''}
|
||||
''
|
||||
}
|
||||
}
|
||||
inet_listener imaps {
|
||||
${if cfg.enableImapSsl then ''
|
||||
${
|
||||
if cfg.enableImapSsl then
|
||||
''
|
||||
port = 993
|
||||
ssl = yes
|
||||
'' else ''
|
||||
''
|
||||
else
|
||||
''
|
||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
||||
port = 0
|
||||
''}
|
||||
''
|
||||
}
|
||||
}
|
||||
}
|
||||
''}
|
||||
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
|
||||
service pop3-login {
|
||||
inet_listener pop3 {
|
||||
${if cfg.enablePop3 then ''
|
||||
${
|
||||
if cfg.enablePop3 then
|
||||
''
|
||||
port = 110
|
||||
'' else ''
|
||||
''
|
||||
else
|
||||
''
|
||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
||||
port = 0
|
||||
''}
|
||||
''
|
||||
}
|
||||
}
|
||||
inet_listener pop3s {
|
||||
${if cfg.enablePop3Ssl then ''
|
||||
${
|
||||
if cfg.enablePop3Ssl then
|
||||
''
|
||||
port = 995
|
||||
ssl = yes
|
||||
'' else ''
|
||||
''
|
||||
else
|
||||
''
|
||||
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
|
||||
port = 0
|
||||
''}
|
||||
''
|
||||
}
|
||||
}
|
||||
}
|
||||
''}
|
||||
@@ -305,10 +356,13 @@ in
|
||||
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_min_protocol = TLSv1
|
||||
ssl_prefer_server_ciphers = no
|
||||
ssl_curve_list = X25519MLKEM768:X25519:prime256v1:secp384r1
|
||||
|
||||
service lmtp {
|
||||
unix_listener dovecot-lmtp {
|
||||
@@ -344,7 +398,10 @@ in
|
||||
userdb {
|
||||
driver = passwd-file
|
||||
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 ''
|
||||
@@ -356,7 +413,14 @@ in
|
||||
userdb {
|
||||
driver = ldap
|
||||
args = ${ldapConfFile}
|
||||
default_fields = home=/var/vmail/ldap/%{user} 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}"
|
||||
}
|
||||
|
||||
}
|
||||
''}
|
||||
|
||||
@@ -377,25 +441,25 @@ in
|
||||
|
||||
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_autocreate = yes
|
||||
'';
|
||||
}
|
||||
(lib.mkIf haveDovecotModulesOption {
|
||||
modules = dovecotModules;
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
systemd.services.dovecot2 = {
|
||||
systemd.services.dovecot = {
|
||||
preStart = ''
|
||||
${genPasswdScript}
|
||||
'' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
|
||||
''
|
||||
+ (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
|
||||
};
|
||||
|
||||
systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]);
|
||||
systemd.services.postfix.restartTriggers = [
|
||||
genPasswdScript
|
||||
]
|
||||
++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,15 +14,26 @@
|
||||
# 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, ... }:
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
environment.systemPackages = with pkgs; [
|
||||
dovecot openssh postfix rspamd
|
||||
] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []);
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages =
|
||||
with pkgs;
|
||||
[
|
||||
dovecot
|
||||
openssh
|
||||
postfix
|
||||
rspamd
|
||||
]
|
||||
++ (if cfg.certificateScheme == "selfsigned" then [ openssl ] else [ ]);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -24,4 +24,3 @@ in
|
||||
services.kresd.enable = true;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,18 +20,20 @@ let
|
||||
cfg = config.mailserver;
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf (enable && openFirewall) {
|
||||
config = lib.mkIf (cfg.enable && cfg.openFirewall) {
|
||||
|
||||
networking.firewall = {
|
||||
allowedTCPPorts = [ 25 ]
|
||||
++ lib.optional enableSubmission 587
|
||||
++ lib.optional enableSubmissionSsl 465
|
||||
++ lib.optional enableImap 143
|
||||
++ lib.optional enableImapSsl 993
|
||||
++ lib.optional enablePop3 110
|
||||
++ lib.optional enablePop3Ssl 995
|
||||
++ lib.optional enableManageSieve 4190
|
||||
++ lib.optional (certificateScheme == "acme-nginx") 80;
|
||||
allowedTCPPorts = [
|
||||
25
|
||||
]
|
||||
++ lib.optional cfg.enableSubmission 587
|
||||
++ lib.optional cfg.enableSubmissionSsl 465
|
||||
++ lib.optional cfg.enableImap 143
|
||||
++ lib.optional cfg.enableImapSsl 993
|
||||
++ lib.optional cfg.enablePop3 110
|
||||
++ lib.optional cfg.enablePop3Ssl 995
|
||||
++ lib.optional cfg.enableManageSieve 4190
|
||||
++ lib.optional (cfg.certificateScheme == "acme-nginx") 80;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,16 +14,30 @@
|
||||
# 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,
|
||||
options,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with (import ./common.nix { inherit config lib pkgs; });
|
||||
with (import ./common.nix {
|
||||
inherit
|
||||
config
|
||||
options
|
||||
lib
|
||||
pkgs
|
||||
;
|
||||
});
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
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") {
|
||||
enable = true;
|
||||
virtualHosts."${cfg.fqdn}" = {
|
||||
@@ -34,9 +48,12 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
|
||||
security.acme.certs."${cfg.acmeCertificateName}" = {
|
||||
extraDomainNames = lib.mkIf (cfg.certificateScheme == "acme") cfg.certificateDomains;
|
||||
reloadServices = [
|
||||
"postfix.service"
|
||||
"dovecot2.service"
|
||||
"dovecot.service"
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,45 +14,84 @@
|
||||
# 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, ... }:
|
||||
{
|
||||
config,
|
||||
options,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
with (import ./common.nix { inherit config pkgs lib; });
|
||||
with (import ./common.nix {
|
||||
inherit
|
||||
config
|
||||
options
|
||||
lib
|
||||
pkgs
|
||||
;
|
||||
});
|
||||
|
||||
let
|
||||
inherit (lib.strings) concatStringsSep;
|
||||
cfg = config.mailserver;
|
||||
|
||||
iniFormat = pkgs.formats.iniWithGlobalSection { };
|
||||
|
||||
# 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 value is a list of addresses
|
||||
mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables;
|
||||
|
||||
# valiases_postfix :: Map String [String]
|
||||
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||
(name: value:
|
||||
let to = name;
|
||||
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name))
|
||||
cfg.loginAccounts));
|
||||
regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||
(name: value:
|
||||
let to = name;
|
||||
in map (from: {"${from}" = to;}) value.aliasesRegexp)
|
||||
cfg.loginAccounts));
|
||||
valiases_postfix = mergeLookupTables (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
let
|
||||
to = name;
|
||||
in
|
||||
map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name)
|
||||
) cfg.loginAccounts
|
||||
)
|
||||
);
|
||||
regex_valiases_postfix = mergeLookupTables (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
let
|
||||
to = name;
|
||||
in
|
||||
map (from: { "${from}" = to; }) value.aliasesRegexp
|
||||
) cfg.loginAccounts
|
||||
)
|
||||
);
|
||||
|
||||
# catchAllPostfix :: Map String [String]
|
||||
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList
|
||||
(name: value:
|
||||
let to = name;
|
||||
in map (from: {"@${from}" = to;}) value.catchAll)
|
||||
cfg.loginAccounts));
|
||||
catchAllPostfix = mergeLookupTables (
|
||||
lib.flatten (
|
||||
lib.mapAttrsToList (
|
||||
name: value:
|
||||
let
|
||||
to = name;
|
||||
in
|
||||
map (from: { "@${from}" = to; }) value.catchAll
|
||||
) cfg.loginAccounts
|
||||
)
|
||||
);
|
||||
|
||||
# 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 = aliases: let
|
||||
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases;
|
||||
in mergeLookupTables lookupTables;
|
||||
attrsToLookupTable =
|
||||
aliases:
|
||||
let
|
||||
lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases;
|
||||
in
|
||||
mergeLookupTables lookupTables;
|
||||
|
||||
# extra_valiases_postfix :: Map String [String]
|
||||
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
|
||||
@@ -61,37 +100,49 @@ let
|
||||
forwards = attrsToLookupTable cfg.forwards;
|
||||
|
||||
# lookupTableToString :: Map String [String] -> String
|
||||
lookupTableToString = attrs: let
|
||||
lookupTableToString =
|
||||
attrs:
|
||||
let
|
||||
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 = let
|
||||
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]);
|
||||
in builtins.toFile "valias" content;
|
||||
valiases_file =
|
||||
let
|
||||
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;
|
||||
in builtins.toFile "regex_valias" content;
|
||||
in
|
||||
builtins.toFile "regex_valias" content;
|
||||
|
||||
# denied_recipients_postfix :: [ String ]
|
||||
denied_recipients_postfix = (map
|
||||
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}")
|
||||
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)));
|
||||
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix);
|
||||
denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") (
|
||||
lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)
|
||||
);
|
||||
denied_recipients_file = builtins.toFile "denied_recipients" (
|
||||
lib.concatStringsSep "\n" denied_recipients_postfix
|
||||
);
|
||||
|
||||
reject_senders_postfix = (map
|
||||
(sender:
|
||||
"${sender} REJECT")
|
||||
(cfg.rejectSender));
|
||||
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
|
||||
reject_senders_postfix = map (sender: "${sender} REJECT") cfg.rejectSender;
|
||||
reject_senders_file = builtins.toFile "reject_senders" (
|
||||
lib.concatStringsSep "\n" reject_senders_postfix
|
||||
);
|
||||
|
||||
reject_recipients_postfix = (map
|
||||
(recipient:
|
||||
"${recipient} REJECT")
|
||||
(cfg.rejectRecipients));
|
||||
reject_recipients_postfix = map (recipient: "${recipient} REJECT") cfg.rejectRecipients;
|
||||
# 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 = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
|
||||
@@ -103,9 +154,12 @@ let
|
||||
# every alias is owned (uniquely) by its user.
|
||||
# The user's own address is already in all_valiases_postfix.
|
||||
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
|
||||
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (lookupTableToString regex_valiases_postfix);
|
||||
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.
|
||||
# See https://thomas-leister.de/mailserver-debian-stretch/
|
||||
# Uses "pcre" style regex.
|
||||
@@ -115,21 +169,22 @@ let
|
||||
/^X-Mailer:/ IGNORE
|
||||
/^User-Agent:/ 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
|
||||
# user's host or network.
|
||||
|
||||
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
|
||||
'');
|
||||
''
|
||||
);
|
||||
|
||||
smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
||||
|
||||
mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
|
||||
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
|
||||
|
||||
submissionOptions =
|
||||
{
|
||||
submissionOptions = {
|
||||
smtpd_tls_security_level = "encrypt";
|
||||
smtpd_sasl_auth_enable = "yes";
|
||||
smtpd_sasl_type = "dovecot";
|
||||
@@ -137,7 +192,9 @@ let
|
||||
smtpd_sasl_security_options = "noanonymous";
|
||||
smtpd_sasl_local_domain = "$myhostname";
|
||||
smtpd_client_restrictions = "permit_sasl_authenticated,reject";
|
||||
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${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_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
|
||||
cleanup_service_name = "submission-header-cleanup";
|
||||
@@ -186,20 +243,55 @@ let
|
||||
};
|
||||
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 {
|
||||
preStart = ''
|
||||
${appendPwdInVirtualMailboxMap}
|
||||
${appendPwdInSenderLoginMap}
|
||||
'';
|
||||
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
|
||||
restartTriggers = [
|
||||
appendPwdInVirtualMailboxMap
|
||||
appendPwdInSenderLoginMap
|
||||
];
|
||||
};
|
||||
|
||||
services.postfix = {
|
||||
enable = true;
|
||||
hostname = "${sendingFqdn}";
|
||||
networksStyle = "host";
|
||||
mapFiles."valias" = valiases_file;
|
||||
mapFiles."regex_valias" = regex_valiases_file;
|
||||
mapFiles."vaccounts" = vaccounts_file;
|
||||
@@ -207,50 +299,54 @@ in
|
||||
mapFiles."denied_recipients" = denied_recipients_file;
|
||||
mapFiles."reject_senders" = reject_senders_file;
|
||||
mapFiles."reject_recipients" = reject_recipients_file;
|
||||
sslCert = certificatePath;
|
||||
sslKey = keyPath;
|
||||
enableSubmission = cfg.enableSubmission;
|
||||
enableSubmissions = cfg.enableSubmissionSsl;
|
||||
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
|
||||
virtual = lookupTableToString (mergeLookupTables [
|
||||
all_valiases_postfix
|
||||
catchAllPostfix
|
||||
forwards
|
||||
]);
|
||||
|
||||
config = {
|
||||
# Extra Config
|
||||
mydestination = "";
|
||||
settings.main = {
|
||||
myhostname = cfg.sendingFqdn;
|
||||
mydestination = ""; # disable local mail delivery
|
||||
recipient_delimiter = cfg.recipientDelimiter;
|
||||
smtpd_banner = "${fqdn} ESMTP NO UCE";
|
||||
smtpd_banner = "${cfg.fqdn} ESMTP NO UCE";
|
||||
disable_vrfy_command = true;
|
||||
message_size_limit = toString cfg.messageSizeLimit;
|
||||
message_size_limit = cfg.messageSizeLimit;
|
||||
|
||||
# virtual mail system
|
||||
virtual_uid_maps = "static:5000";
|
||||
virtual_gid_maps = "static:5000";
|
||||
virtual_mailbox_base = mailDirectory;
|
||||
virtual_mailbox_base = cfg.mailDirectory;
|
||||
virtual_mailbox_domains = vhosts_file;
|
||||
virtual_mailbox_maps = [
|
||||
(mappedFile "valias")
|
||||
] ++ lib.optionals (cfg.ldap.enable) [
|
||||
]
|
||||
++ lib.optionals cfg.ldap.enable [
|
||||
"ldap:${ldapVirtualMailboxMapFile}"
|
||||
] ++ lib.optionals (regex_valiases_postfix != {}) [
|
||||
]
|
||||
++ lib.optionals (regex_valiases_postfix != { }) [
|
||||
(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")
|
||||
]);
|
||||
]
|
||||
);
|
||||
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
|
||||
|
||||
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
|
||||
lmtp_destination_recipient_limit = "1";
|
||||
|
||||
# Opportunistic DANE support
|
||||
# https://www.postfix.org/postconf.5.html#smtp_tls_security_level
|
||||
smtp_dns_support_level = "dnssec";
|
||||
smtp_tls_security_level = "dane";
|
||||
|
||||
# sasl with dovecot
|
||||
smtpd_sasl_type = "dovecot";
|
||||
smtpd_sasl_path = "/run/dovecot2/auth";
|
||||
smtpd_sasl_auth_enable = true;
|
||||
smtpd_relay_restrictions = [
|
||||
"permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination"
|
||||
"permit_mynetworks"
|
||||
"permit_sasl_authenticated"
|
||||
"reject_unauth_destination"
|
||||
];
|
||||
|
||||
# reject selected senders
|
||||
@@ -266,52 +362,92 @@ in
|
||||
"check_policy_service unix:/run/dovecot2/quota-status"
|
||||
];
|
||||
|
||||
# TLS settings, inspired by https://github.com/jeaye/nix-files
|
||||
# Submission by mail clients is handled in submissionOptions
|
||||
# The X509 private key followed by the corresponding certificate
|
||||
smtpd_tls_chain_files = [
|
||||
"${keyPath}"
|
||||
"${certificatePath}"
|
||||
];
|
||||
|
||||
# TLS for incoming mail is optional
|
||||
smtpd_tls_security_level = "may";
|
||||
|
||||
# Disable obselete protocols
|
||||
smtpd_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, TLSv1, !SSLv2, !SSLv3";
|
||||
smtp_tls_protocols = "TLSv1.3, TLSv1.2, TLSv1.1, TLSv1, !SSLv2, !SSLv3";
|
||||
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";
|
||||
# But required for authentication attempts
|
||||
smtpd_tls_auth_only = true;
|
||||
|
||||
smtp_tls_ciphers = "high";
|
||||
# TLS versions supported for the SMTP server
|
||||
smtpd_tls_protocols = ">=TLSv1";
|
||||
smtpd_tls_mandatory_protocols = ">=TLSv1";
|
||||
|
||||
# Require ciphersuites that OpenSSL classifies as "High"
|
||||
smtpd_tls_ciphers = "high";
|
||||
smtp_tls_mandatory_ciphers = "high";
|
||||
smtpd_tls_mandatory_ciphers = "high";
|
||||
|
||||
# Disable deprecated ciphers
|
||||
smtpd_tls_mandatory_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, eNULL, aNULL";
|
||||
smtpd_tls_exclude_ciphers = "MD5, DES, ADH, RC4, PSD, SRP, 3DES, 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";
|
||||
# Exclude cipher suites with undesirable properties
|
||||
smtpd_tls_exclude_ciphers = "SHA1, eNULL, aNULL";
|
||||
smtpd_tls_mandatory_exclude_ciphers = "SHA1, 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
|
||||
smtp_tls_loglevel = "1";
|
||||
smtpd_tls_loglevel = "1";
|
||||
|
||||
# Configure a non blocking source of randomness
|
||||
tls_random_source = "dev:/dev/urandom";
|
||||
|
||||
smtpd_milters = smtpdMilters;
|
||||
non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ];
|
||||
milter_protocol = "6";
|
||||
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;
|
||||
submissionsOptions = submissionOptions;
|
||||
|
||||
masterConfig = {
|
||||
settings.master = {
|
||||
"lmtp" = {
|
||||
# Add headers when delivering, see http://www.postfix.org/smtp.8.html
|
||||
# D => Delivered-To, O => X-Original-To, R => Return-Path
|
||||
@@ -323,7 +459,10 @@ in
|
||||
chroot = false;
|
||||
maxproc = 0;
|
||||
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
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
with lib;
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
optionalString
|
||||
mkIf
|
||||
;
|
||||
|
||||
cfg = config.mailserver;
|
||||
|
||||
preexecDefined = cfg.backup.cmdPreexec != null;
|
||||
@@ -38,7 +46,8 @@ let
|
||||
${cfg.backup.cmdPostexec}
|
||||
'';
|
||||
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
|
||||
in {
|
||||
in
|
||||
{
|
||||
config = mkIf (cfg.enable && cfg.backup.enable) {
|
||||
services.rsnapshot = {
|
||||
enable = true;
|
||||
|
||||
@@ -14,7 +14,12 @@
|
||||
# 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, ... }:
|
||||
{
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
@@ -26,10 +31,13 @@ let
|
||||
rspamdUser = config.services.rspamd.user;
|
||||
rspamdGroup = config.services.rspamd.group;
|
||||
|
||||
createDkimKeypair = domain: let
|
||||
createDkimKeypair =
|
||||
domain:
|
||||
let
|
||||
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
|
||||
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
|
||||
in pkgs.writeShellScript "dkim-keygen-${domain}" ''
|
||||
in
|
||||
pkgs.writeShellScript "dkim-keygen-${domain}" ''
|
||||
if [ ! -f "${privateKey}" ]
|
||||
then
|
||||
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
|
||||
@@ -42,40 +50,54 @@ let
|
||||
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
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
config = lib.mkIf cfg.enable {
|
||||
environment.systemPackages = lib.mkBefore [
|
||||
(pkgs.runCommand "rspamc-wrapped" {
|
||||
(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 = {
|
||||
enable = true;
|
||||
inherit debug;
|
||||
debug = cfg.debug.rspamd;
|
||||
locals = {
|
||||
"milter_headers.conf" = { text = ''
|
||||
"milter_headers.conf" = {
|
||||
text = ''
|
||||
extended_spam_headers = true;
|
||||
''; };
|
||||
"redis.conf" = { text = ''
|
||||
servers = "${if cfg.redis.port == null
|
||||
then
|
||||
'';
|
||||
};
|
||||
"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) ''
|
||||
"${cfg.redis.address}:${toString cfg.redis.port}"
|
||||
}";
|
||||
''
|
||||
+ (lib.optionalString (cfg.redis.password != null) ''
|
||||
password = "${cfg.redis.password}";
|
||||
''); };
|
||||
"classifier-bayes.conf" = { text = ''
|
||||
'');
|
||||
};
|
||||
"classifier-bayes.conf" = {
|
||||
text = ''
|
||||
cache {
|
||||
backend = "redis";
|
||||
}
|
||||
''; };
|
||||
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = ''
|
||||
'';
|
||||
};
|
||||
"antivirus.conf" = lib.mkIf cfg.virusScanning {
|
||||
text = ''
|
||||
clamav {
|
||||
action = "reject";
|
||||
symbol = "CLAM_VIRUS";
|
||||
@@ -84,36 +106,52 @@ in
|
||||
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
|
||||
}
|
||||
''; };
|
||||
"dkim_signing.conf" = { text = ''
|
||||
'';
|
||||
};
|
||||
"dkim_signing.conf" = {
|
||||
text = ''
|
||||
enabled = ${lib.boolToString cfg.dkimSigning};
|
||||
path = "${cfg.dkimKeyDirectory}/$domain.$selector.key";
|
||||
selector = "${cfg.dkimSelector}";
|
||||
# Allow for usernames w/o domain part
|
||||
allow_username_mismatch = true
|
||||
''; };
|
||||
"dmarc.conf" = { text = ''
|
||||
allow_username_mismatch = true;
|
||||
# Don't normalize DKIM key selection for subdomains
|
||||
use_esld = false;
|
||||
'';
|
||||
};
|
||||
"dmarc.conf" = {
|
||||
text = ''
|
||||
${lib.optionalString cfg.dmarcReporting.enable ''
|
||||
reporting {
|
||||
enabled = true;
|
||||
email = "${cfg.dmarcReporting.email}";
|
||||
domain = "${cfg.dmarcReporting.domain}";
|
||||
org_name = "${cfg.dmarcReporting.organizationName}";
|
||||
from_name = "${cfg.dmarcReporting.fromName}";
|
||||
msgid_from = "${cfg.dmarcReporting.domain}";
|
||||
${lib.optionalString (cfg.dmarcReporting.excludeDomains != []) ''
|
||||
email = "noreply-dmarc@${cfg.systemDomain}";
|
||||
domain = "${cfg.systemDomain}";
|
||||
org_name = "${cfg.systemName}";
|
||||
from_name = "${cfg.systemName}";
|
||||
msgid_from = "${cfg.systemDomain}";
|
||||
${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) ''
|
||||
exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
|
||||
''}
|
||||
}''}
|
||||
''; };
|
||||
'';
|
||||
};
|
||||
};
|
||||
overrides = {
|
||||
"options.inc" = {
|
||||
text = ''
|
||||
local_addrs = [::1/128, 127.0.0.0/8]
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
workers.rspamd_proxy = {
|
||||
type = "rspamd_proxy";
|
||||
bindSockets = [{
|
||||
bindSockets = [
|
||||
{
|
||||
socket = "/run/rspamd/rspamd-milter.sock";
|
||||
mode = "0664";
|
||||
}];
|
||||
}
|
||||
];
|
||||
count = 1; # Do not spawn too many processes of this type
|
||||
extraConfig = ''
|
||||
milter = yes; # Enable milter mode
|
||||
@@ -128,11 +166,13 @@ in
|
||||
workers.controller = {
|
||||
type = "controller";
|
||||
count = 1;
|
||||
bindSockets = [{
|
||||
bindSockets = [
|
||||
{
|
||||
socket = "/run/rspamd/worker-controller.sock";
|
||||
mode = "0666";
|
||||
}];
|
||||
includes = [];
|
||||
}
|
||||
];
|
||||
includes = [ ];
|
||||
extraConfig = ''
|
||||
static_dir = "''${WWWDIR}"; # Serve the web UI static assets
|
||||
'';
|
||||
@@ -140,7 +180,7 @@ in
|
||||
|
||||
};
|
||||
|
||||
services.redis.servers.rspamd.enable = lib.mkDefault true;
|
||||
services.redis.servers.rspamd.enable = lib.mkDefault cfg.redis.configureLocally;
|
||||
|
||||
systemd.tmpfiles.settings."10-rspamd.conf" = {
|
||||
"${cfg.dkimKeyDirectory}" = {
|
||||
@@ -165,24 +205,26 @@ in
|
||||
SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
|
||||
}
|
||||
(lib.optionalAttrs cfg.dkimSigning {
|
||||
ExecStartPre = map createDkimKeypair cfg.domains;
|
||||
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
|
||||
# default behaviour when called without a date.
|
||||
# https://github.com/rspamd/rspamd/issues/4062
|
||||
script = ''
|
||||
${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
|
||||
'';
|
||||
script = toString [
|
||||
(lib.getExe' pkgs.rspamd "rspamadm")
|
||||
"dmarc_report"
|
||||
"$(date -d 'yesterday' '+%Y%m%d')"
|
||||
];
|
||||
serviceConfig = {
|
||||
User = "${config.services.rspamd.user}";
|
||||
Group = "${config.services.rspamd.group}";
|
||||
|
||||
AmbientCapabilities = [];
|
||||
AmbientCapabilities = [ ];
|
||||
CapabilityBoundingSet = "";
|
||||
DevicePolicy = "closed";
|
||||
IPAddressAllow = "localhost";
|
||||
@@ -203,10 +245,17 @@ in
|
||||
ProcSubset = "pid";
|
||||
ProtectSystem = "strict";
|
||||
RemoveIPC = true;
|
||||
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
|
||||
RestrictAddressFamilies = [
|
||||
"AF_INET"
|
||||
"AF_INET6"
|
||||
"AF_UNIX"
|
||||
];
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SupplementaryGroups = lib.optionals cfg.redis.configureLocally [
|
||||
config.services.redis.servers.rspamd.group
|
||||
];
|
||||
SystemCallArchitectures = "native";
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
@@ -216,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";
|
||||
wantedBy = [
|
||||
"timers.target"
|
||||
@@ -237,4 +286,3 @@ in
|
||||
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,22 +14,40 @@
|
||||
# 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, ... }:
|
||||
{
|
||||
config,
|
||||
options,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
with (import ./common.nix {
|
||||
inherit
|
||||
config
|
||||
options
|
||||
lib
|
||||
pkgs
|
||||
;
|
||||
});
|
||||
|
||||
let
|
||||
cfg = config.mailserver;
|
||||
certificatesDeps =
|
||||
if cfg.certificateScheme == "manual" then
|
||||
[]
|
||||
[ ]
|
||||
else if cfg.certificateScheme == "selfsigned" then
|
||||
[ "mailserver-selfsigned-certificate.service" ]
|
||||
else
|
||||
[ "acme-finished-${cfg.fqdn}.target" ];
|
||||
|
||||
in
|
||||
{
|
||||
config = with cfg; lib.mkIf enable {
|
||||
config = lib.mkIf cfg.enable {
|
||||
# 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" ];
|
||||
script = ''
|
||||
# Create certificates if they do not exist yet
|
||||
@@ -53,21 +71,22 @@ in
|
||||
};
|
||||
|
||||
# Create maildir folder before dovecot startup
|
||||
systemd.services.dovecot2 = {
|
||||
systemd.services.dovecot = {
|
||||
wants = certificatesDeps;
|
||||
after = certificatesDeps;
|
||||
preStart = let
|
||||
preStart =
|
||||
let
|
||||
directories = lib.strings.escapeShellArgs (
|
||||
[ mailDirectory ]
|
||||
++ lib.optional (cfg.indexDir != null) cfg.indexDir
|
||||
[ cfg.mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
|
||||
);
|
||||
in ''
|
||||
in
|
||||
''
|
||||
# Create mail directory and set permissions. See
|
||||
# <https://doc.dovecot.org/main/core/config/shared_mailboxes.html#filesystem-permissions-1>.
|
||||
# Prevent world-readable paths, even temporarily.
|
||||
umask 007
|
||||
mkdir -p ${directories}
|
||||
chgrp "${vmailGroupName}" ${directories}
|
||||
chgrp "${cfg.vmailGroupName}" ${directories}
|
||||
chmod 02770 ${directories}
|
||||
'';
|
||||
};
|
||||
@@ -75,11 +94,12 @@ in
|
||||
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
|
||||
systemd.services.postfix = {
|
||||
wants = certificatesDeps;
|
||||
after = [ "dovecot2.service" ]
|
||||
after = [
|
||||
"dovecot.service"
|
||||
]
|
||||
++ lib.optional cfg.dkimSigning "rspamd.service"
|
||||
++ certificatesDeps;
|
||||
requires = [ "dovecot2.service" ]
|
||||
++ lib.optional cfg.dkimSigning "rspamd.service";
|
||||
requires = [ "dovecot.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service";
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,22 @@
|
||||
# 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, ... }:
|
||||
{
|
||||
config,
|
||||
options,
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}:
|
||||
|
||||
with (import ./common.nix {
|
||||
inherit
|
||||
config
|
||||
options
|
||||
lib
|
||||
pkgs
|
||||
;
|
||||
});
|
||||
|
||||
with config.mailserver;
|
||||
|
||||
@@ -28,7 +43,6 @@ let
|
||||
group = vmailGroupName;
|
||||
};
|
||||
|
||||
|
||||
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
|
||||
#!${pkgs.stdenv.shell}
|
||||
|
||||
@@ -46,8 +60,10 @@ let
|
||||
|
||||
# Copy user's sieve script to the correct location (if it exists). If it
|
||||
# is null, remove the file.
|
||||
${lib.concatMapStringsSep "\n" ({ name, sieveScript }:
|
||||
if lib.isString sieveScript then ''
|
||||
${lib.concatMapStringsSep "\n" (
|
||||
{ name, sieveScript }:
|
||||
if lib.isString sieveScript then
|
||||
''
|
||||
if (! test -d "${sieveDirectory}/${name}"); then
|
||||
mkdir -p "${sieveDirectory}/${name}"
|
||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
|
||||
@@ -57,34 +73,41 @@ let
|
||||
${sieveScript}
|
||||
EOF
|
||||
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
|
||||
'' else ''
|
||||
''
|
||||
else
|
||||
''
|
||||
if (test -f "${sieveDirectory}/${name}/default.sieve"); then
|
||||
rm "${sieveDirectory}/${name}/default.sieve"
|
||||
fi
|
||||
if (test -f "${sieveDirectory}/${name}.svbin"); then
|
||||
rm "${sieveDirectory}/${name}/default.svbin"
|
||||
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 {
|
||||
# assert that all accounts provide a password
|
||||
assertions = (map (acct: {
|
||||
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
|
||||
assertions = map (acct: {
|
||||
assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null;
|
||||
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
|
||||
warnings = (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.attrValues loginAccounts)));
|
||||
warnings =
|
||||
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.attrValues loginAccounts
|
||||
)
|
||||
);
|
||||
|
||||
# set the vmail gid to a specific value
|
||||
users.groups = {
|
||||
"${vmailGroupName}" = { gid = vmailUID; };
|
||||
"${vmailGroupName}" = {
|
||||
gid = vmailUID;
|
||||
};
|
||||
};
|
||||
|
||||
# define all users
|
||||
@@ -94,7 +117,7 @@ in {
|
||||
|
||||
systemd.services.activate-virtual-mail-users = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
before = [ "dovecot2.service" ];
|
||||
before = [ "dovecot.service" ];
|
||||
serviceConfig = {
|
||||
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)
|
||||
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
|
||||
@@ -27,6 +27,7 @@ groups = [
|
||||
"mailserver.loginAccounts",
|
||||
"mailserver.certificate",
|
||||
"mailserver.dkim",
|
||||
"mailserver.srs",
|
||||
"mailserver.dmarcReporting",
|
||||
"mailserver.fullTextSearch",
|
||||
"mailserver.redis",
|
||||
@@ -90,7 +91,9 @@ def print_option(option):
|
||||
key=option["name"],
|
||||
description=description or "",
|
||||
type=f"- type: {md_literal(option['type'])}",
|
||||
default=render_option_value(option, "default"),
|
||||
default=render_option_value(option, "defaultText")
|
||||
if "defaultText" in option
|
||||
else render_option_value(option, "default"),
|
||||
example=render_option_value(option, "example"),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -12,7 +12,15 @@ RETRY = 100
|
||||
|
||||
|
||||
def _send_mail(
|
||||
smtp_host, smtp_port, smtp_username, from_addr, from_pwd, to_addr, subject, starttls
|
||||
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(
|
||||
@@ -28,9 +36,10 @@ def _send_mail(
|
||||
)
|
||||
|
||||
retry = RETRY
|
||||
smtp_class = smtplib.SMTP_SSL if ssl else smtplib.SMTP
|
||||
while True:
|
||||
try:
|
||||
with smtplib.SMTP(smtp_host, port=smtp_port) as smtp:
|
||||
with smtp_class(smtp_host, port=smtp_port) as smtp:
|
||||
try:
|
||||
if starttls:
|
||||
smtp.starttls()
|
||||
@@ -73,7 +82,7 @@ def _read_mail(
|
||||
show_body=False,
|
||||
delete=True,
|
||||
):
|
||||
print("Reading mail from {imap_username}")
|
||||
print(f"Reading mail from {imap_username}")
|
||||
|
||||
message = None
|
||||
|
||||
@@ -171,6 +180,7 @@ def send_and_read(args):
|
||||
to_addr=args.to_addr,
|
||||
subject=subject,
|
||||
starttls=args.smtp_starttls,
|
||||
ssl=args.smtp_ssl,
|
||||
)
|
||||
|
||||
_read_mail(
|
||||
@@ -206,6 +216,7 @@ parser_send_and_read = subparsers.add_parser(
|
||||
parser_send_and_read.add_argument("--smtp-host", type=str)
|
||||
parser_send_and_read.add_argument("--smtp-port", type=str, default=25)
|
||||
parser_send_and_read.add_argument("--smtp-starttls", action="store_true")
|
||||
parser_send_and_read.add_argument("--smtp-ssl", action="store_true")
|
||||
parser_send_and_read.add_argument(
|
||||
"--smtp-username",
|
||||
type=str,
|
||||
|
||||
11
shell.nix
11
shell.nix
@@ -1,10 +1,9 @@
|
||||
(import
|
||||
(
|
||||
let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in
|
||||
(import (
|
||||
let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in
|
||||
fetchTarball {
|
||||
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
|
||||
sha256 = lock.nodes.flake-compat.locked.narHash;
|
||||
}
|
||||
)
|
||||
{ src = ./.; }
|
||||
).shellNix
|
||||
) { src = ./.; }).shellNix
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
name = "clamav";
|
||||
|
||||
nodes = {
|
||||
server = { pkgs, ... }:
|
||||
server =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
@@ -70,7 +71,10 @@
|
||||
mailserver = {
|
||||
enable = true;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.com" "example2.com" ];
|
||||
domains = [
|
||||
"example.com"
|
||||
"example2.com"
|
||||
];
|
||||
virusScanning = true;
|
||||
|
||||
loginAccounts = {
|
||||
@@ -90,7 +94,9 @@
|
||||
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
|
||||
};
|
||||
};
|
||||
client = { nodes, pkgs, ... }: let
|
||||
client =
|
||||
{ nodes, pkgs, ... }:
|
||||
let
|
||||
serverIP = nodes.server.networking.primaryIPAddress;
|
||||
clientIP = nodes.client.networking.primaryIPAddress;
|
||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
||||
@@ -98,13 +104,18 @@
|
||||
echo grep '${clientIP}' "$@" >&2
|
||||
exec grep '${clientIP}' "$@"
|
||||
'';
|
||||
in {
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./lib/config.nix
|
||||
];
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
fetchmail msmtp procmail findutils grep-ip
|
||||
fetchmail
|
||||
msmtp
|
||||
procmail
|
||||
findutils
|
||||
grep-ip
|
||||
];
|
||||
environment.etc = {
|
||||
"root/.fetchmailrc" = {
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
name = "external";
|
||||
|
||||
nodes = {
|
||||
server = { pkgs, ... }:
|
||||
server =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
@@ -36,19 +37,17 @@
|
||||
'';
|
||||
};
|
||||
|
||||
|
||||
mailserver = {
|
||||
enable = true;
|
||||
debug = true;
|
||||
debug.dovecot = true; # enabled for sieve script logging
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.com" "example2.com" ];
|
||||
domains = [
|
||||
"example.com"
|
||||
"example2.com"
|
||||
];
|
||||
rewriteMessageId = true;
|
||||
dkimKeyBits = 1535;
|
||||
dmarcReporting = {
|
||||
enable = true;
|
||||
domain = "example.com";
|
||||
organizationName = "ACME Corp";
|
||||
};
|
||||
dmarcReporting.enable = true;
|
||||
|
||||
loginAccounts = {
|
||||
"user1@example.com" = {
|
||||
@@ -71,7 +70,10 @@
|
||||
|
||||
extraVirtualAliases = {
|
||||
"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;
|
||||
@@ -80,12 +82,16 @@
|
||||
enable = true;
|
||||
autoIndex = true;
|
||||
# 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";
|
||||
};
|
||||
};
|
||||
};
|
||||
client = { nodes, pkgs, ... }: let
|
||||
client =
|
||||
{ nodes, pkgs, ... }:
|
||||
let
|
||||
serverIP = nodes.server.networking.primaryIPAddress;
|
||||
clientIP = nodes.client.networking.primaryIPAddress;
|
||||
grep-ip = pkgs.writeScriptBin "grep-ip" ''
|
||||
@@ -172,12 +178,21 @@
|
||||
assert needle in repr(response)
|
||||
imap.close()
|
||||
'';
|
||||
in {
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./lib/config.nix
|
||||
];
|
||||
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 = {
|
||||
"root/.fetchmailrc" = {
|
||||
@@ -471,9 +486,9 @@
|
||||
server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
|
||||
|
||||
client.succeed("imap-mark-spam >&2")
|
||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-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")
|
||||
server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-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"):
|
||||
# send 2 email from user2 to user1
|
||||
@@ -491,9 +506,9 @@
|
||||
# should fail because this folder is not indexed
|
||||
client.fail("search Junk a >&2")
|
||||
# check that search really goes through the indexer
|
||||
server.succeed("journalctl -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2")
|
||||
server.succeed("journalctl -u dovecot -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2")
|
||||
# check that Junk is not indexed
|
||||
server.fail("journalctl -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2")
|
||||
server.fail("journalctl -u dovecot -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2")
|
||||
|
||||
with subtest("dmarc reporting"):
|
||||
server.systemctl("start rspamd-dmarc-reporter.service")
|
||||
@@ -501,10 +516,10 @@
|
||||
with subtest("no warnings or errors"):
|
||||
server.fail("journalctl -u postfix | grep -i error >&2")
|
||||
server.fail("journalctl -u postfix | grep -i warning >&2")
|
||||
server.fail("journalctl -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | 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
|
||||
server.fail(
|
||||
"journalctl -u dovecot2 | \
|
||||
"journalctl -u dovecot -u dovecot2 | \
|
||||
grep -v 'Expunged message reappeared, giving a new UID' | \
|
||||
grep -v 'Time moved forwards' | \
|
||||
grep -i warning >&2"
|
||||
|
||||
@@ -30,9 +30,14 @@ let
|
||||
'';
|
||||
};
|
||||
|
||||
hashPassword = password: pkgs.runCommand
|
||||
"password-${password}-hashed"
|
||||
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } ''
|
||||
hashPassword =
|
||||
password:
|
||||
pkgs.runCommand "password-${password}-hashed"
|
||||
{
|
||||
buildInputs = [ pkgs.mkpasswd ];
|
||||
inherit password;
|
||||
}
|
||||
''
|
||||
mkpasswd -sm bcrypt <<<"$password" > $out
|
||||
'';
|
||||
|
||||
@@ -43,7 +48,9 @@ in
|
||||
name = "internal";
|
||||
|
||||
nodes = {
|
||||
machine = { pkgs, ... }: {
|
||||
machine =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
./../default.nix
|
||||
./lib/config.nix
|
||||
@@ -55,7 +62,8 @@ in
|
||||
(pkgs.writeScriptBin "mail-check" ''
|
||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||
'')
|
||||
] ++ (with pkgs; [
|
||||
]
|
||||
++ (with pkgs; [
|
||||
curl
|
||||
openssl
|
||||
netcat
|
||||
@@ -64,7 +72,10 @@ in
|
||||
mailserver = {
|
||||
enable = true;
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.com" "domain.com" ];
|
||||
domains = [
|
||||
"example.com"
|
||||
"domain.com"
|
||||
];
|
||||
localDnsResolver = false;
|
||||
|
||||
loginAccounts = {
|
||||
@@ -73,7 +84,7 @@ in
|
||||
};
|
||||
"user2@example.com" = {
|
||||
hashedPasswordFile = hashedPasswordFile;
|
||||
aliasesRegexp = [''/^user2.*@domain\.com$/''];
|
||||
aliasesRegexp = [ ''/^user2.*@domain\.com$/'' ];
|
||||
};
|
||||
"send-only@example.com" = {
|
||||
hashedPasswordFile = hashPassword "send-only";
|
||||
@@ -88,18 +99,24 @@ in
|
||||
|
||||
vmailGroupName = "vmail";
|
||||
vmailUID = 5000;
|
||||
indexDir = "/var/lib/dovecot/indices";
|
||||
|
||||
enableImap = false;
|
||||
};
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
testScript =
|
||||
{
|
||||
nodes,
|
||||
...
|
||||
}:
|
||||
''
|
||||
machine.start()
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
|
||||
# Regression test for https://gitlab.com/simple-nixos-mailserver/nixos-mailserver/-/issues/205
|
||||
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(
|
||||
" ".join(
|
||||
[
|
||||
@@ -117,13 +134,13 @@ in
|
||||
]
|
||||
)
|
||||
)
|
||||
# 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(
|
||||
" ".join(
|
||||
[
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-port 465",
|
||||
"--smtp-ssl",
|
||||
"--smtp-host localhost",
|
||||
"--imap-host localhost",
|
||||
"--imap-username user2@example.com",
|
||||
@@ -137,7 +154,7 @@ in
|
||||
)
|
||||
|
||||
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(
|
||||
" ".join(
|
||||
[
|
||||
@@ -157,13 +174,14 @@ in
|
||||
)
|
||||
|
||||
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(
|
||||
" ".join(
|
||||
[
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-port 465",
|
||||
"--smtp-ssl",
|
||||
"--smtp-host localhost",
|
||||
"--imap-host localhost",
|
||||
"--smtp-username user2@example.com",
|
||||
@@ -179,6 +197,11 @@ in
|
||||
with subtest("vmail gid is set correctly"):
|
||||
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"):
|
||||
machine.wait_for_open_port(25)
|
||||
# TODO put this blocking into the systemd units
|
||||
|
||||
@@ -7,7 +7,9 @@ in
|
||||
name = "ldap";
|
||||
|
||||
nodes = {
|
||||
machine = { pkgs, ... }: {
|
||||
machine =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
./../default.nix
|
||||
./lib/config.nix
|
||||
@@ -23,7 +25,8 @@ in
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeScriptBin "mail-check" ''
|
||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||
'')];
|
||||
'')
|
||||
];
|
||||
|
||||
environment.etc.bind-password.text = bindPassword;
|
||||
|
||||
@@ -87,6 +90,7 @@ in
|
||||
fqdn = "mail.example.com";
|
||||
domains = [ "example.com" ];
|
||||
localDnsResolver = false;
|
||||
indexDir = "/var/lib/dovecot/indices";
|
||||
|
||||
ldap = {
|
||||
enable = true;
|
||||
@@ -112,7 +116,12 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
testScript =
|
||||
{
|
||||
nodes,
|
||||
...
|
||||
}:
|
||||
''
|
||||
import sys
|
||||
import re
|
||||
|
||||
@@ -148,7 +157,7 @@ in
|
||||
machine.succeed("ls -l /run/postfix/*.cf | grep -e '-rw------- 1 root root'")
|
||||
machine.succeed("ls -l /run/dovecot2/dovecot-ldap.conf.ext | grep -e '-rw------- 1 root root'")
|
||||
|
||||
with subtest("Test account/mail address binding"):
|
||||
with subtest("Test account/mail address binding via explicit TLS"):
|
||||
machine.fail(" ".join([
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
@@ -165,11 +174,11 @@ in
|
||||
]))
|
||||
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([
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-port 465",
|
||||
"--smtp-ssl",
|
||||
"--smtp-host localhost",
|
||||
"--smtp-username alice@example.com",
|
||||
"--imap-host localhost",
|
||||
@@ -181,7 +190,7 @@ in
|
||||
"--ignore-dkim-spf"
|
||||
]))
|
||||
|
||||
with subtest("Test mail forwarding works"):
|
||||
with subtest("Test mail forwarding via explicit TLS works"):
|
||||
machine.succeed(" ".join([
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
@@ -197,11 +206,11 @@ in
|
||||
"--ignore-dkim-spf"
|
||||
]))
|
||||
|
||||
with subtest("Test cannot send mail from forwarded address"):
|
||||
with subtest("Test cannot send mail via implicit TLS from forwarded address"):
|
||||
machine.fail(" ".join([
|
||||
"mail-check send-and-read",
|
||||
"--smtp-port 587",
|
||||
"--smtp-starttls",
|
||||
"--smtp-port 465",
|
||||
"--smtp-ssl",
|
||||
"--smtp-host localhost",
|
||||
"--smtp-username bob@example.com",
|
||||
"--imap-host localhost",
|
||||
@@ -214,5 +223,9 @@ in
|
||||
]))
|
||||
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 = 2048; # 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,27 +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/>
|
||||
|
||||
{
|
||||
name = "minimal";
|
||||
|
||||
nodes.machine = {
|
||||
imports = [ ./../default.nix ];
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
machine.wait_for_unit("multi-user.target");
|
||||
'';
|
||||
}
|
||||
@@ -1,22 +1,33 @@
|
||||
# This tests is used to test features requiring several mail domains.
|
||||
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
hashPassword = password: pkgs.runCommand
|
||||
"password-${password}-hashed"
|
||||
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; }
|
||||
hashPassword =
|
||||
password:
|
||||
pkgs.runCommand "password-${password}-hashed"
|
||||
{
|
||||
buildInputs = [ pkgs.mkpasswd ];
|
||||
inherit password;
|
||||
}
|
||||
''
|
||||
mkpasswd -sm bcrypt <<<"$password" > $out
|
||||
'';
|
||||
|
||||
password = pkgs.writeText "password" "password";
|
||||
|
||||
domainGenerator = domain: { pkgs, ... }: {
|
||||
imports = [../default.nix];
|
||||
domainGenerator =
|
||||
domain:
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
./lib/config.nix
|
||||
];
|
||||
environment.systemPackages = with pkgs; [ netcat ];
|
||||
virtualisation.memorySize = 1024;
|
||||
mailserver = {
|
||||
@@ -34,8 +45,14 @@ let
|
||||
};
|
||||
services.dnsmasq = {
|
||||
enable = true;
|
||||
settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ];
|
||||
settings.mx-host = [
|
||||
"domain1.com,domain1,10"
|
||||
"domain2.com,domain2,10"
|
||||
];
|
||||
};
|
||||
|
||||
# breaks the test, due to running into DNS timeouts
|
||||
services.postfix-tlspol.configurePostfix = lib.mkForce false;
|
||||
};
|
||||
|
||||
in
|
||||
@@ -44,22 +61,33 @@ in
|
||||
name = "multiple";
|
||||
|
||||
nodes = {
|
||||
domain1 = {...}: {
|
||||
domain1 =
|
||||
{ ... }:
|
||||
{
|
||||
imports = [
|
||||
../default.nix
|
||||
(domainGenerator "domain1.com")
|
||||
];
|
||||
mailserver.forwards = {
|
||||
"non-local@domain1.com" = ["user@domain2.com" "user@domain1.com"];
|
||||
"non@domain1.com" = ["user@domain2.com" "user@domain1.com"];
|
||||
"non-local@domain1.com" = [
|
||||
"user@domain2.com"
|
||||
"user@domain1.com"
|
||||
];
|
||||
"non@domain1.com" = [
|
||||
"user@domain2.com"
|
||||
"user@domain1.com"
|
||||
];
|
||||
};
|
||||
};
|
||||
domain2 = domainGenerator "domain2.com";
|
||||
client = { pkgs, ... }: {
|
||||
client =
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeScriptBin "mail-check" ''
|
||||
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
|
||||
'')];
|
||||
'')
|
||||
];
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
@@ -76,14 +104,14 @@ in
|
||||
"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(
|
||||
"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(
|
||||
"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"
|
||||
)
|
||||
'';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user