Merge branch 'cleanup' into 'master'

Format with nixfmt, drop redundant parentheses

See merge request simple-nixos-mailserver/nixos-mailserver!415
This commit is contained in:
Martin Weinelt 2025-06-15 02:45:24 +00:00
commit b555b3e8dc
21 changed files with 2090 additions and 1681 deletions

View file

@ -1,11 +1,11 @@
{ nixpkgs, pulls, ... }: { nixpkgs, pulls, ... }:
let let
pkgs = import nixpkgs {}; pkgs = import nixpkgs { };
prs = builtins.fromJSON (builtins.readFile pulls); prs = builtins.fromJSON (builtins.readFile pulls);
prJobsets = pkgs.lib.mapAttrs (num: info: prJobsets = pkgs.lib.mapAttrs (num: info: {
{ enabled = 1; enabled = 1;
hidden = false; hidden = false;
description = "PR ${num}: ${info.title}"; description = "PR ${num}: ${info.title}";
checkinterval = 300; checkinterval = 300;
@ -15,8 +15,7 @@ let
keepnr = 1; keepnr = 1;
type = 1; type = 1;
flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head"; flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
} }) prs;
) prs;
mkFlakeJobset = branch: { mkFlakeJobset = branch: {
description = "Build ${branch} branch of Simple NixOS MailServer"; description = "Build ${branch} branch of Simple NixOS MailServer";
checkinterval = 300; checkinterval = 300;
@ -41,8 +40,9 @@ let
jobsets = desc; jobsets = desc;
}; };
in { in
jobsets = pkgs.runCommand "spec-jobsets.json" {} '' {
jobsets = pkgs.runCommand "spec-jobsets.json" { } ''
cat >$out <<EOF cat >$out <<EOF
${builtins.toJSON desc} ${builtins.toJSON desc}
EOF EOF

View file

@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, lib, pkgs, ... }: {
config,
lib,
pkgs,
...
}:
with lib; with lib;
@ -56,14 +61,17 @@ in
domains = mkOption { domains = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
example = [ "example.com" ]; example = [ "example.com" ];
default = []; default = [ ];
description = "The domains that this mail server serves."; description = "The domains that this mail server serves.";
}; };
certificateDomains = mkOption { certificateDomains = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
example = [ "imap.example.com" "pop3.example.com" ]; example = [
default = []; "imap.example.com"
"pop3.example.com"
];
default = [ ];
description = '' description = ''
({option}`mailserver.certificateScheme` == `acme-nginx`) ({option}`mailserver.certificateScheme` == `acme-nginx`)
@ -79,7 +87,10 @@ in
}; };
loginAccounts = mkOption { loginAccounts = mkOption {
type = types.attrsOf (types.submodule ({ name, ... }: { type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = { options = {
name = mkOption { name = mkOption {
type = types.str; type = types.str;
@ -118,8 +129,11 @@ in
aliases = mkOption { aliases = mkOption {
type = with types; listOf types.str; type = with types; listOf types.str;
example = ["abuse@example.com" "postmaster@example.com"]; example = [
default = []; "abuse@example.com"
"postmaster@example.com"
];
default = [ ];
description = '' description = ''
A list of aliases of this login account. A list of aliases of this login account.
Note: Use list entries like "@example.com" to create a catchAll Note: Use list entries like "@example.com" to create a catchAll
@ -129,8 +143,8 @@ in
aliasesRegexp = mkOption { aliasesRegexp = mkOption {
type = with types; listOf types.str; type = with types; listOf types.str;
example = [''/^tom\..*@domain\.com$/'']; example = [ ''/^tom\..*@domain\.com$/'' ];
default = []; default = [ ];
description = '' description = ''
Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex). Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex).
''; '';
@ -138,8 +152,11 @@ in
catchAll = mkOption { catchAll = mkOption {
type = with types; listOf (enum cfg.domains); type = with types; listOf (enum cfg.domains);
example = ["example.com" "example2.com"]; example = [
default = []; "example.com"
"example2.com"
];
default = [ ];
description = '' description = ''
For which domains should this account act as a catch all? For which domains should this account act as a catch all?
Note: Does not allow sending from all addresses of these domains. Note: Does not allow sending from all addresses of these domains.
@ -202,7 +219,9 @@ in
}; };
config.name = mkDefault name; config.name = mkDefault name;
})); }
)
);
example = { example = {
user1 = { user1 = {
hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/"; hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
@ -220,7 +239,7 @@ in
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt' nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
``` ```
''; '';
default = {}; default = { };
}; };
ldap = { ldap = {
@ -284,7 +303,11 @@ in
}; };
searchScope = mkOption { searchScope = mkOption {
type = types.enum [ "sub" "base" "one" ]; type = types.enum [
"sub"
"base"
"one"
];
default = "sub"; default = "sub";
description = '' description = ''
Search scope below which users accounts are looked for. Search scope below which users accounts are looked for.
@ -419,14 +442,22 @@ in
autoIndexExclude = mkOption { autoIndexExclude = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = [ ]; default = [ ];
example = [ "\\Trash" "SomeFolder" "Other/*" ]; example = [
"\\Trash"
"SomeFolder"
"Other/*"
];
description = '' description = ''
Mailboxes to exclude from automatic indexing. Mailboxes to exclude from automatic indexing.
''; '';
}; };
enforced = mkOption { enforced = mkOption {
type = types.enum [ "yes" "no" "body" ]; type = types.enum [
"yes"
"no"
"body"
];
default = "no"; default = "no";
description = '' description = ''
Fail searches when no index is available. If set to Fail searches when no index is available. If set to
@ -439,7 +470,10 @@ in
languages = mkOption { languages = mkOption {
type = types.nonEmptyListOf types.str; type = types.nonEmptyListOf types.str;
default = [ "en" ]; default = [ "en" ];
example = [ "en" "de" ]; example = [
"en"
"de"
];
description = '' description = ''
A list of languages that the full text search should detect. A list of languages that the full text search should detect.
At least one language must be specified. At least one language must be specified.
@ -488,7 +522,10 @@ in
}; };
lmtpSaveToDetailMailbox = mkOption { lmtpSaveToDetailMailbox = mkOption {
type = types.enum ["yes" "no"]; type = types.enum [
"yes"
"no"
];
default = "yes"; default = "yes";
description = '' description = ''
If an email address is delimited by a "+", should it be filed into a If an email address is delimited by a "+", should it be filed into a
@ -514,17 +551,23 @@ in
}; };
extraVirtualAliases = mkOption { extraVirtualAliases = mkOption {
type = let type =
let
loginAccount = mkOptionType { loginAccount = mkOptionType {
name = "Login Account"; name = "Login Account";
check = (account: builtins.elem account (builtins.attrNames cfg.loginAccounts)); check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts);
}; };
in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount)); in
with types;
attrsOf (either loginAccount (nonEmptyListOf loginAccount));
example = { example = {
"info@example.com" = "user1@example.com"; "info@example.com" = "user1@example.com";
"postmaster@example.com" = "user1@example.com"; "postmaster@example.com" = "user1@example.com";
"abuse@example.com" = "user1@example.com"; "abuse@example.com" = "user1@example.com";
"multi@example.com" = [ "user1@example.com" "user2@example.com" ]; "multi@example.com" = [
"user1@example.com"
"user2@example.com"
];
}; };
description = '' description = ''
Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that Virtual Aliases. A virtual alias `"info@example.com" = "user1@example.com"` means that
@ -537,7 +580,7 @@ in
example all mails for `multi@example.com` will be forwarded to both example all mails for `multi@example.com` will be forwarded to both
`user1@example.com` and `user2@example.com`. `user1@example.com` and `user2@example.com`.
''; '';
default = {}; default = { };
}; };
forwards = mkOption { forwards = mkOption {
@ -554,28 +597,34 @@ in
can't send mail as `user@example.com`. Also, this option can't send mail as `user@example.com`. Also, this option
allows to forward mails to external addresses. allows to forward mails to external addresses.
''; '';
default = {}; default = { };
}; };
rejectSender = mkOption { rejectSender = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
example = [ "example.com" "spammer@example.net" ]; example = [
"example.com"
"spammer@example.net"
];
description = '' description = ''
Reject emails from these addresses from unauthorized senders. Reject emails from these addresses from unauthorized senders.
Use if a spammer is using the same domain or the same sender over and over. Use if a spammer is using the same domain or the same sender over and over.
''; '';
default = []; default = [ ];
}; };
rejectRecipients = mkOption { rejectRecipients = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
example = [ "sales@example.com" "info@example.com" ]; example = [
"sales@example.com"
"info@example.com"
];
description = '' description = ''
Reject emails addressed to these local addresses from unauthorized senders. Reject emails addressed to these local addresses from unauthorized senders.
Use if a spammer has found email addresses in a catchall domain but you do Use if a spammer has found email addresses in a catchall domain but you do
not want to disable the catchall. not want to disable the catchall.
''; '';
default = []; default = [ ];
}; };
vmailUID = mkOption { vmailUID = mkOption {
@ -673,12 +722,30 @@ in
}; };
}; };
certificateScheme = let certificateScheme =
schemes = [ "manual" "selfsigned" "acme-nginx" "acme" ]; let
translate = i: warn "Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${(builtins.elemAt schemes (i - 1))}\"'." schemes = [
"manual"
"selfsigned"
"acme-nginx"
"acme"
];
translate =
i:
warn
"Setting mailserver.certificateScheme by number is deprecated, please use names instead: 'mailserver.certificateScheme = ${builtins.toString i}' can be replaced by 'mailserver.certificateScheme = \"${
(builtins.elemAt schemes (i - 1))
}\"'."
(builtins.elemAt schemes (i - 1)); (builtins.elemAt schemes (i - 1));
in mkOption { in
type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes); mkOption {
type =
with types;
coercedTo (enum [
1
2
3
]) translate (enum schemes);
default = "selfsigned"; default = "selfsigned";
description = '' description = ''
The scheme to use for managing TLS certificates: The scheme to use for managing TLS certificates:
@ -851,7 +918,10 @@ in
}; };
dkimKeyType = mkOption { dkimKeyType = mkOption {
type = types.enum [ "rsa" "ed25519" ]; type = types.enum [
"rsa"
"ed25519"
];
default = "rsa"; default = "rsa";
description = '' description = ''
The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018). The key type used for generating DKIM keys. ED25519 was introduced in RFC6376 (2018).
@ -901,7 +971,7 @@ in
}; };
domain = mkOption { domain = mkOption {
type = types.enum (cfg.domains); type = types.enum cfg.domains;
example = "example.com"; example = "example.com";
description = '' description = ''
The domain from which outgoing DMARC reports are served. The domain from which outgoing DMARC reports are served.
@ -938,7 +1008,7 @@ in
excludeDomains = mkOption { excludeDomains = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = []; default = [ ];
description = '' description = ''
List of domains or eSLDs to be excluded from DMARC reports. List of domains or eSLDs to be excluded from DMARC reports.
''; '';
@ -1150,7 +1220,15 @@ in
compression = { compression = {
method = mkOption { method = mkOption {
type = types.nullOr (types.enum ["none" "lz4" "zstd" "zlib" "lzma"]); type = types.nullOr (
types.enum [
"none"
"lz4"
"zstd"
"zlib"
"lzma"
]
);
default = null; default = null;
description = "Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4."; description = "Leaving this unset allows borg to choose. The default for borg 1.1.4 is lz4.";
}; };
@ -1208,14 +1286,14 @@ in
locations = mkOption { locations = mkOption {
type = types.listOf types.path; type = types.listOf types.path;
default = [cfg.mailDirectory]; default = [ cfg.mailDirectory ];
defaultText = lib.literalExpression "[ config.mailserver.mailDirectory ]"; defaultText = lib.literalExpression "[ config.mailserver.mailDirectory ]";
description = "The locations that are to be backed up by borg."; description = "The locations that are to be backed up by borg.";
}; };
extraArgumentsForInit = mkOption { extraArgumentsForInit = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
default = ["--critical"]; default = [ "--critical" ];
description = "Additional arguments to add to the borg init command line."; description = "Additional arguments to add to the borg init command line.";
}; };

View file

@ -20,7 +20,16 @@
}; };
}; };
outputs = { self, blobs, git-hooks, nixpkgs, nixpkgs-25_05, ... }: let outputs =
{
self,
blobs,
git-hooks,
nixpkgs,
nixpkgs-25_05,
...
}:
let
lib = nixpkgs.lib; lib = nixpkgs.lib;
system = "x86_64-linux"; system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
@ -44,13 +53,16 @@
"multiple" "multiple"
]; ];
genTest = testName: release: let genTest =
testName: release:
let
pkgs = release.pkgs; pkgs = release.pkgs;
nixos-lib = import (release.nixpkgs + "/nixos/lib") { nixos-lib = import (release.nixpkgs + "/nixos/lib") {
inherit (pkgs) lib; inherit (pkgs) lib;
}; };
in { in
name = "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}"; {
name = "${testName}-${builtins.replaceStrings [ "." ] [ "_" ] release.name}";
value = nixos-lib.runTest { value = nixos-lib.runTest {
hostPkgs = pkgs; hostPkgs = pkgs;
imports = [ ./tests/${testName}.nix ]; imports = [ ./tests/${testName}.nix ];
@ -65,13 +77,13 @@
# external-21_05 = <derivation>; # external-21_05 = <derivation>;
# ... # ...
# } # }
allTests = lib.listToAttrs ( allTests = lib.listToAttrs (lib.flatten (map (t: map (r: genTest t r) releases) testNames));
lib.flatten (map (t: map (r: genTest t r) releases) testNames));
mailserverModule = import ./.; mailserverModule = import ./.;
# Generate a MarkDown file describing the options of the NixOS mailserver module # Generate a MarkDown file describing the options of the NixOS mailserver module
optionsDoc = let optionsDoc =
let
eval = lib.evalModules { eval = lib.evalModules {
modules = [ modules = [
mailserverModule mailserverModule
@ -90,10 +102,15 @@
} }
]; ];
}; };
options = builtins.toFile "options.json" (builtins.toJSON options = builtins.toFile "options.json" (
(lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") builtins.toJSON (
(lib.optionAttrSetToDocList eval.options))); lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver") (
in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } '' lib.optionAttrSetToDocList eval.options
)
)
);
in
pkgs.runCommand "options.md" { buildInputs = [ pkgs.python3Minimal ]; } ''
echo "Generating options.md from ${options}" echo "Generating options.md from ${options}"
python ${./scripts/generate-options.py} ${options} > $out python ${./scripts/generate-options.py} ${options} > $out
echo $out echo $out
@ -101,15 +118,22 @@
documentation = pkgs.stdenv.mkDerivation { documentation = pkgs.stdenv.mkDerivation {
name = "documentation"; name = "documentation";
src = lib.sourceByRegex ./docs ["logo\\.png" "conf\\.py" "Makefile" ".*\\.rst"]; src = lib.sourceByRegex ./docs [
buildInputs = [( "logo\\.png"
pkgs.python3.withPackages (p: with p; [ "conf\\.py"
"Makefile"
".*\\.rst"
];
buildInputs = [
(pkgs.python3.withPackages (
p: with p; [
sphinx sphinx
sphinx_rtd_theme sphinx_rtd_theme
myst-parser myst-parser
linkify-it-py linkify-it-py
]) ]
)]; ))
];
buildPhase = '' buildPhase = ''
cp ${optionsDoc} options.md cp ${optionsDoc} options.md
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451 # Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
@ -121,7 +145,8 @@
''; '';
}; };
in { in
{
nixosModules = rec { nixosModules = rec {
mailserver = mailserverModule; mailserver = mailserverModule;
default = mailserver; default = mailserver;
@ -153,6 +178,7 @@
# nix # nix
deadnix.enable = true; deadnix.enable = true;
nixfmt-rfc-style.enable = true;
# python # python
pyright.enable = true; pyright.enable = true;
@ -183,11 +209,16 @@
}; };
devShells.${system}.default = pkgs.mkShellNoCC { devShells.${system}.default = pkgs.mkShellNoCC {
inputsFrom = [ documentation ]; inputsFrom = [ documentation ];
packages = with pkgs; [ packages =
with pkgs;
[
glab glab
] ++ self.checks.${system}.pre-commit.enabledPackages; ]
++ self.checks.${system}.pre-commit.enabledPackages;
shellHook = self.checks.${system}.pre-commit.shellHook; shellHook = self.checks.${system}.pre-commit.shellHook;
}; };
devShell.${system} = self.devShells.${system}.default; # compatibility devShell.${system} = self.devShells.${system}.default; # compatibility
formatter.${system} = pkgs.nixfmt-tree;
}; };
} }

