treewide: reformat with nixfmt-rfc-style

This commit is contained in:
Martin Weinelt 2025-06-15 03:39:44 +02:00
parent 03433d472f
commit 1a7f3d718c
No known key found for this signature in database
GPG key ID: 87C1E9888F856759
21 changed files with 2086 additions and 1680 deletions

View file

@ -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;
@ -41,8 +40,9 @@ let
jobsets = desc;
};
in {
jobsets = pkgs.runCommand "spec-jobsets.json" {} ''
in
{
jobsets = pkgs.runCommand "spec-jobsets.json" { } ''
cat >$out <<EOF
${builtins.toJSON desc}
EOF

View file

@ -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, lib, pkgs, ... }:
{
config,
lib,
pkgs,
...
}:
with lib;
@ -56,14 +61,17 @@ in
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`)
@ -79,7 +87,10 @@ in
};
loginAccounts = mkOption {
type = types.attrsOf (types.submodule ({ name, ... }: {
type = types.attrsOf (
types.submodule (
{ name, ... }:
{
options = {
name = mkOption {
type = types.str;
@ -118,8 +129,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
@ -129,8 +143,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).
'';
@ -138,8 +152,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.
@ -202,7 +219,9 @@ in
};
config.name = mkDefault name;
}));
}
)
);
example = {
user1 = {
hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
@ -220,7 +239,7 @@ in
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
```
'';
default = {};
default = { };
};
ldap = {
@ -284,7 +303,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.
@ -419,14 +442,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
@ -439,7 +470,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.
@ -488,7 +522,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
@ -514,17 +551,23 @@ in
};
extraVirtualAliases = mkOption {
type = let
type =
let
loginAccount = mkOptionType {
name = "Login Account";
check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts);
};
in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount));
in
with types;
attrsOf (either loginAccount (nonEmptyListOf loginAccount));
example = {
"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
@ -537,7 +580,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 {
@ -554,28 +597,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 {
@ -673,12 +722,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:
@ -851,7 +918,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).
@ -938,7 +1008,7 @@ in
excludeDomains = mkOption {
type = types.listOf types.str;
default = [];
default = [ ];
description = ''
List of domains or eSLDs to be excluded from DMARC reports.
'';
@ -1150,7 +1220,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.";
};
@ -1208,14 +1286,14 @@ in
locations = mkOption {
type = types.listOf types.path;
default = [cfg.mailDirectory];
default = [ cfg.mailDirectory ];
defaultText = lib.literalExpression "[ config.mailserver.mailDirectory ]";
description = "The locations that are to be backed up by borg.";
};
extraArgumentsForInit = mkOption {
type = types.listOf types.str;
default = ["--critical"];
default = [ "--critical" ];
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;
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
@ -44,13 +53,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 +77,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
@ -90,10 +102,15 @@
}
];
};
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 +118,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
myst-parser
linkify-it-py
])
)];
]
))
];
buildPhase = ''
cp ${optionsDoc} options.md
# Workaround for https://github.com/sphinx-doc/sphinx/issues/3451
@ -121,7 +145,8 @@
'';
};
in {
in
{
nixosModules = rec {
mailserver = mailserverModule;
default = mailserver;
@ -184,9 +209,12 @@
};
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

View file

@ -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

View file

@ -14,43 +14,62 @@
# 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

View file

@ -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,
...
}:
with (import ./common.nix { inherit config pkgs lib; });
@ -28,10 +33,14 @@ 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";
@ -39,9 +48,7 @@ let
# maildir in format "/${domain}/${user}"
dovecotMaildir =
"maildir:${cfg.mailDirectory}/%{domain}/%{username}${maildirLayoutAppendix}${maildirUTF8FolderNames}"
+ (lib.optionalString (cfg.indexDir != null)
":INDEX=${cfg.indexDir}/%{domain}/%{username}"
);
+ (lib.optionalString (cfg.indexDir != null) ":INDEX=${cfg.indexDir}/%{domain}/%{username}");
postfixCfg = config.services.postfix;
@ -93,7 +100,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,34 +110,49 @@ 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
);
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;
@ -139,7 +163,9 @@ let
in
{
config = with cfg; lib.mkIf enable {
config =
with cfg;
lib.mkIf enable {
assertions = [
{
assertion = junkMailboxNumber == 1;
@ -148,18 +174,19 @@ 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,
@ -211,17 +238,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;
}
{
@ -245,42 +273,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
''}
''
}
}
}
''}
@ -373,7 +421,7 @@ 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)}
''}
}
@ -383,11 +431,15 @@ in
};
systemd.services.dovecot2 = {
preStart = ''
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 ]);
};
}

View file

@ -14,15 +14,28 @@
# 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 =
with cfg;
lib.mkIf enable {
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;
};
}

View file

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

View file

@ -14,8 +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,
...
}:
with (import ./common.nix { inherit config lib pkgs; });
@ -23,7 +27,9 @@ 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}" = {

View file