View file

@ -14,28 +14,44 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.mailserver.borgbackup; cfg = config.mailserver.borgbackup;
methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method; methodFragment = lib.optional (cfg.compression.method != null) cfg.compression.method;
autoFragment = autoFragment =
if cfg.compression.auto && cfg.compression.method == null if cfg.compression.auto && cfg.compression.method == null then
then throw "compression.method must be set when using auto." throw "compression.method must be set when using auto."
else lib.optional cfg.compression.auto "auto"; else
lib.optional cfg.compression.auto "auto";
levelFragment = levelFragment =
if cfg.compression.level != null && cfg.compression.method == null if cfg.compression.level != null && cfg.compression.method == null then
then throw "compression.method must be set when using compression.level." throw "compression.method must be set when using compression.level."
else lib.optional (cfg.compression.level != null) (toString cfg.compression.level); else
compressionFragment = lib.concatStringsSep "," (lib.flatten [autoFragment methodFragment levelFragment]); lib.optional (cfg.compression.level != null) (toString cfg.compression.level);
compressionFragment = lib.concatStringsSep "," (
lib.flatten [
autoFragment
methodFragment
levelFragment
]
);
compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}"; compression = lib.optionalString (compressionFragment != "") "--compression ${compressionFragment}";
encryptionFragment = cfg.encryption.method; encryptionFragment = cfg.encryption.method;
passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile; passphraseFile = lib.escapeShellArg cfg.encryption.passphraseFile;
passphraseFragment = lib.optionalString (cfg.encryption.method != "none") passphraseFragment = lib.optionalString (cfg.encryption.method != "none") (
(if cfg.encryption.passphraseFile != null then ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"'' if cfg.encryption.passphraseFile != null then
else throw "passphraseFile must be set when using encryption."); ''env BORG_PASSPHRASE="$(cat ${passphraseFile})"''
else
throw "passphraseFile must be set when using encryption."
);
locations = lib.escapeShellArgs cfg.locations; locations = lib.escapeShellArgs cfg.locations;
name = lib.escapeShellArg cfg.name; name = lib.escapeShellArg cfg.name;
@ -55,7 +71,8 @@ let
${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations} ${passphraseFragment} ${pkgs.borgbackup}/bin/borg create ${extraCreateArgs} ${compression} ::${name} ${locations}
${cmdPostexec} ${cmdPostexec}
''; '';
in { in
{
config = lib.mkIf (config.mailserver.enable && cfg.enable) { config = lib.mkIf (config.mailserver.enable && cfg.enable) {
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
borgbackup borgbackup

View file

@ -14,43 +14,62 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib }: {
config,
pkgs,
lib,
}:
let let
cfg = config.mailserver; cfg = config.mailserver;
in in
{ {
# cert :: PATH # cert :: PATH
certificatePath = if cfg.certificateScheme == "manual" certificatePath =
then cfg.certificateFile if cfg.certificateScheme == "manual" then
else if cfg.certificateScheme == "selfsigned" cfg.certificateFile
then "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem" else if cfg.certificateScheme == "selfsigned" then
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" "${cfg.certificateDirectory}/cert-${cfg.fqdn}.pem"
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem" else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
else throw "unknown certificate scheme"; "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/fullchain.pem"
else
throw "unknown certificate scheme";
# key :: PATH # key :: PATH
keyPath = if cfg.certificateScheme == "manual" keyPath =
then cfg.keyFile if cfg.certificateScheme == "manual" then
else if cfg.certificateScheme == "selfsigned" cfg.keyFile
then "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem" else if cfg.certificateScheme == "selfsigned" then
else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" "${cfg.certificateDirectory}/key-${cfg.fqdn}.pem"
then "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem" else if cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx" then
else throw "unknown certificate scheme"; "${config.security.acme.certs.${cfg.acmeCertificateName}.directory}/key.pem"
else
throw "unknown certificate scheme";
passwordFiles = let passwordFiles =
let
mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash; mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
in in
lib.mapAttrs (name: value: lib.mapAttrs (
name: value:
if value.hashedPasswordFile == null then if value.hashedPasswordFile == null then
builtins.toString (mkHashFile name value.hashedPassword) builtins.toString (mkHashFile name value.hashedPassword)
else value.hashedPasswordFile) cfg.loginAccounts; else
value.hashedPasswordFile
) cfg.loginAccounts;
# Appends the LDAP bind password to files to avoid writing this # Appends the LDAP bind password to files to avoid writing this
# password into the Nix store. # password into the Nix store.
appendLdapBindPwd = { appendLdapBindPwd =
name, file, prefix, suffix ? "", passwordFile, destination {
}: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' name,
file,
prefix,
suffix ? "",
passwordFile,
destination,
}:
pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
#!${pkgs.stdenv.shell} #!${pkgs.stdenv.shell}
set -euo pipefail set -euo pipefail

View file

@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
with (import ./common.nix { inherit config pkgs lib; }); with (import ./common.nix { inherit config pkgs lib; });
@ -28,10 +33,14 @@ let
ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext"; ldapConfFile = "${passwdDir}/dovecot-ldap.conf.ext";
boolToYesNo = x: if x then "yes" else "no"; boolToYesNo = x: if x then "yes" else "no";
listToLine = lib.concatStringsSep " "; listToLine = lib.concatStringsSep " ";
listToMultiAttrs = keyPrefix: attrs: lib.listToAttrs (lib.imap1 (n: x: { listToMultiAttrs =
name = "${keyPrefix}${if n==1 then "" else toString n}"; keyPrefix: attrs:
lib.listToAttrs (
lib.imap1 (n: x: {
name = "${keyPrefix}${if n == 1 then "" else toString n}";
value = x; value = x;
}) attrs); }) attrs
);
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs"; maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8"; maildirUTF8FolderNames = lib.optionalString cfg.useUTF8FolderNames ":UTF-8";
@ -39,9 +48,7 @@ let
# maildir in format "/${domain}/${user}" # maildir in format "/${domain}/${user}"
dovecotMaildir = dovecotMaildir =
"maildir:${cfg.mailDirectory}/%{domain}/%{username}${maildirLayoutAppendix}${maildirUTF8FolderNames}" "maildir:${cfg.mailDirectory}/%{domain}/%{username}${maildirLayoutAppendix}${maildirUTF8FolderNames}"
+ (lib.optionalString (cfg.indexDir != null) + (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}");
":INDEX=${cfg.indexDir}/%{domain}/%{username}"
);
postfixCfg = config.services.postfix; postfixCfg = config.services.postfix;
@ -93,7 +100,9 @@ let
# Prevent world-readable password files, even temporarily. # Prevent world-readable password files, even temporarily.
umask 077 umask 077
for f in ${builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts)}; do for f in ${
builtins.toString (lib.mapAttrsToList (name: _: passwordFiles."${name}") cfg.loginAccounts)
}; do
if [ ! -f "$f" ]; then if [ ! -f "$f" ]; then
echo "Expected password hash file $f does not exist!" echo "Expected password hash file $f does not exist!"
exit 1 exit 1
@ -101,34 +110,49 @@ let
done done
cat <<EOF > ${passwdFile} cat <<EOF > ${passwdFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: _: ${lib.concatStringsSep "\n" (
"${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::" lib.mapAttrsToList (
) cfg.loginAccounts)} name: _: "${name}:${"$(head -n 1 ${passwordFiles."${name}"})"}::::::"
) cfg.loginAccounts
)}
EOF EOF
cat <<EOF > ${userdbFile} cat <<EOF > ${userdbFile}
${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: ${lib.concatStringsSep "\n" (
lib.mapAttrsToList (
name: value:
"${name}:::::::" "${name}:::::::"
+ lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}" + lib.optionalString (value.quota != null) "userdb_quota_rule=*:storage=${value.quota}"
) cfg.loginAccounts)} ) cfg.loginAccounts
)}
EOF 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; junkMailboxNumber = builtins.length junkMailboxes;
# The assertion garantees there is exactly one Junk mailbox. # The assertion garantees there is exactly one Junk mailbox.
junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else ""; junkMailboxName = if junkMailboxNumber == 1 then builtins.elemAt junkMailboxes 0 else "";
mkLdapSearchScope = scope: ( mkLdapSearchScope =
if scope == "sub" then "subtree" scope:
else if scope == "one" then "onelevel" (
else scope if scope == "sub" then
"subtree"
else if scope == "one" then
"onelevel"
else
scope
); );
ftsPluginSettings = { ftsPluginSettings = {
fts = "flatcurve"; fts = "flatcurve";
fts_languages = listToLine cfg.fullTextSearch.languages; 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_tokenizer_email_address = "maxlen=100"; # default 254 too large for Xapian
fts_flatcurve_substring_search = boolToYesNo cfg.fullTextSearch.substringSearch; fts_flatcurve_substring_search = boolToYesNo cfg.fullTextSearch.substringSearch;
fts_filters = listToLine cfg.fullTextSearch.filters; fts_filters = listToLine cfg.fullTextSearch.filters;
@ -139,7 +163,9 @@ let
in in
{ {
config = with cfg; lib.mkIf enable { config =
with cfg;
lib.mkIf enable {
assertions = [ assertions = [
{ {
assertion = junkMailboxNumber == 1; assertion = junkMailboxNumber == 1;
@ -148,18 +174,19 @@ in
]; ];
warnings = warnings =
(lib.optional ( lib.optional
(builtins.length cfg.fullTextSearch.languages > 1) && (
(builtins.elem "stopwords" cfg.fullTextSearch.filters) (builtins.length cfg.fullTextSearch.languages > 1)
) '' && (builtins.elem "stopwords" cfg.fullTextSearch.filters)
)
''
Using stopwords in `mailserver.fullTextSearch.filters` with multiple Using stopwords in `mailserver.fullTextSearch.filters` with multiple
languages in `mailserver.fullTextSearch.languages` configured WILL languages in `mailserver.fullTextSearch.languages` configured WILL
cause some searches to fail. cause some searches to fail.
The recommended solution is to NOT use the stopword filter when The recommended solution is to NOT use the stopword filter when
multiple languages are present in the configuration. multiple languages are present in the configuration.
'') '';
;
# for sieve-test. Shelling it in on demand usually doesnt' work, as it reads # for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
# the global config and tries to open shared libraries configured in there, # the global config and tries to open shared libraries configured in there,
@ -211,17 +238,18 @@ in
''; '';
pipeBins = map lib.getExe [ pipeBins = map lib.getExe [
(pkgs.writeShellScriptBin "rspamd-learn-ham.sh" (pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
"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-spam.sh"
"exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
]; ];
}; };
imapsieve.mailbox = [ imapsieve.mailbox = [
{ {
name = junkMailboxName; name = junkMailboxName;
causes = [ "COPY" "APPEND" ]; causes = [
"COPY"
"APPEND"
];
before = ./dovecot/imap_sieve/report-spam.sieve; before = ./dovecot/imap_sieve/report-spam.sieve;
} }
{ {
@ -245,42 +273,62 @@ in
${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) '' ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
service imap-login { service imap-login {
inet_listener imap { inet_listener imap {
${if cfg.enableImap then '' ${
if cfg.enableImap then
''
port = 143 port = 143
'' else '' ''
else
''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0 port = 0
''} ''
}
} }
inet_listener imaps { inet_listener imaps {
${if cfg.enableImapSsl then '' ${
if cfg.enableImapSsl then
''
port = 993 port = 993
ssl = yes ssl = yes
'' else '' ''
else
''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0 port = 0
''} ''
}
} }
} }
''} ''}
${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) '' ${lib.optionalString (cfg.enablePop3 || cfg.enablePop3Ssl) ''
service pop3-login { service pop3-login {
inet_listener pop3 { inet_listener pop3 {
${if cfg.enablePop3 then '' ${
if cfg.enablePop3 then
''
port = 110 port = 110
'' else '' ''
else
''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0 port = 0
''} ''
}
} }
inet_listener pop3s { inet_listener pop3s {
${if cfg.enablePop3Ssl then '' ${
if cfg.enablePop3Ssl then
''
port = 995 port = 995
ssl = yes ssl = yes
'' else '' ''
else
''
# see https://dovecot.org/pipermail/dovecot/2010-March/047479.html # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
port = 0 port = 0
''} ''
}
} }
} }
''} ''}
@ -373,7 +421,7 @@ in
service indexer-worker { service indexer-worker {
${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) '' ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)} vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)}
''} ''}
} }
@ -383,11 +431,15 @@ in
}; };
systemd.services.dovecot2 = { systemd.services.dovecot2 = {
preStart = '' preStart =
''
${genPasswdScript} ${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 ]);
}; };
} }

View file

@ -14,15 +14,28 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.mailserver; cfg = config.mailserver;
in in
{ {
config = with cfg; lib.mkIf enable { config =
environment.systemPackages = with pkgs; [ with cfg;
dovecot openssh postfix rspamd lib.mkIf enable {
] ++ (if certificateScheme == "selfsigned" then [ openssl ] else []); environment.systemPackages =
with pkgs;
[
dovecot
openssh
postfix
rspamd
]
++ (if certificateScheme == "selfsigned" then [ openssl ] else [ ]);
}; };
} }

View file

@ -24,4 +24,3 @@ in
services.kresd.enable = true; services.kresd.enable = true;
}; };
} }

View file

@ -20,10 +20,13 @@ let
cfg = config.mailserver; cfg = config.mailserver;
in in
{ {
config = with cfg; lib.mkIf (enable && openFirewall) { config =
with cfg;
lib.mkIf (enable && openFirewall) {
networking.firewall = { networking.firewall = {
allowedTCPPorts = [ 25 ] allowedTCPPorts =
[ 25 ]
++ lib.optional enableSubmission 587 ++ lib.optional enableSubmission 587
++ lib.optional enableSubmissionSsl 465 ++ lib.optional enableSubmissionSsl 465
++ lib.optional enableImap 143 ++ lib.optional enableImap 143

View file

@ -14,8 +14,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{
{ config, pkgs, lib, ... }: config,
pkgs,
lib,
...
}:
with (import ./common.nix { inherit config lib pkgs; }); with (import ./common.nix { inherit config lib pkgs; });
@ -23,7 +27,9 @@ let
cfg = config.mailserver; cfg = config.mailserver;
in in
{ {
config = lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx")) { config =
lib.mkIf (cfg.enable && (cfg.certificateScheme == "acme" || cfg.certificateScheme == "acme-nginx"))
{
services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") { services.nginx = lib.mkIf (cfg.certificateScheme == "acme-nginx") {
enable = true; enable = true;
virtualHosts."${cfg.fqdn}" = { virtualHosts."${cfg.fqdn}" = {

View file

@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
with (import ./common.nix { inherit config pkgs lib; }); with (import ./common.nix { inherit config pkgs lib; });
@ -28,31 +33,55 @@ let
mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables; mergeLookupTables = tables: lib.zipAttrsWith (_: v: lib.flatten v) tables;
# valiases_postfix :: Map String [String] # valiases_postfix :: Map String [String]
valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList valiases_postfix = mergeLookupTables (
(name: value: lib.flatten (
let to = name; lib.mapAttrsToList (
in map (from: {"${from}" = to;}) (value.aliases ++ lib.singleton name)) name: value:
cfg.loginAccounts)); let
regex_valiases_postfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList to = name;
(name: value: in
let to = name; map (from: { "${from}" = to; }) (value.aliases ++ lib.singleton name)
in map (from: {"${from}" = to;}) value.aliasesRegexp) ) cfg.loginAccounts
cfg.loginAccounts)); )
);
regex_valiases_postfix = mergeLookupTables (
lib.flatten (
lib.mapAttrsToList (
name: value:
let
to = name;
in
map (from: { "${from}" = to; }) value.aliasesRegexp
) cfg.loginAccounts
)
);
# catchAllPostfix :: Map String [String] # catchAllPostfix :: Map String [String]
catchAllPostfix = mergeLookupTables (lib.flatten (lib.mapAttrsToList catchAllPostfix = mergeLookupTables (
(name: value: lib.flatten (
let to = name; lib.mapAttrsToList (
in map (from: {"@${from}" = to;}) value.catchAll) name: value:
cfg.loginAccounts)); let
to = name;
in
map (from: { "@${from}" = to; }) value.catchAll
) cfg.loginAccounts
)
);
# all_valiases_postfix :: Map String [String] # all_valiases_postfix :: Map String [String]
all_valiases_postfix = mergeLookupTables [valiases_postfix extra_valiases_postfix]; all_valiases_postfix = mergeLookupTables [
valiases_postfix
extra_valiases_postfix
];
# attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String] # attrsToLookupTable :: Map String (Either String [ String ]) -> Map String [String]
attrsToLookupTable = aliases: let attrsToLookupTable =
lookupTables = lib.mapAttrsToList (from: to: {"${from}" = to;}) aliases; aliases:
in mergeLookupTables lookupTables; let
lookupTables = lib.mapAttrsToList (from: to: { "${from}" = to; }) aliases;
in
mergeLookupTables lookupTables;
# extra_valiases_postfix :: Map String [String] # extra_valiases_postfix :: Map String [String]
extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases; extra_valiases_postfix = attrsToLookupTable cfg.extraVirtualAliases;
@ -61,37 +90,49 @@ let
forwards = attrsToLookupTable cfg.forwards; forwards = attrsToLookupTable cfg.forwards;
# lookupTableToString :: Map String [String] -> String # lookupTableToString :: Map String [String] -> String
lookupTableToString = attrs: let lookupTableToString =
attrs:
let
valueToString = value: lib.concatStringsSep ", " value; valueToString = value: lib.concatStringsSep ", " value;
in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs); in
lib.concatStringsSep "\n" (
lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs
);
# valiases_file :: Path # valiases_file :: Path
valiases_file = let valiases_file =
content = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix]); let
in builtins.toFile "valias" content; content = lookupTableToString (mergeLookupTables [
all_valiases_postfix
catchAllPostfix
]);
in
builtins.toFile "valias" content;
regex_valiases_file = let regex_valiases_file =
let
content = lookupTableToString regex_valiases_postfix; content = lookupTableToString regex_valiases_postfix;
in builtins.toFile "regex_valias" content; in
builtins.toFile "regex_valias" content;
# denied_recipients_postfix :: [ String ] # denied_recipients_postfix :: [ String ]
denied_recipients_postfix = (map denied_recipients_postfix = map (acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") (
(acct: "${acct.name} REJECT ${acct.sendOnlyRejectMessage}") lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts)
(lib.filter (acct: acct.sendOnly) (lib.attrValues cfg.loginAccounts))); );
denied_recipients_file = builtins.toFile "denied_recipients" (lib.concatStringsSep "\n" denied_recipients_postfix); denied_recipients_file = builtins.toFile "denied_recipients" (
lib.concatStringsSep "\n" denied_recipients_postfix
);
reject_senders_postfix = (map reject_senders_postfix = map (sender: "${sender} REJECT") cfg.rejectSender;
(sender: reject_senders_file = builtins.toFile "reject_senders" (
"${sender} REJECT") lib.concatStringsSep "\n" reject_senders_postfix
(cfg.rejectSender)); );
reject_senders_file = builtins.toFile "reject_senders" (lib.concatStringsSep "\n" (reject_senders_postfix)) ;
reject_recipients_postfix = (map reject_recipients_postfix = map (recipient: "${recipient} REJECT") cfg.rejectRecipients;
(recipient:
"${recipient} REJECT")
(cfg.rejectRecipients));
# rejectRecipients :: [ Path ] # rejectRecipients :: [ Path ]
reject_recipients_file = builtins.toFile "reject_recipients" (lib.concatStringsSep "\n" (reject_recipients_postfix)) ; reject_recipients_file = builtins.toFile "reject_recipients" (
lib.concatStringsSep "\n" reject_recipients_postfix
);
# vhosts_file :: Path # vhosts_file :: Path
vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains); vhosts_file = builtins.toFile "vhosts" (concatStringsSep "\n" cfg.domains);
@ -103,9 +144,12 @@ let
# every alias is owned (uniquely) by its user. # every alias is owned (uniquely) by its user.
# The user's own address is already in all_valiases_postfix. # The user's own address is already in all_valiases_postfix.
vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix); vaccounts_file = builtins.toFile "vaccounts" (lookupTableToString all_valiases_postfix);
regex_vaccounts_file = builtins.toFile "regex_vaccounts" (lookupTableToString regex_valiases_postfix); regex_vaccounts_file = builtins.toFile "regex_vaccounts" (
lookupTableToString regex_valiases_postfix
);
submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" ('' submissionHeaderCleanupRules = pkgs.writeText "submission_header_cleanup_rules" (
''
# Removes sensitive headers from mails handed in via the submission port. # Removes sensitive headers from mails handed in via the submission port.
# See https://thomas-leister.de/mailserver-debian-stretch/ # See https://thomas-leister.de/mailserver-debian-stretch/
# Uses "pcre" style regex. # Uses "pcre" style regex.
@ -115,21 +159,22 @@ let
/^X-Mailer:/ IGNORE /^X-Mailer:/ IGNORE
/^User-Agent:/ IGNORE /^User-Agent:/ IGNORE
/^X-Enigmail:/ IGNORE /^X-Enigmail:/ IGNORE
'' + lib.optionalString cfg.rewriteMessageId '' ''
+ lib.optionalString cfg.rewriteMessageId ''
# Replaces the user submitted hostname with the server's FQDN to hide the # Replaces the user submitted hostname with the server's FQDN to hide the
# user's host or network. # user's host or network.
/^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}> /^Message-ID:\s+<(.*?)@.*?>/ REPLACE Message-ID: <$1@${cfg.fqdn}>
''); ''
);
smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ]; smtpdMilters = [ "unix:/run/rspamd/rspamd-milter.sock" ];
mappedFile = name: "hash:/var/lib/postfix/conf/${name}"; mappedFile = name: "hash:/var/lib/postfix/conf/${name}";
mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}"; mappedRegexFile = name: "pcre:/var/lib/postfix/conf/${name}";
submissionOptions = submissionOptions = {
{
smtpd_tls_security_level = "encrypt"; smtpd_tls_security_level = "encrypt";
smtpd_sasl_auth_enable = "yes"; smtpd_sasl_auth_enable = "yes";
smtpd_sasl_type = "dovecot"; smtpd_sasl_type = "dovecot";
@ -137,7 +182,9 @@ let
smtpd_sasl_security_options = "noanonymous"; smtpd_sasl_security_options = "noanonymous";
smtpd_sasl_local_domain = "$myhostname"; smtpd_sasl_local_domain = "$myhostname";
smtpd_client_restrictions = "permit_sasl_authenticated,reject"; smtpd_client_restrictions = "permit_sasl_authenticated,reject";
smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${lib.optionalString (regex_valiases_postfix != {}) ",pcre:/etc/postfix/regex_vaccounts"}"; smtpd_sender_login_maps = "hash:/etc/postfix/vaccounts${lib.optionalString cfg.ldap.enable ",ldap:${ldapSenderLoginMapFile}"}${
lib.optionalString (regex_valiases_postfix != { }) ",pcre:/etc/postfix/regex_vaccounts"
}";
smtpd_sender_restrictions = "reject_sender_login_mismatch"; smtpd_sender_restrictions = "reject_sender_login_mismatch";
smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject"; smtpd_recipient_restrictions = "reject_non_fqdn_recipient,reject_unknown_recipient_domain,permit_sasl_authenticated,reject";
cleanup_service_name = "submission-header-cleanup"; cleanup_service_name = "submission-header-cleanup";
@ -186,14 +233,19 @@ let
}; };
in in
{ {
config = with cfg; lib.mkIf enable { config =
with cfg;
lib.mkIf enable {
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable { systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
preStart = '' preStart = ''
${appendPwdInVirtualMailboxMap} ${appendPwdInVirtualMailboxMap}
${appendPwdInSenderLoginMap} ${appendPwdInSenderLoginMap}
''; '';
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ]; restartTriggers = [
appendPwdInVirtualMailboxMap
appendPwdInSenderLoginMap
];
}; };
services.postfix = { services.postfix = {
@ -209,7 +261,11 @@ in
mapFiles."reject_recipients" = reject_recipients_file; mapFiles."reject_recipients" = reject_recipients_file;
enableSubmission = cfg.enableSubmission; enableSubmission = cfg.enableSubmission;
enableSubmissions = cfg.enableSubmissionSsl; enableSubmissions = cfg.enableSubmissionSsl;
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]); virtual = lookupTableToString (mergeLookupTables [
all_valiases_postfix
catchAllPostfix
forwards
]);
config = { config = {
smtpd_tls_chain_files = [ smtpd_tls_chain_files = [
@ -229,16 +285,21 @@ in
virtual_gid_maps = "static:5000"; virtual_gid_maps = "static:5000";
virtual_mailbox_base = mailDirectory; virtual_mailbox_base = mailDirectory;
virtual_mailbox_domains = vhosts_file; virtual_mailbox_domains = vhosts_file;
virtual_mailbox_maps = [ virtual_mailbox_maps =
[
(mappedFile "valias") (mappedFile "valias")
] ++ lib.optionals (cfg.ldap.enable) [ ]
++ lib.optionals cfg.ldap.enable [
"ldap:${ldapVirtualMailboxMapFile}" "ldap:${ldapVirtualMailboxMapFile}"
] ++ lib.optionals (regex_valiases_postfix != {}) [ ]
++ lib.optionals (regex_valiases_postfix != { }) [
(mappedRegexFile "regex_valias") (mappedRegexFile "regex_valias")
]; ];
virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [ virtual_alias_maps = lib.mkAfter (
lib.optionals (regex_valiases_postfix != { }) [
(mappedRegexFile "regex_valias") (mappedRegexFile "regex_valias")
]); ]
);
virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp"; virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
# Avoid leakage of X-Original-To, X-Delivered-To headers between recipients # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
lmtp_destination_recipient_limit = "1"; lmtp_destination_recipient_limit = "1";
@ -248,7 +309,9 @@ in
smtpd_sasl_path = "/run/dovecot2/auth"; smtpd_sasl_path = "/run/dovecot2/auth";
smtpd_sasl_auth_enable = true; smtpd_sasl_auth_enable = true;
smtpd_relay_restrictions = [ smtpd_relay_restrictions = [
"permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination" "permit_mynetworks"
"permit_sasl_authenticated"
"reject_unauth_destination"
]; ];
# reject selected senders # reject selected senders
@ -341,7 +404,10 @@ in
chroot = false; chroot = false;
maxproc = 0; maxproc = 0;
command = "cleanup"; command = "cleanup";
args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"]; args = [
"-o"
"header_checks=pcre:${submissionHeaderCleanupRules}"
];
}; };
}; };
}; };