@ -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,
...
}:
with (import ./common.nix { inherit config pkgs lib; });
@ -28,31 +33,55 @@ let
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 +90,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 +144,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 +159,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 +182,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,14 +233,19 @@ let
};
in
{
config = with cfg; lib.mkIf enable {
config =
with cfg;
lib.mkIf enable {
systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
preStart = ''
${appendPwdInVirtualMailboxMap}
${appendPwdInSenderLoginMap}
'';
restartTriggers = [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ];
restartTriggers = [
appendPwdInVirtualMailboxMap
appendPwdInSenderLoginMap
];
};
services.postfix = {
@ -209,7 +261,11 @@ in
mapFiles."reject_recipients" = reject_recipients_file;
enableSubmission = cfg.enableSubmission;
enableSubmissions = cfg.enableSubmissionSsl;
virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
virtual = lookupTableToString (mergeLookupTables [
all_valiases_postfix
catchAllPostfix
forwards
]);
config = {
smtpd_tls_chain_files = [
@ -229,16 +285,21 @@ in
virtual_gid_maps = "static:5000";
virtual_mailbox_base = mailDirectory;
virtual_mailbox_domains = vhosts_file;
virtual_mailbox_maps = [
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";
@ -248,7 +309,9 @@ in
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
@ -341,7 +404,10 @@ in
chroot = false;
maxproc = 0;
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
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
with lib;
@ -38,7 +43,8 @@ let
${cfg.backup.cmdPostexec}
'';
postexecString = optionalString postexecDefined "cmd_postexec ${postexecWrapped}";
in {
in
{
config = mkIf (cfg.enable && cfg.backup.enable) {
services.rsnapshot = {
enable = true;

View file

@ -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 \
@ -44,38 +52,53 @@ let
'';
in
{
config = with cfg; lib.mkIf enable {
config =
with cfg;
lib.mkIf 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;
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,15 +107,19 @@ 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 = ''
'';
};
"dmarc.conf" = {
text = ''
${lib.optionalString cfg.dmarcReporting.enable ''
reporting {
enabled = true;
@ -101,19 +128,22 @@ in
org_name = "${cfg.dmarcReporting.organizationName}";
from_name = "${cfg.dmarcReporting.fromName}";
msgid_from = "${cfg.dmarcReporting.domain}";
${lib.optionalString (cfg.dmarcReporting.excludeDomains != []) ''
${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) ''
exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
''}
}''}
''; };
'';
};
};
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 +158,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
'';
@ -182,7 +214,7 @@ in
User = "${config.services.rspamd.user}";
Group = "${config.services.rspamd.group}";
AmbientCapabilities = [];
AmbientCapabilities = [ ];
CapabilityBoundingSet = "";
DevicePolicy = "closed";
IPAddressAllow = "localhost";
@ -203,7 +235,10 @@ in
ProcSubset = "pid";
ProtectSystem = "strict";
RemoveIPC = true;
RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
];
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
@ -237,4 +272,3 @@ in
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
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
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 =
with cfg;
lib.mkIf 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
@ -56,12 +65,13 @@ in
systemd.services.dovecot2 = {
wants = certificatesDeps;
after = certificatesDeps;
preStart = let
preStart =
let
directories = lib.strings.escapeShellArgs (
[ mailDirectory ]
++ lib.optional (cfg.indexDir != null) cfg.indexDir
[ 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.
@ -75,11 +85,8 @@ in
# Postfix requires dovecot lmtp socket, dovecot auth socket and certificate to work
systemd.services.postfix = {
wants = certificatesDeps;
after = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "rspamd.service"
++ certificatesDeps;
requires = [ "dovecot2.service" ]
++ lib.optional cfg.dkimSigning "rspamd.service";
after = [ "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
# along with this program. If not, see <http://www.gnu.org/licenses/>
{ config, pkgs, lib, ... }:
{
config,
pkgs,
lib,
...
}:
with config.mailserver;
@ -28,7 +33,6 @@ let
group = vmailGroupName;
};
virtualMailUsersActivationScript = pkgs.writeScript "activate-virtual-mail-users" ''
#!${pkgs.stdenv.shell}
@ -46,8 +50,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,17 +63,20 @@ 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: {
@ -76,15 +85,19 @@ in {
}) (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

View file

@ -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

View file

@ -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" = {

View file

@ -18,7 +18,8 @@
name = "external";
nodes = {
server = { pkgs, ... }:
server =
{ pkgs, ... }:
{
imports = [
../default.nix
@ -36,12 +37,14 @@
'';
};
mailserver = {
enable = true;
debug = true;
fqdn = "mail.example.com";
domains = [ "example.com" "example2.com" ];
domains = [
"example.com"
"example2.com"
];
rewriteMessageId = true;
dkimKeyBits = 1535;
dmarcReporting = {
@ -71,7 +74,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 +86,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 +182,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" = {

View file

@ -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
@ -51,11 +58,13 @@ in
virtualisation.memorySize = 1024;
environment.systemPackages = [
environment.systemPackages =
[
(pkgs.writeScriptBin "mail-check" ''
${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
'')
] ++ (with pkgs; [
]
++ (with pkgs; [
curl
openssl
netcat
@ -64,7 +73,10 @@ in
mailserver = {
enable = true;
fqdn = "mail.example.com";
domains = [ "example.com" "domain.com" ];
domains = [
"example.com"
"domain.com"
];
localDnsResolver = false;
loginAccounts = {
@ -73,7 +85,7 @@ in
};
"user2@example.com" = {
hashedPasswordFile = hashedPasswordFile;
aliasesRegexp = [''/^user2.*@domain\.com$/''];
aliasesRegexp = [ ''/^user2.*@domain\.com$/'' ];
};
"send-only@example.com" = {
hashedPasswordFile = hashPassword "send-only";

View file

@ -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;

View file

@ -6,16 +6,23 @@
}:
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, ... }: {
domainGenerator =
domain:
{ pkgs, ... }:
{
imports = [
../default.nix
./lib/config.nix
@ -37,7 +44,10 @@ 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"
];
};
};
@ -47,22 +57,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 = ''