View file

@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
with lib; with lib;
@ -38,7 +43,8 @@ let
${cfg.backup.cmdPostexec} ${cfg.backup.cmdPostexec}
''; '';
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}"; postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
in { in
{
config = mkIf (cfg.enable && cfg.backup.enable) { config = mkIf (cfg.enable && cfg.backup.enable) {
services.rsnapshot = { services.rsnapshot = {
enable = true; enable = true;

View file

@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.mailserver; cfg = config.mailserver;
@ -26,10 +31,13 @@ let
rspamdUser = config.services.rspamd.user; rspamdUser = config.services.rspamd.user;
rspamdGroup = config.services.rspamd.group; rspamdGroup = config.services.rspamd.group;
createDkimKeypair = domain: let createDkimKeypair =
domain:
let
privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key"; privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt"; publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
in pkgs.writeShellScript "dkim-keygen-${domain}" '' in
pkgs.writeShellScript "dkim-keygen-${domain}" ''
if [ ! -f "${privateKey}" ] if [ ! -f "${privateKey}" ]
then then
${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \ ${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
@ -44,38 +52,53 @@ let
''; '';
in in
{ {
config = with cfg; lib.mkIf enable { config =
with cfg;
lib.mkIf enable {
environment.systemPackages = lib.mkBefore [ environment.systemPackages = lib.mkBefore [
(pkgs.runCommand "rspamc-wrapped" { (pkgs.runCommand "rspamc-wrapped"
{
nativeBuildInputs = with pkgs; [ makeWrapper ]; nativeBuildInputs = with pkgs; [ makeWrapper ];
}'' }
''
makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \ makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \
--add-flags "-h /run/rspamd/worker-controller.sock" --add-flags "-h /run/rspamd/worker-controller.sock"
'') ''
)
]; ];
services.rspamd = { services.rspamd = {
enable = true; enable = true;
inherit debug; inherit debug;
locals = { locals = {
"milter_headers.conf" = { text = '' "milter_headers.conf" = {
text = ''
extended_spam_headers = true; extended_spam_headers = true;
''; }; '';
"redis.conf" = { text = '' };
servers = "${if cfg.redis.port == null "redis.conf" = {
then text =
''
servers = "${
if cfg.redis.port == null then
cfg.redis.address cfg.redis.address
else else
"${cfg.redis.address}:${toString cfg.redis.port}"}"; "${cfg.redis.address}:${toString cfg.redis.port}"
'' + (lib.optionalString (cfg.redis.password != null) '' }";
''
+ (lib.optionalString (cfg.redis.password != null) ''
password = "${cfg.redis.password}"; password = "${cfg.redis.password}";
''); }; '');
"classifier-bayes.conf" = { text = '' };
"classifier-bayes.conf" = {
text = ''
cache { cache {
backend = "redis"; backend = "redis";
} }
''; }; '';
"antivirus.conf" = lib.mkIf cfg.virusScanning { text = '' };
"antivirus.conf" = lib.mkIf cfg.virusScanning {
text = ''
clamav { clamav {
action = "reject"; action = "reject";
symbol = "CLAM_VIRUS"; symbol = "CLAM_VIRUS";
@ -84,15 +107,19 @@ in
servers = "/run/clamav/clamd.ctl"; servers = "/run/clamav/clamd.ctl";
scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all scan_mime_parts = false; # scan mail as a whole unit, not parts. seems to be needed to work at all
} }
''; }; '';
"dkim_signing.conf" = { text = '' };
"dkim_signing.conf" = {
text = ''
enabled = ${lib.boolToString cfg.dkimSigning}; enabled = ${lib.boolToString cfg.dkimSigning};
path = "${cfg.dkimKeyDirectory}/$domain.$selector.key"; path = "${cfg.dkimKeyDirectory}/$domain.$selector.key";
selector = "${cfg.dkimSelector}"; selector = "${cfg.dkimSelector}";
# Allow for usernames w/o domain part # Allow for usernames w/o domain part
allow_username_mismatch = true allow_username_mismatch = true
''; }; '';
"dmarc.conf" = { text = '' };
"dmarc.conf" = {
text = ''
${lib.optionalString cfg.dmarcReporting.enable '' ${lib.optionalString cfg.dmarcReporting.enable ''
reporting { reporting {
enabled = true; enabled = true;
@ -101,19 +128,22 @@ in
org_name = "${cfg.dmarcReporting.organizationName}"; org_name = "${cfg.dmarcReporting.organizationName}";
from_name = "${cfg.dmarcReporting.fromName}"; from_name = "${cfg.dmarcReporting.fromName}";
msgid_from = "${cfg.dmarcReporting.domain}"; msgid_from = "${cfg.dmarcReporting.domain}";
${lib.optionalString (cfg.dmarcReporting.excludeDomains != []) '' ${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) ''
exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains}; exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
''} ''}
}''} }''}
''; }; '';
};
}; };
workers.rspamd_proxy = { workers.rspamd_proxy = {
type = "rspamd_proxy"; type = "rspamd_proxy";
bindSockets = [{ bindSockets = [
{
socket = "/run/rspamd/rspamd-milter.sock"; socket = "/run/rspamd/rspamd-milter.sock";
mode = "0664"; mode = "0664";
}]; }
];
count = 1; # Do not spawn too many processes of this type count = 1; # Do not spawn too many processes of this type
extraConfig = '' extraConfig = ''
milter = yes; # Enable milter mode milter = yes; # Enable milter mode
@ -128,11 +158,13 @@ in
workers.controller = { workers.controller = {
type = "controller"; type = "controller";
count = 1; count = 1;
bindSockets = [{ bindSockets = [
{
socket = "/run/rspamd/worker-controller.sock"; socket = "/run/rspamd/worker-controller.sock";
mode = "0666"; mode = "0666";
}]; }
includes = []; ];
includes = [ ];
extraConfig = '' extraConfig = ''
static_dir = "''${WWWDIR}"; # Serve the web UI static assets static_dir = "''${WWWDIR}"; # Serve the web UI static assets
''; '';
@ -171,7 +203,7 @@ in
]; ];
}; };
systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) { systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
# Explicitly select yesterday's date to work around broken # Explicitly select yesterday's date to work around broken
# default behaviour when called without a date. # default behaviour when called without a date.
# https://github.com/rspamd/rspamd/issues/4062 # https://github.com/rspamd/rspamd/issues/4062
@ -182,7 +214,7 @@ in
User = "${config.services.rspamd.user}"; User = "${config.services.rspamd.user}";
Group = "${config.services.rspamd.group}"; Group = "${config.services.rspamd.group}";
AmbientCapabilities = []; AmbientCapabilities = [ ];
CapabilityBoundingSet = ""; CapabilityBoundingSet = "";
DevicePolicy = "closed"; DevicePolicy = "closed";
IPAddressAllow = "localhost"; IPAddressAllow = "localhost";
@ -203,7 +235,10 @@ in
ProcSubset = "pid"; ProcSubset = "pid";
ProtectSystem = "strict"; ProtectSystem = "strict";
RemoveIPC = true; RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true; RestrictNamespaces = true;
RestrictRealtime = true; RestrictRealtime = true;
RestrictSUIDSGID = true; RestrictSUIDSGID = true;
@ -216,7 +251,7 @@ in
}; };
}; };
systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) { systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
description = "Daily delivery of aggregated DMARC reports"; description = "Daily delivery of aggregated DMARC reports";
wantedBy = [ wantedBy = [
"timers.target" "timers.target"
@ -237,4 +272,3 @@ in
users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ]; users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
}; };
} }

View file

@ -14,22 +14,31 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
let let
cfg = config.mailserver; cfg = config.mailserver;
certificatesDeps = certificatesDeps =
if cfg.certificateScheme == "manual" then if cfg.certificateScheme == "manual" then
[] [ ]
else if cfg.certificateScheme == "selfsigned" then else if cfg.certificateScheme == "selfsigned" then
[ "mailserver-selfsigned-certificate.service" ] [ "mailserver-selfsigned-certificate.service" ]
else else
[ "acme-finished-${cfg.fqdn}.target" ]; [ "acme-finished-${cfg.fqdn}.target" ];
in in
{ {
config = with cfg; lib.mkIf enable { config =
with cfg;
lib.mkIf enable {
# Create self signed certificate # Create self signed certificate
systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") { systemd.services.mailserver-selfsigned-certificate =
lib.mkIf (cfg.certificateScheme == "selfsigned")
{
after = [ "local-fs.target" ]; after = [ "local-fs.target" ];
script = '' script = ''
# Create certificates if they do not exist yet # Create certificates if they do not exist yet
@ -56,12 +65,13 @@ in
systemd.services.dovecot2 = { systemd.services.dovecot2 = {
wants = certificatesDeps; wants = certificatesDeps;
after = certificatesDeps; after = certificatesDeps;
preStart = let preStart =
let
directories = lib.strings.escapeShellArgs ( directories = lib.strings.escapeShellArgs (
[ mailDirectory ] [ mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
++ lib.optional (cfg.indexDir != null) cfg.indexDir
); );
in '' in
''
# Create mail directory and set permissions. See # Create mail directory and set permissions. See
# <https://doc.dovecot.org/main/core/config/shared_mailboxes.html#filesystem-permissions-1>. # <https://doc.dovecot.org/main/core/config/shared_mailboxes.html#filesystem-permissions-1>.
# Prevent world-readable paths, even temporarily. # Prevent world-readable paths, even temporarily.
@ -75,11 +85,8 @@ in
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work # Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
systemd.services.postfix = { systemd.services.postfix = {
wants = certificatesDeps; wants = certificatesDeps;
after = [ "dovecot2.service" ] after = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service" ++ certificatesDeps;
++ lib.optional cfg.dkimSigning "rspamd.service" requires = [ "dovecot2.service" ] ++ lib.optional cfg.dkimSigning "rspamd.service";
++ certificatesDeps;
requires = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "rspamd.service";
}; };
}; };
} }

View file

@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/> # along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }: {
config,
pkgs,
lib,
...
}:
with config.mailserver; with config.mailserver;
@ -28,7 +33,6 @@ let
group = vmailGroupName; group = vmailGroupName;
}; };
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" '' virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
#!${pkgs.stdenv.shell} #!${pkgs.stdenv.shell}
@ -46,8 +50,10 @@ let
# Copy user's sieve script to the correct location (if it exists). If it # Copy user's sieve script to the correct location (if it exists). If it
# is null, remove the file. # is null, remove the file.
${lib.concatMapStringsSep "\n" ({ name, sieveScript }: ${lib.concatMapStringsSep "\n" (
if lib.isString sieveScript then '' { name, sieveScript }:
if lib.isString sieveScript then
''
if (! test -d "${sieveDirectory}/${name}"); then if (! test -d "${sieveDirectory}/${name}"); then
mkdir -p "${sieveDirectory}/${name}" mkdir -p "${sieveDirectory}/${name}"
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}" chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
@ -57,34 +63,41 @@ let
${sieveScript} ${sieveScript}
EOF EOF
chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve" chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
'' else '' ''
else
''
if (test -f "${sieveDirectory}/${name}/default.sieve"); then if (test -f "${sieveDirectory}/${name}/default.sieve"); then
rm "${sieveDirectory}/${name}/default.sieve" rm "${sieveDirectory}/${name}/default.sieve"
fi fi
if (test -f "${sieveDirectory}/${name}.svbin"); then if (test -f "${sieveDirectory}/${name}.svbin"); then
rm "${sieveDirectory}/${name}/default.svbin" rm "${sieveDirectory}/${name}/default.svbin"
fi fi
'') (map (user: { inherit (user) name sieveScript; }) ''
(lib.attrValues loginAccounts))} ) (map (user: { inherit (user) name sieveScript; }) (lib.attrValues loginAccounts))}
''; '';
in { in
{
config = lib.mkIf enable { config = lib.mkIf enable {
# assert that all accounts provide a password # assert that all accounts provide a password
assertions = (map (acct: { assertions = map (acct: {
assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null); assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null;
message = "${acct.name} must provide either a hashed password or a password hash file"; message = "${acct.name} must provide either a hashed password or a password hash file";
}) (lib.attrValues loginAccounts)); }) (lib.attrValues loginAccounts);
# warn for accounts that specify both password and file # warn for accounts that specify both password and file
warnings = (map warnings =
(acct: "${acct.name} specifies both a password hash and hash file; hash file will be used") map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
(lib.filter (
(acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) (
(lib.attrValues loginAccounts))); lib.attrValues loginAccounts
)
);
# set the vmail gid to a specific value # set the vmail gid to a specific value
users.groups = { users.groups = {
"${vmailGroupName}" = { gid = vmailUID; }; "${vmailGroupName}" = {
gid = vmailUID;
};
}; };
# define all users # define all users

View file

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

View file

@ -24,7 +24,8 @@
name = "clamav"; name = "clamav";
nodes = { nodes = {
server = { pkgs, ... }: server =
{ pkgs, ... }:
{ {
imports = [ imports = [
../default.nix ../default.nix
@ -70,7 +71,10 @@
mailserver = { mailserver = {
enable = true; enable = true;
fqdn = "mail.example.com"; fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ]; domains = [
"example.com"
"example2.com"
];
virusScanning = true; virusScanning = true;
loginAccounts = { loginAccounts = {
@ -90,7 +94,9 @@
"root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*"; "root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
}; };
}; };
client = { nodes, pkgs, ... }: let client =
{ nodes, pkgs, ... }:
let
serverIP = nodes.server.networking.primaryIPAddress; serverIP = nodes.server.networking.primaryIPAddress;
clientIP = nodes.client.networking.primaryIPAddress; clientIP = nodes.client.networking.primaryIPAddress;
grep-ip = pkgs.writeScriptBin "grep-ip" '' grep-ip = pkgs.writeScriptBin "grep-ip" ''
@ -98,13 +104,18 @@
echo grep '${clientIP}' "$@" >&2 echo grep '${clientIP}' "$@" >&2
exec grep '${clientIP}' "$@" exec grep '${clientIP}' "$@"
''; '';
in { in
{
imports = [ imports = [
./lib/config.nix ./lib/config.nix
]; ];
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils grep-ip fetchmail
msmtp
procmail
findutils
grep-ip
]; ];
environment.etc = { environment.etc = {
"root/.fetchmailrc" = { "root/.fetchmailrc" = {

View file

@ -18,7 +18,8 @@
name = "external"; name = "external";
nodes = { nodes = {
server = { pkgs, ... }: server =
{ pkgs, ... }:
{ {
imports = [ imports = [
../default.nix ../default.nix
@ -36,12 +37,14 @@
''; '';
}; };
mailserver = { mailserver = {
enable = true; enable = true;
debug = true; debug = true;
fqdn = "mail.example.com"; fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ]; domains = [
"example.com"
"example2.com"
];
rewriteMessageId = true; rewriteMessageId = true;
dkimKeyBits = 1535; dkimKeyBits = 1535;
dmarcReporting = { dmarcReporting = {
@ -71,7 +74,10 @@
extraVirtualAliases = { extraVirtualAliases = {
"single-alias@example.com" = "user1@example.com"; "single-alias@example.com" = "user1@example.com";
"multi-alias@example.com" = [ "user1@example.com" "user2@example.com" ]; "multi-alias@example.com" = [
"user1@example.com"
"user2@example.com"
];
}; };
enableImap = true; enableImap = true;
@ -80,12 +86,16 @@
enable = true; enable = true;
autoIndex = true; autoIndex = true;
# special use depends on https://github.com/NixOS/nixpkgs/pull/93201 # special use depends on https://github.com/NixOS/nixpkgs/pull/93201
autoIndexExclude = [ (if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk") ]; autoIndexExclude = [
(if (pkgs.lib.versionAtLeast pkgs.lib.version "21") then "\\Junk" else "Junk")
];
enforced = "yes"; enforced = "yes";
}; };
}; };
}; };
client = { nodes, pkgs, ... }: let client =
{ nodes, pkgs, ... }:
let
serverIP = nodes.server.networking.primaryIPAddress; serverIP = nodes.server.networking.primaryIPAddress;
clientIP = nodes.client.networking.primaryIPAddress; clientIP = nodes.client.networking.primaryIPAddress;
grep-ip = pkgs.writeScriptBin "grep-ip" '' grep-ip = pkgs.writeScriptBin "grep-ip" ''
@ -172,12 +182,21 @@
assert needle in repr(response) assert needle in repr(response)
imap.close() imap.close()
''; '';
in { in
{
imports = [ imports = [
./lib/config.nix ./lib/config.nix
]; ];
environment.systemPackages = with pkgs; [ environment.systemPackages = with pkgs; [
fetchmail msmtp procmail findutils grep-ip check-mail-id test-imap-spam test-imap-ham search fetchmail
msmtp
procmail
findutils
grep-ip
check-mail-id
test-imap-spam
test-imap-ham
search
]; ];
environment.etc = { environment.etc = {
"root/.fetchmailrc" = { "root/.fetchmailrc" = {

View file

@ -30,9 +30,14 @@ let
''; '';
}; };
hashPassword = password: pkgs.runCommand hashPassword =
"password-${password}-hashed" password:
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } '' pkgs.runCommand "password-${password}-hashed"
{
buildInputs = [ pkgs.mkpasswd ];
inherit password;
}
''
mkpasswd -sm bcrypt <<<"$password" > $out mkpasswd -sm bcrypt <<<"$password" > $out
''; '';
@ -43,7 +48,9 @@ in
name = "internal"; name = "internal";
nodes = { nodes = {
machine = { pkgs, ... }: { machine =
{ pkgs, ... }:
{
imports = [ imports = [
./../default.nix ./../default.nix
./lib/config.nix ./lib/config.nix
@ -51,11 +58,13 @@ in
virtualisation.memorySize = 1024; virtualisation.memorySize = 1024;
environment.systemPackages = [ environment.systemPackages =
[
(pkgs.writeScriptBin "mail-check" '' (pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'') '')
] ++ (with pkgs; [ ]
++ (with pkgs; [
curl curl
openssl openssl
netcat netcat
@ -64,7 +73,10 @@ in
mailserver = { mailserver = {
enable = true; enable = true;
fqdn = "mail.example.com"; fqdn = "mail.example.com";
domains = [ "example.com" "domain.com" ]; domains = [
"example.com"
"domain.com"
];
localDnsResolver = false; localDnsResolver = false;
loginAccounts = { loginAccounts = {
@ -73,7 +85,7 @@ in
}; };
"user2@example.com" = { "user2@example.com" = {
hashedPasswordFile = hashedPasswordFile; hashedPasswordFile = hashedPasswordFile;
aliasesRegexp = [''/^user2.*@domain\.com$/'']; aliasesRegexp = [ ''/^user2.*@domain\.com$/'' ];
}; };
"send-only@example.com" = { "send-only@example.com" = {
hashedPasswordFile = hashPassword "send-only"; hashedPasswordFile = hashPassword "send-only";

View file

@ -7,7 +7,9 @@ in
name = "ldap"; name = "ldap";
nodes = { nodes = {
machine = { pkgs, ... }: { machine =
{ pkgs, ... }:
{
imports = [ imports = [
./../default.nix ./../default.nix
./lib/config.nix ./lib/config.nix
@ -23,7 +25,8 @@ in
environment.systemPackages = [ environment.systemPackages = [
(pkgs.writeScriptBin "mail-check" '' (pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'')]; '')
];
environment.etc.bind-password.text = bindPassword; environment.etc.bind-password.text = bindPassword;

View file

@ -6,16 +6,23 @@
}: }:
let let
hashPassword = password: pkgs.runCommand hashPassword =
"password-${password}-hashed" password:
{ buildInputs = [ pkgs.mkpasswd ]; inherit password; } pkgs.runCommand "password-${password}-hashed"
{
buildInputs = [ pkgs.mkpasswd ];
inherit password;
}
'' ''
mkpasswd -sm bcrypt <<<"$password" > $out mkpasswd -sm bcrypt <<<"$password" > $out
''; '';
password = pkgs.writeText "password" "password"; password = pkgs.writeText "password" "password";
domainGenerator = domain: { pkgs, ... }: { domainGenerator =
domain:
{ pkgs, ... }:
{
imports = [ imports = [
../default.nix ../default.nix
./lib/config.nix ./lib/config.nix
@ -37,7 +44,10 @@ let
}; };
services.dnsmasq = { services.dnsmasq = {
enable = true; enable = true;
settings.mx-host = [ "domain1.com,domain1,10" "domain2.com,domain2,10" ]; settings.mx-host = [
"domain1.com,domain1,10"
"domain2.com,domain2,10"
];
}; };
}; };
@ -47,22 +57,33 @@ in
name = "multiple"; name = "multiple";
nodes = { nodes = {
domain1 = {...}: { domain1 =
{ ... }:
{
imports = [ imports = [
../default.nix ../default.nix
(domainGenerator "domain1.com") (domainGenerator "domain1.com")
]; ];
mailserver.forwards = { mailserver.forwards = {
"non-local@domain1.com" = ["user@domain2.com" "user@domain1.com"]; "non-local@domain1.com" = [
"non@domain1.com" = ["user@domain2.com" "user@domain1.com"]; "user@domain2.com"
"user@domain1.com"
];
"non@domain1.com" = [
"user@domain2.com"
"user@domain1.com"
];
}; };
}; };
domain2 = domainGenerator "domain2.com"; domain2 = domainGenerator "domain2.com";
client = { pkgs, ... }: { client =
{ pkgs, ... }:
{
environment.systemPackages = [ environment.systemPackages = [
(pkgs.writeScriptBin "mail-check" '' (pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@ ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'')]; '')
];
}; };
}; };
testScript = '' testScript = ''