diff --git a/.hydra/declarative-jobsets.nix b/.hydra/declarative-jobsets.nix
index 7b99844..6877235 100644
--- a/.hydra/declarative-jobsets.nix
+++ b/.hydra/declarative-jobsets.nix
@@ -1,22 +1,21 @@
{ nixpkgs, pulls, ... }:
let
- pkgs = import nixpkgs {};
+ pkgs = import nixpkgs { };
prs = builtins.fromJSON (builtins.readFile pulls);
- prJobsets = pkgs.lib.mapAttrs (num: info:
- { enabled = 1;
- hidden = false;
- description = "PR ${num}: ${info.title}";
- checkinterval = 300;
- schedulingshares = 20;
- enableemail = false;
- emailoverride = "";
- keepnr = 1;
- type = 1;
- flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
- }
- ) prs;
+ prJobsets = pkgs.lib.mapAttrs (num: info: {
+ enabled = 1;
+ hidden = false;
+ description = "PR ${num}: ${info.title}";
+ checkinterval = 300;
+ schedulingshares = 20;
+ enableemail = false;
+ emailoverride = "";
+ keepnr = 1;
+ type = 1;
+ flake = "gitlab:simple-nixos-mailserver/nixos-mailserver/merge-requests/${info.iid}/head";
+ }) 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 <
-{ 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,130 +87,141 @@ in
};
loginAccounts = mkOption {
- type = types.attrsOf (types.submodule ({ name, ... }: {
- options = {
- name = mkOption {
- type = types.str;
- example = "user1@example.com";
- description = "Username";
- };
+ type = types.attrsOf (
+ types.submodule (
+ { name, ... }:
+ {
+ options = {
+ name = mkOption {
+ type = types.str;
+ example = "user1@example.com";
+ description = "Username";
+ };
- hashedPassword = mkOption {
- type = with types; nullOr str;
- default = null;
- example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
- description = ''
- The user's hashed password. Use `mkpasswd` as follows
+ hashedPassword = mkOption {
+ type = with types; nullOr str;
+ default = null;
+ example = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
+ description = ''
+ The user's hashed password. Use `mkpasswd` as follows
- ```
- nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
- ```
+ ```
+ nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
+ ```
- Warning: this is stored in plaintext in the Nix store!
- Use {option}`mailserver.loginAccounts..hashedPasswordFile` instead.
- '';
- };
+ Warning: this is stored in plaintext in the Nix store!
+ Use {option}`mailserver.loginAccounts..hashedPasswordFile` instead.
+ '';
+ };
- hashedPasswordFile = mkOption {
- type = with types; nullOr path;
- default = null;
- example = "/run/keys/user1-passwordhash";
- description = ''
- A file containing the user's hashed password. Use `mkpasswd` as follows
+ hashedPasswordFile = mkOption {
+ type = with types; nullOr path;
+ default = null;
+ example = "/run/keys/user1-passwordhash";
+ description = ''
+ A file containing the user's hashed password. Use `mkpasswd` as follows
- ```
- nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
- ```
- '';
- };
+ ```
+ nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
+ ```
+ '';
+ };
- aliases = mkOption {
- type = with types; listOf types.str;
- 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
- that allows sending from all email addresses in these domain.
- '';
- };
+ aliases = mkOption {
+ type = with types; listOf types.str;
+ 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
+ that allows sending from all email addresses in these domain.
+ '';
+ };
- aliasesRegexp = mkOption {
- type = with types; listOf types.str;
- example = [''/^tom\..*@domain\.com$/''];
- default = [];
- description = ''
- Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex).
- '';
- };
+ aliasesRegexp = mkOption {
+ type = with types; listOf types.str;
+ example = [ ''/^tom\..*@domain\.com$/'' ];
+ default = [ ];
+ description = ''
+ Same as {option}`mailserver.aliases` but using PCRE (Perl compatible regex).
+ '';
+ };
- catchAll = mkOption {
- type = with types; listOf (enum cfg.domains);
- 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.
- '';
- };
+ catchAll = mkOption {
+ type = with types; listOf (enum cfg.domains);
+ 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.
+ '';
+ };
- quota = mkOption {
- type = with types; nullOr types.str;
- default = null;
- example = "2G";
- description = ''
- Per user quota rules. Accepted sizes are `xx k/M/G/T` with the
- obvious meaning. Leave blank for the standard quota `100G`.
- '';
- };
+ quota = mkOption {
+ type = with types; nullOr types.str;
+ default = null;
+ example = "2G";
+ description = ''
+ Per user quota rules. Accepted sizes are `xx k/M/G/T` with the
+ obvious meaning. Leave blank for the standard quota `100G`.
+ '';
+ };
- sieveScript = mkOption {
- type = with types; nullOr lines;
- default = null;
- example = ''
- require ["fileinto", "mailbox"];
+ sieveScript = mkOption {
+ type = with types; nullOr lines;
+ default = null;
+ example = ''
+ require ["fileinto", "mailbox"];
- if address :is "from" "gitlab@mg.gitlab.com" {
- fileinto :create "GitLab";
- stop;
- }
+ if address :is "from" "gitlab@mg.gitlab.com" {
+ fileinto :create "GitLab";
+ stop;
+ }
- # This must be the last rule, it will check if list-id is set, and
- # file the message into the Lists folder for further investigation
- elsif header :matches "list-id" "*>" {
- fileinto :create "Lists";
- stop;
- }
- '';
- description = ''
- Per-user sieve script.
- '';
- };
+ # This must be the last rule, it will check if list-id is set, and
+ # file the message into the Lists folder for further investigation
+ elsif header :matches "list-id" "*>" {
+ fileinto :create "Lists";
+ stop;
+ }
+ '';
+ description = ''
+ Per-user sieve script.
+ '';
+ };
- sendOnly = mkOption {
- type = types.bool;
- default = false;
- description = ''
- Specifies if the account should be a send-only account.
- Emails sent to send-only accounts will be rejected from
- unauthorized senders with the `sendOnlyRejectMessage`
- stating the reason.
- '';
- };
+ sendOnly = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Specifies if the account should be a send-only account.
+ Emails sent to send-only accounts will be rejected from
+ unauthorized senders with the `sendOnlyRejectMessage`
+ stating the reason.
+ '';
+ };
- sendOnlyRejectMessage = mkOption {
- type = types.str;
- default = "This account cannot receive emails.";
- description = ''
- The message that will be returned to the sender when an email is
- sent to a send-only account. Only used if the account is marked
- as send-only.
- '';
- };
- };
+ sendOnlyRejectMessage = mkOption {
+ type = types.str;
+ default = "This account cannot receive emails.";
+ description = ''
+ The message that will be returned to the sender when an email is
+ sent to a send-only account. Only used if the account is marked
+ as send-only.
+ '';
+ };
+ };
- config.name = mkDefault name;
- }));
+ config.name = mkDefault name;
+ }
+ )
+ );
example = {
user1 = {
hashedPassword = "$6$evQJs5CFQyPAW09S$Cn99Y8.QjZ2IBnSu4qf1vBxDRWkaIZWOtmu1Ddsm3.H3CFpeVc0JU4llIq8HQXgeatvYhh5O33eWG3TSpjzu6/";
@@ -220,13 +239,13 @@ in
nix-shell -p mkpasswd --run 'mkpasswd -sm bcrypt'
```
'';
- default = {};
+ default = { };
};
ldap = {
enable = mkEnableOption "LDAP support";
- uris = mkOption {
+ uris = mkOption {
type = types.listOf types.str;
example = literalExpression ''
[
@@ -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
- loginAccount = mkOptionType {
- name = "Login Account";
- check = (account: builtins.elem account (builtins.attrNames cfg.loginAccounts));
- };
- in with types; attrsOf (either loginAccount (nonEmptyListOf loginAccount));
+ type =
+ let
+ loginAccount = mkOptionType {
+ name = "Login Account";
+ check = account: builtins.elem account (builtins.attrNames cfg.loginAccounts);
+ };
+ 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,28 +722,46 @@ 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))}\"'."
- (builtins.elemAt schemes (i - 1));
- in mkOption {
- type = with types; coercedTo (enum [ 1 2 3 ]) translate (enum schemes);
- default = "selfsigned";
- description = ''
- The scheme to use for managing TLS certificates:
+ 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);
+ default = "selfsigned";
+ description = ''
+ The scheme to use for managing TLS certificates:
- 1. `manual`: you specify locations via {option}`mailserver.certificateFile` and
- {option}`mailserver.keyFile` and manually copy certificates there.
- 2. `selfsigned`: you let the server create new (self-signed) certificates on the fly.
- 3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org)
- via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for
- {option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly
- configured to point to your server (see the [setup guide](setup-guide.rst) for more information).
- 4. `acme`: you already have an ACME certificate set up (for example, you're already running a TLS-enabled
- Nginx server on the FQDN). This is better than `manual` because the appropriate services will be reloaded
- when the certificate is renewed.
- '';
- };
+ 1. `manual`: you specify locations via {option}`mailserver.certificateFile` and
+ {option}`mailserver.keyFile` and manually copy certificates there.
+ 2. `selfsigned`: you let the server create new (self-signed) certificates on the fly.
+ 3. `acme-nginx`: you let the server request certificates from [Let's Encrypt](https://letsencrypt.org)
+ via NixOS' ACME module. By default, this will set up a stripped-down Nginx server for
+ {option}`mailserver.fqdn` and open port 80. For this to work, the FQDN must be properly
+ configured to point to your server (see the [setup guide](setup-guide.rst) for more information).
+ 4. `acme`: you already have an ACME certificate set up (for example, you're already running a TLS-enabled
+ Nginx server on the FQDN). This is better than `manual` because the appropriate services will be reloaded
+ when the certificate is renewed.
+ '';
+ };
certificateFile = mkOption {
type = types.path;
@@ -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).
@@ -864,16 +934,16 @@ in
};
dkimKeyBits = mkOption {
- type = types.int;
- default = 1024;
- description = ''
- How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
+ type = types.int;
+ default = 1024;
+ description = ''
+ How many bits in generated DKIM keys. RFC6376 advises minimum 1024-bit keys.
- If you have already deployed a key with a different number of bits than specified
- here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
- this package to generate a key with the new number of bits, you will either have to
- change the selector or delete the old key file.
- '';
+ If you have already deployed a key with a different number of bits than specified
+ here, then you should use a different selector ({option}`mailserver.dkimSelector`). In order to get
+ this package to generate a key with the new number of bits, you will either have to
+ change the selector or delete the old key file.
+ '';
};
dmarcReporting = {
@@ -901,7 +971,7 @@ in
};
domain = mkOption {
- type = types.enum (cfg.domains);
+ type = types.enum cfg.domains;
example = "example.com";
description = ''
The domain from which outgoing DMARC reports are served.
@@ -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.";
};
@@ -1295,9 +1373,9 @@ in
cronIntervals = mkOption {
type = types.attrsOf types.str;
default = {
- # minute, hour, day-in-month, month, weekday (0 = sunday)
+ # minute, hour, day-in-month, month, weekday (0 = sunday)
hourly = " 0 * * * *"; # Every full hour
- daily = "30 3 * * *"; # Every day at 3:30
+ daily = "30 3 * * *"; # Every day at 3:30
weekly = " 0 5 * * 0"; # Every sunday at 5:00 AM
};
description = ''
@@ -1311,29 +1389,29 @@ in
imports = [
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "enable" ] ''
- This option is not needed for fts-flatcurve
+ This option is not needed for fts-flatcurve
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "onCalendar" ] ''
- This option is not needed for fts-flatcurve
+ This option is not needed for fts-flatcurve
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maintenance" "randomizedDelaySec" ] ''
- This option is not needed for fts-flatcurve
+ This option is not needed for fts-flatcurve
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "minSize" ] ''
- This option is not supported by fts-flatcurve
+ This option is not supported by fts-flatcurve
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "maxSize" ] ''
- This option is not needed since fts-xapian 1.8.3
+ This option is not needed since fts-xapian 1.8.3
'')
(lib.mkRemovedOptionModule [ "mailserver" "fullTextSearch" "indexAttachments" ] ''
- Text attachments are always indexed since fts-xapian 1.4.8
+ Text attachments are always indexed since fts-xapian 1.4.8
'')
(lib.mkRenamedOptionModule
[ "mailserver" "rebootAfterKernelUpgrade" "enable" ]
[ "system" "autoUpgrade" "allowReboot" ]
)
(lib.mkRemovedOptionModule [ "mailserver" "rebootAfterKernelUpgrade" "method" ] ''
- Use `system.autoUpgrade` instead.
+ Use `system.autoUpgrade` instead.
'')
./mail-server/assertions.nix
./mail-server/borgbackup.nix
diff --git a/flake.nix b/flake.nix
index e93f8c2..18757c5 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,174 +20,205 @@
};
};
- outputs = { self, blobs, git-hooks, nixpkgs, nixpkgs-25_05, ... }: let
- lib = nixpkgs.lib;
- system = "x86_64-linux";
- pkgs = nixpkgs.legacyPackages.${system};
- releases = [
- {
- name = "unstable";
- nixpkgs = nixpkgs;
- pkgs = nixpkgs.legacyPackages.${system};
- }
- {
- name = "25.05";
- nixpkgs = nixpkgs-25_05;
- pkgs = nixpkgs-25_05.legacyPackages.${system};
- }
- ];
- testNames = [
- "clamav"
- "external"
- "internal"
- "ldap"
- "multiple"
- ];
+ outputs =
+ {
+ self,
+ blobs,
+ git-hooks,
+ nixpkgs,
+ nixpkgs-25_05,
+ ...
+ }:
+ let
+ lib = nixpkgs.lib;
+ system = "x86_64-linux";
+ pkgs = nixpkgs.legacyPackages.${system};
+ releases = [
+ {
+ name = "unstable";
+ nixpkgs = nixpkgs;
+ pkgs = nixpkgs.legacyPackages.${system};
+ }
+ {
+ name = "25.05";
+ nixpkgs = nixpkgs-25_05;
+ pkgs = nixpkgs-25_05.legacyPackages.${system};
+ }
+ ];
+ testNames = [
+ "clamav"
+ "external"
+ "internal"
+ "ldap"
+ "multiple"
+ ];
- genTest = testName: release: let
- pkgs = release.pkgs;
- nixos-lib = import (release.nixpkgs + "/nixos/lib") {
- inherit (pkgs) lib;
- };
- in {
- name = "${testName}-${builtins.replaceStrings ["."] ["_"] release.name}";
- value = nixos-lib.runTest {
- hostPkgs = pkgs;
- imports = [ ./tests/${testName}.nix ];
- _module.args = { inherit blobs; };
- extraBaseModules.imports = [ ./default.nix ];
- };
- };
-
- # Generate an attribute set such as
- # {
- # external-unstable = ;
- # external-21_05 = ;
- # ...
- # }
- 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
- eval = lib.evalModules {
- modules = [
- mailserverModule
- {
- _module.check = false;
- mailserver = {
- fqdn = "mx.example.com";
- domains = [
- "example.com"
- ];
- dmarcReporting = {
- organizationName = "Example Corp";
- domain = "example.com";
- };
- };
- }
- ];
- };
- options = builtins.toFile "options.json" (builtins.toJSON
- (lib.filter (opt: opt.visible && !opt.internal && lib.head opt.loc == "mailserver")
- (lib.optionAttrSetToDocList eval.options)));
- in pkgs.runCommand "options.md" { buildInputs = [pkgs.python3Minimal]; } ''
- echo "Generating options.md from ${options}"
- python ${./scripts/generate-options.py} ${options} > $out
- echo $out
- '';
-
- documentation = pkgs.stdenv.mkDerivation {
- name = "documentation";
- 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
- unset SOURCE_DATE_EPOCH
- make html
- '';
- installPhase = ''
- cp -Tr _build/html $out
- '';
- };
-
- in {
- nixosModules = rec {
- mailserver = mailserverModule;
- default = mailserver;
- };
- nixosModule = self.nixosModules.default; # compatibility
- hydraJobs.${system} = allTests // {
- inherit documentation;
- inherit (self.checks.${system}) pre-commit;
- };
- checks.${system} = allTests // {
- pre-commit = git-hooks.lib.${system}.run {
- src = ./.;
- hooks = {
- # docs
- markdownlint = {
- enable = true;
- settings.configuration = {
- # Max line length, doesn't seem to correclty account for lines containing links
- # https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md
- MD013 = false;
- };
+ genTest =
+ testName: release:
+ let
+ pkgs = release.pkgs;
+ nixos-lib = import (release.nixpkgs + "/nixos/lib") {
+ inherit (pkgs) lib;
};
- rstcheck = {
- enable = true;
- package = pkgs.rstcheckWithSphinx;
- entry = lib.getExe pkgs.rstcheckWithSphinx;
- files = "\\.rst$";
+ in
+ {
+ name = "${testName}-${builtins.replaceStrings [ "." ] [ "_" ] release.name}";
+ value = nixos-lib.runTest {
+ hostPkgs = pkgs;
+ imports = [ ./tests/${testName}.nix ];
+ _module.args = { inherit blobs; };
+ extraBaseModules.imports = [ ./default.nix ];
};
+ };
- # nix
- deadnix.enable = true;
+ # Generate an attribute set such as
+ # {
+ # external-unstable = ;
+ # external-21_05 = ;
+ # ...
+ # }
+ allTests = lib.listToAttrs (lib.flatten (map (t: map (r: genTest t r) releases) testNames));
- # python
- pyright.enable = true;
- ruff = {
- enable = true;
- args = [
- "--extend-select"
- "I"
+ mailserverModule = import ./.;
+
+ # Generate a MarkDown file describing the options of the NixOS mailserver module
+ optionsDoc =
+ let
+ eval = lib.evalModules {
+ modules = [
+ mailserverModule
+ {
+ _module.check = false;
+ mailserver = {
+ fqdn = "mx.example.com";
+ domains = [
+ "example.com"
+ ];
+ dmarcReporting = {
+ organizationName = "Example Corp";
+ domain = "example.com";
+ };
+ };
+ }
];
};
- ruff-format.enable = true;
+ 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
+ '';
- # scripts
- shellcheck.enable = true;
+ documentation = pkgs.stdenv.mkDerivation {
+ name = "documentation";
+ 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
+ unset SOURCE_DATE_EPOCH
+ make html
+ '';
+ installPhase = ''
+ cp -Tr _build/html $out
+ '';
+ };
- # sieve
- check-sieve = {
- enable = true;
- package = pkgs.check-sieve;
- entry = lib.getExe pkgs.check-sieve;
- files = "\\.sieve$";
+ in
+ {
+ nixosModules = rec {
+ mailserver = mailserverModule;
+ default = mailserver;
+ };
+ nixosModule = self.nixosModules.default; # compatibility
+ hydraJobs.${system} = allTests // {
+ inherit documentation;
+ inherit (self.checks.${system}) pre-commit;
+ };
+ checks.${system} = allTests // {
+ pre-commit = git-hooks.lib.${system}.run {
+ src = ./.;
+ hooks = {
+ # docs
+ markdownlint = {
+ enable = true;
+ settings.configuration = {
+ # Max line length, doesn't seem to correclty account for lines containing links
+ # https://github.com/DavidAnson/markdownlint/blob/main/doc/md013.md
+ MD013 = false;
+ };
+ };
+ rstcheck = {
+ enable = true;
+ package = pkgs.rstcheckWithSphinx;
+ entry = lib.getExe pkgs.rstcheckWithSphinx;
+ files = "\\.rst$";
+ };
+
+ # nix
+ deadnix.enable = true;
+ nixfmt-rfc-style.enable = true;
+
+ # python
+ pyright.enable = true;
+ ruff = {
+ enable = true;
+ args = [
+ "--extend-select"
+ "I"
+ ];
+ };
+ ruff-format.enable = true;
+
+ # scripts
+ shellcheck.enable = true;
+
+ # sieve
+ check-sieve = {
+ enable = true;
+ package = pkgs.check-sieve;
+ entry = lib.getExe pkgs.check-sieve;
+ files = "\\.sieve$";
+ };
};
};
};
+ packages.${system} = {
+ inherit optionsDoc documentation;
+ };
+ devShells.${system}.default = pkgs.mkShellNoCC {
+ inputsFrom = [ documentation ];
+ packages =
+ with pkgs;
+ [
+ glab
+ ]
+ ++ self.checks.${system}.pre-commit.enabledPackages;
+ shellHook = self.checks.${system}.pre-commit.shellHook;
+ };
+ devShell.${system} = self.devShells.${system}.default; # compatibility
+
+ formatter.${system} = pkgs.nixfmt-tree;
};
- packages.${system} = {
- inherit optionsDoc documentation;
- };
- devShells.${system}.default = pkgs.mkShellNoCC {
- inputsFrom = [ documentation ];
- packages = with pkgs; [
- glab
- ] ++ self.checks.${system}.pre-commit.enabledPackages;
- shellHook = self.checks.${system}.pre-commit.shellHook;
- };
- devShell.${system} = self.devShells.${system}.default; # compatibility
- };
}
diff --git a/mail-server/borgbackup.nix b/mail-server/borgbackup.nix
index ef83b0d..51ae986 100644
--- a/mail-server/borgbackup.nix
+++ b/mail-server/borgbackup.nix
@@ -14,28 +14,44 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-{ 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
diff --git a/mail-server/common.nix b/mail-server/common.nix
index 813a5f4..cb044b6 100644
--- a/mail-server/common.nix
+++ b/mail-server/common.nix
@@ -14,57 +14,76 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-{ 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
- mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
- in
- lib.mapAttrs (name: value:
- if value.hashedPasswordFile == null then
- builtins.toString (mkHashFile name value.hashedPassword)
- else value.hashedPasswordFile) cfg.loginAccounts;
+ passwordFiles =
+ let
+ mkHashFile = name: hash: pkgs.writeText "${builtins.hashString "sha256" name}-password-hash" hash;
+ in
+ lib.mapAttrs (
+ name: value:
+ if value.hashedPasswordFile == null then
+ builtins.toString (mkHashFile name value.hashedPassword)
+ else
+ value.hashedPasswordFile
+ ) cfg.loginAccounts;
# Appends the LDAP bind password to files to avoid writing this
# password into the Nix store.
- appendLdapBindPwd = {
- name, file, prefix, suffix ? "", passwordFile, destination
- }: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
- #!${pkgs.stdenv.shell}
- set -euo pipefail
+ appendLdapBindPwd =
+ {
+ name,
+ file,
+ prefix,
+ suffix ? "",
+ passwordFile,
+ destination,
+ }:
+ pkgs.writeScript "append-ldap-bind-pwd-in-${name}" ''
+ #!${pkgs.stdenv.shell}
+ set -euo pipefail
- baseDir=$(dirname ${destination})
- if (! test -d "$baseDir"); then
- mkdir -p $baseDir
- chmod 755 $baseDir
- fi
+ baseDir=$(dirname ${destination})
+ if (! test -d "$baseDir"); then
+ mkdir -p $baseDir
+ chmod 755 $baseDir
+ fi
- cat ${file} > ${destination}
- echo -n '${prefix}' >> ${destination}
- cat ${passwordFile} | tr -d '\n' >> ${destination}
- echo -n '${suffix}' >> ${destination}
- chmod 600 ${destination}
- '';
+ cat ${file} > ${destination}
+ echo -n '${prefix}' >> ${destination}
+ cat ${passwordFile} | tr -d '\n' >> ${destination}
+ echo -n '${suffix}' >> ${destination}
+ chmod 600 ${destination}
+ '';
}
diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix
index c06b478..148befc 100644
--- a/mail-server/dovecot.nix
+++ b/mail-server/dovecot.nix
@@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-{ 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}";
- value = x;
- }) attrs);
+ listToMultiAttrs =
+ keyPrefix: attrs:
+ lib.listToAttrs (
+ lib.imap1 (n: x: {
+ name = "${keyPrefix}${if n == 1 then "" else toString n}";
+ value = x;
+ }) attrs
+ );
maildirLayoutAppendix = lib.optionalString cfg.useFsLayout ":LAYOUT=fs";
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;
@@ -51,7 +58,7 @@ let
ldap_version = 3
uris = ${lib.concatStringsSep " " cfg.ldap.uris}
${lib.optionalString cfg.ldap.startTls ''
- tls = yes
+ tls = yes
''}
tls_require_cert = hard
tls_ca_cert_file = ${cfg.ldap.tlsCAFile}
@@ -61,11 +68,11 @@ let
base = ${cfg.ldap.searchBase}
scope = ${mkLdapSearchScope cfg.ldap.searchScope}
${lib.optionalString (cfg.ldap.dovecot.userAttrs != null) ''
- user_attrs = ${cfg.ldap.dovecot.userAttrs}
+ user_attrs = ${cfg.ldap.dovecot.userAttrs}
''}
user_filter = ${cfg.ldap.dovecot.userFilter}
${lib.optionalString (cfg.ldap.dovecot.passAttrs != "") ''
- pass_attrs = ${cfg.ldap.dovecot.passAttrs}
+ pass_attrs = ${cfg.ldap.dovecot.passAttrs}
''}
pass_filter = ${cfg.ldap.dovecot.passFilter}
'';
@@ -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 < ${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 < ${userdbFile}
- ${lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value:
- "${name}:::::::"
+ ${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,255 +163,283 @@ let
in
{
- config = with cfg; lib.mkIf enable {
- assertions = [
- {
- assertion = junkMailboxNumber == 1;
- message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
- }
- ];
-
- warnings =
- (lib.optional (
- (builtins.length cfg.fullTextSearch.languages > 1) &&
- (builtins.elem "stopwords" cfg.fullTextSearch.filters)
- ) ''
- Using stopwords in `mailserver.fullTextSearch.filters` with multiple
- languages in `mailserver.fullTextSearch.languages` configured WILL
- cause some searches to fail.
-
- The recommended solution is to NOT use the stopword filter when
- multiple languages are present in the configuration.
- '')
- ;
-
- # for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
- # the global config and tries to open shared libraries configured in there,
- # which are usually not compatible.
- environment.systemPackages = [
- pkgs.dovecot_pigeonhole
- ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
-
- # For compatibility with python imaplib
- environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
-
- services.dovecot2 = {
- enable = true;
- enableImap = enableImap || enableImapSsl;
- enablePop3 = enablePop3 || enablePop3Ssl;
- enablePAM = false;
- enableQuota = true;
- mailGroup = vmailGroupName;
- mailUser = vmailUserName;
- mailLocation = dovecotMaildir;
- sslServerCert = certificatePath;
- sslServerKey = keyPath;
- enableDHE = lib.mkDefault false;
- enableLmtp = true;
- mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [
- "fts"
- "fts_flatcurve"
+ config =
+ with cfg;
+ lib.mkIf enable {
+ assertions = [
+ {
+ assertion = junkMailboxNumber == 1;
+ message = "nixos-mailserver requires exactly one dovecot mailbox with the 'special use' flag set to 'Junk' (${builtins.toString junkMailboxNumber} have been found)";
+ }
];
- protocols = lib.optional cfg.enableManageSieve "sieve";
- pluginSettings = {
- sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve";
- sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve";
- sieve_default_name = "default";
- } // (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings);
+ warnings =
+ lib.optional
+ (
+ (builtins.length cfg.fullTextSearch.languages > 1)
+ && (builtins.elem "stopwords" cfg.fullTextSearch.filters)
+ )
+ ''
+ Using stopwords in `mailserver.fullTextSearch.filters` with multiple
+ languages in `mailserver.fullTextSearch.languages` configured WILL
+ cause some searches to fail.
- sieve = {
- extensions = [
- "fileinto"
+ The recommended solution is to NOT use the stopword filter when
+ multiple languages are present in the configuration.
+ '';
+
+ # for sieve-test. Shelling it in on demand usually doesnt' work, as it reads
+ # the global config and tries to open shared libraries configured in there,
+ # which are usually not compatible.
+ environment.systemPackages = [
+ pkgs.dovecot_pigeonhole
+ ] ++ lib.optional cfg.fullTextSearch.enable pkgs.dovecot-fts-flatcurve;
+
+ # For compatibility with python imaplib
+ environment.etc."dovecot/modules".source = "/run/current-system/sw/lib/dovecot/modules";
+
+ services.dovecot2 = {
+ enable = true;
+ enableImap = enableImap || enableImapSsl;
+ enablePop3 = enablePop3 || enablePop3Ssl;
+ enablePAM = false;
+ enableQuota = true;
+ mailGroup = vmailGroupName;
+ mailUser = vmailUserName;
+ mailLocation = dovecotMaildir;
+ sslServerCert = certificatePath;
+ sslServerKey = keyPath;
+ enableDHE = lib.mkDefault false;
+ enableLmtp = true;
+ mailPlugins.globally.enable = lib.optionals cfg.fullTextSearch.enable [
+ "fts"
+ "fts_flatcurve"
];
+ protocols = lib.optional cfg.enableManageSieve "sieve";
- scripts.after = builtins.toFile "spam.sieve" ''
- require "fileinto";
+ pluginSettings = {
+ sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve";
+ sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve";
+ sieve_default_name = "default";
+ } // (lib.optionalAttrs cfg.fullTextSearch.enable ftsPluginSettings);
- if header :is "X-Spam" "Yes" {
- fileinto "${junkMailboxName}";
- stop;
+ sieve = {
+ extensions = [
+ "fileinto"
+ ];
+
+ scripts.after = builtins.toFile "spam.sieve" ''
+ require "fileinto";
+
+ if header :is "X-Spam" "Yes" {
+ fileinto "${junkMailboxName}";
+ stop;
+ }
+ '';
+
+ pipeBins = map lib.getExe [
+ (pkgs.writeShellScriptBin "rspamd-learn-ham.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_ham")
+ (pkgs.writeShellScriptBin "rspamd-learn-spam.sh" "exec ${pkgs.rspamd}/bin/rspamc -h /run/rspamd/worker-controller.sock learn_spam")
+ ];
+ };
+
+ imapsieve.mailbox = [
+ {
+ name = junkMailboxName;
+ causes = [
+ "COPY"
+ "APPEND"
+ ];
+ before = ./dovecot/imap_sieve/report-spam.sieve;
+ }
+ {
+ name = "*";
+ from = junkMailboxName;
+ causes = [ "COPY" ];
+ before = ./dovecot/imap_sieve/report-ham.sieve;
}
- '';
-
- 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")
];
+
+ mailboxes = cfg.mailboxes;
+
+ extraConfig = ''
+ #Extra Config
+ ${lib.optionalString debug ''
+ mail_debug = yes
+ auth_debug = yes
+ verbose_ssl = yes
+ ''}
+
+ ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
+ service imap-login {
+ inet_listener imap {
+ ${
+ if cfg.enableImap then
+ ''
+ port = 143
+ ''
+ else
+ ''
+ # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
+ port = 0
+ ''
+ }
+ }
+ inet_listener imaps {
+ ${
+ if cfg.enableImapSsl then
+ ''
+ port = 993
+ ssl = yes
+ ''
+ 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
+ ''
+ port = 110
+ ''
+ else
+ ''
+ # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
+ port = 0
+ ''
+ }
+ }
+ inet_listener pop3s {
+ ${
+ if cfg.enablePop3Ssl then
+ ''
+ port = 995
+ ssl = yes
+ ''
+ else
+ ''
+ # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
+ port = 0
+ ''
+ }
+ }
+ }
+ ''}
+
+ protocol imap {
+ mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
+ mail_plugins = $mail_plugins imap_sieve
+ }
+
+ service imap {
+ vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB
+ }
+
+ protocol pop3 {
+ mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
+ }
+
+ mail_access_groups = ${vmailGroupName}
+
+ # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7
+ ssl = required
+ ssl_min_protocol = TLSv1.2
+ ssl_prefer_server_ciphers = no
+ ssl_curve_list = X25519:prime256v1:secp384r1
+
+ service lmtp {
+ unix_listener dovecot-lmtp {
+ group = ${postfixCfg.group}
+ mode = 0600
+ user = ${postfixCfg.user}
+ }
+ vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB
+ }
+
+ service quota-status {
+ inet_listener {
+ port = 0
+ }
+ unix_listener quota-status {
+ user = postfix
+ }
+ vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB
+ }
+
+ recipient_delimiter = ${cfg.recipientDelimiter}
+ lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
+
+ protocol lmtp {
+ mail_plugins = $mail_plugins sieve
+ }
+
+ passdb {
+ driver = passwd-file
+ args = ${passwdFile}
+ }
+
+ userdb {
+ driver = passwd-file
+ args = ${userdbFile}
+ default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}
+ }
+
+ ${lib.optionalString cfg.ldap.enable ''
+ passdb {
+ driver = ldap
+ args = ${ldapConfFile}
+ }
+
+ userdb {
+ driver = ldap
+ args = ${ldapConfFile}
+ default_fields = home=${cfg.mailDirectory}/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID}
+ }
+ ''}
+
+ service auth {
+ unix_listener auth {
+ mode = 0660
+ user = ${postfixCfg.user}
+ group = ${postfixCfg.group}
+ }
+ }
+
+ auth_mechanisms = plain login
+
+ namespace inbox {
+ separator = ${cfg.hierarchySeparator}
+ inbox = yes
+ }
+
+ service indexer-worker {
+ ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
+ vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit * 1024 * 1024)}
+ ''}
+ }
+
+ lda_mailbox_autosubscribe = yes
+ lda_mailbox_autocreate = yes
+ '';
};
- imapsieve.mailbox = [
- {
- name = junkMailboxName;
- causes = [ "COPY" "APPEND" ];
- before = ./dovecot/imap_sieve/report-spam.sieve;
- }
- {
- name = "*";
- from = junkMailboxName;
- causes = [ "COPY" ];
- before = ./dovecot/imap_sieve/report-ham.sieve;
- }
- ];
+ systemd.services.dovecot2 = {
+ preStart =
+ ''
+ ${genPasswdScript}
+ ''
+ + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
+ };
- mailboxes = cfg.mailboxes;
-
- extraConfig = ''
- #Extra Config
- ${lib.optionalString debug ''
- mail_debug = yes
- auth_debug = yes
- verbose_ssl = yes
- ''}
-
- ${lib.optionalString (cfg.enableImap || cfg.enableImapSsl) ''
- service imap-login {
- inet_listener imap {
- ${if cfg.enableImap then ''
- port = 143
- '' else ''
- # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
- port = 0
- ''}
- }
- inet_listener imaps {
- ${if cfg.enableImapSsl then ''
- port = 993
- ssl = yes
- '' 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 ''
- port = 110
- '' else ''
- # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
- port = 0
- ''}
- }
- inet_listener pop3s {
- ${if cfg.enablePop3Ssl then ''
- port = 995
- ssl = yes
- '' else ''
- # see https://dovecot.org/pipermail/dovecot/2010-March/047479.html
- port = 0
- ''}
- }
- }
- ''}
-
- protocol imap {
- mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
- mail_plugins = $mail_plugins imap_sieve
- }
-
- service imap {
- vsz_limit = ${builtins.toString cfg.imapMemoryLimit} MB
- }
-
- protocol pop3 {
- mail_max_userip_connections = ${toString cfg.maxConnectionsPerUser}
- }
-
- mail_access_groups = ${vmailGroupName}
-
- # https://ssl-config.mozilla.org/#server=dovecot&version=2.3.21&config=intermediate&openssl=3.4.1&guideline=5.7
- ssl = required
- ssl_min_protocol = TLSv1.2
- ssl_prefer_server_ciphers = no
- ssl_curve_list = X25519:prime256v1:secp384r1
-
- service lmtp {
- unix_listener dovecot-lmtp {
- group = ${postfixCfg.group}
- mode = 0600
- user = ${postfixCfg.user}
- }
- vsz_limit = ${builtins.toString cfg.lmtpMemoryLimit} MB
- }
-
- service quota-status {
- inet_listener {
- port = 0
- }
- unix_listener quota-status {
- user = postfix
- }
- vsz_limit = ${builtins.toString cfg.quotaStatusMemoryLimit} MB
- }
-
- recipient_delimiter = ${cfg.recipientDelimiter}
- lmtp_save_to_detail_mailbox = ${cfg.lmtpSaveToDetailMailbox}
-
- protocol lmtp {
- mail_plugins = $mail_plugins sieve
- }
-
- passdb {
- driver = passwd-file
- args = ${passwdFile}
- }
-
- userdb {
- driver = passwd-file
- args = ${userdbFile}
- default_fields = uid=${builtins.toString cfg.vmailUID} gid=${builtins.toString cfg.vmailUID} home=${cfg.mailDirectory}
- }
-
- ${lib.optionalString cfg.ldap.enable ''
- passdb {
- driver = ldap
- args = ${ldapConfFile}
- }
-
- userdb {
- driver = ldap
- args = ${ldapConfFile}
- default_fields = home=${cfg.mailDirectory}/ldap/%{user} uid=${toString cfg.vmailUID} gid=${toString cfg.vmailUID}
- }
- ''}
-
- service auth {
- unix_listener auth {
- mode = 0660
- user = ${postfixCfg.user}
- group = ${postfixCfg.group}
- }
- }
-
- auth_mechanisms = plain login
-
- namespace inbox {
- separator = ${cfg.hierarchySeparator}
- inbox = yes
- }
-
- service indexer-worker {
- ${lib.optionalString (cfg.fullTextSearch.memoryLimit != null) ''
- vsz_limit = ${toString (cfg.fullTextSearch.memoryLimit*1024*1024)}
- ''}
- }
-
- lda_mailbox_autosubscribe = yes
- lda_mailbox_autocreate = yes
- '';
+ systemd.services.postfix.restartTriggers = [
+ genPasswdScript
+ ] ++ (lib.optional cfg.ldap.enable [ setPwdInLdapConfFile ]);
};
-
- systemd.services.dovecot2 = {
- preStart = ''
- ${genPasswdScript}
- '' + (lib.optionalString cfg.ldap.enable setPwdInLdapConfFile);
- };
-
- systemd.services.postfix.restartTriggers = [ genPasswdScript ] ++ (lib.optional cfg.ldap.enable [setPwdInLdapConfFile]);
- };
}
diff --git a/mail-server/environment.nix b/mail-server/environment.nix
index b4326a1..b853211 100644
--- a/mail-server/environment.nix
+++ b/mail-server/environment.nix
@@ -14,15 +14,28 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-{ 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 [ ]);
+ };
}
diff --git a/mail-server/kresd.nix b/mail-server/kresd.nix
index 230bdea..3920534 100644
--- a/mail-server/kresd.nix
+++ b/mail-server/kresd.nix
@@ -24,4 +24,3 @@ in
services.kresd.enable = true;
};
}
-
diff --git a/mail-server/networking.nix b/mail-server/networking.nix
index 6af186a..587a8ae 100644
--- a/mail-server/networking.nix
+++ b/mail-server/networking.nix
@@ -20,18 +20,21 @@ let
cfg = config.mailserver;
in
{
- config = with cfg; lib.mkIf (enable && openFirewall) {
+ config =
+ with cfg;
+ lib.mkIf (enable && openFirewall) {
- networking.firewall = {
- allowedTCPPorts = [ 25 ]
- ++ lib.optional enableSubmission 587
- ++ lib.optional enableSubmissionSsl 465
- ++ lib.optional enableImap 143
- ++ lib.optional enableImapSsl 993
- ++ lib.optional enablePop3 110
- ++ lib.optional enablePop3Ssl 995
- ++ lib.optional enableManageSieve 4190
- ++ lib.optional (certificateScheme == "acme-nginx") 80;
+ networking.firewall = {
+ allowedTCPPorts =
+ [ 25 ]
+ ++ lib.optional enableSubmission 587
+ ++ lib.optional enableSubmissionSsl 465
+ ++ lib.optional enableImap 143
+ ++ lib.optional enableImapSsl 993
+ ++ lib.optional enablePop3 110
+ ++ lib.optional enablePop3Ssl 995
+ ++ lib.optional enableManageSieve 4190
+ ++ lib.optional (certificateScheme == "acme-nginx") 80;
+ };
};
- };
}
diff --git a/mail-server/nginx.nix b/mail-server/nginx.nix
index a037f56..27de2fe 100644
--- a/mail-server/nginx.nix
+++ b/mail-server/nginx.nix
@@ -14,8 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-
-{ config, pkgs, lib, ... }:
+{
+ config,
+ pkgs,
+ lib,
+ ...
+}:
with (import ./common.nix { inherit config lib pkgs; });
@@ -23,20 +27,22 @@ let
cfg = config.mailserver;
in
{
- 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}" = {
- serverName = cfg.fqdn;
- serverAliases = cfg.certificateDomains;
- forceSSL = true;
- enableACME = true;
- };
- };
+ 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}" = {
+ serverName = cfg.fqdn;
+ serverAliases = cfg.certificateDomains;
+ forceSSL = true;
+ enableACME = true;
+ };
+ };
- security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
- "postfix.service"
- "dovecot2.service"
- ];
- };
+ security.acme.certs."${cfg.acmeCertificateName}".reloadServices = [
+ "postfix.service"
+ "dovecot2.service"
+ ];
+ };
}
diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix
index e45a725..4237efc 100644
--- a/mail-server/postfix.nix
+++ b/mail-server/postfix.nix
@@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-{ 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
- valueToString = value: lib.concatStringsSep ", " value;
- in lib.concatStringsSep "\n" (lib.mapAttrsToList (name: value: "${name} ${valueToString value}") attrs);
+ lookupTableToString =
+ attrs:
+ let
+ valueToString = value: lib.concatStringsSep ", " value;
+ 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
- content = lookupTableToString regex_valiases_postfix;
- in builtins.toFile "regex_valias" content;
+ regex_valiases_file =
+ let
+ content = lookupTableToString regex_valiases_postfix;
+ 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,45 +144,51 @@ 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" (''
- # Removes sensitive headers from mails handed in via the submission port.
- # See https://thomas-leister.de/mailserver-debian-stretch/
- # Uses "pcre" style regex.
+ 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.
- /^Received:/ IGNORE
- /^X-Originating-IP:/ IGNORE
- /^X-Mailer:/ IGNORE
- /^User-Agent:/ IGNORE
- /^X-Enigmail:/ IGNORE
- '' + lib.optionalString cfg.rewriteMessageId ''
+ /^Received:/ IGNORE
+ /^X-Originating-IP:/ IGNORE
+ /^X-Mailer:/ IGNORE
+ /^User-Agent:/ IGNORE
+ /^X-Enigmail:/ IGNORE
+ ''
+ + lib.optionalString cfg.rewriteMessageId ''
- # Replaces the user submitted hostname with the server's FQDN to hide the
- # user's host or network.
+ # 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}>
- '');
+ /^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 =
- {
- smtpd_tls_security_level = "encrypt";
- smtpd_sasl_auth_enable = "yes";
- smtpd_sasl_type = "dovecot";
- smtpd_sasl_path = "/run/dovecot2/auth";
- 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_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";
- };
+ submissionOptions = {
+ smtpd_tls_security_level = "encrypt";
+ smtpd_sasl_auth_enable = "yes";
+ smtpd_sasl_type = "dovecot";
+ smtpd_sasl_path = "/run/dovecot2/auth";
+ 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_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";
+ };
commonLdapConfig = ''
server_host = ${lib.concatStringsSep " " cfg.ldap.uris}
@@ -186,164 +233,183 @@ 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 ];
- };
-
- services.postfix = {
- enable = true;
- hostname = "${sendingFqdn}";
- networksStyle = "host";
- mapFiles."valias" = valiases_file;
- mapFiles."regex_valias" = regex_valiases_file;
- mapFiles."vaccounts" = vaccounts_file;
- mapFiles."regex_vaccounts" = regex_vaccounts_file;
- mapFiles."denied_recipients" = denied_recipients_file;
- mapFiles."reject_senders" = reject_senders_file;
- mapFiles."reject_recipients" = reject_recipients_file;
- enableSubmission = cfg.enableSubmission;
- enableSubmissions = cfg.enableSubmissionSsl;
- virtual = lookupTableToString (mergeLookupTables [all_valiases_postfix catchAllPostfix forwards]);
-
- config = {
- smtpd_tls_chain_files = [
- "${keyPath}"
- "${certificatePath}"
+ systemd.services.postfix-setup = lib.mkIf cfg.ldap.enable {
+ preStart = ''
+ ${appendPwdInVirtualMailboxMap}
+ ${appendPwdInSenderLoginMap}
+ '';
+ restartTriggers = [
+ appendPwdInVirtualMailboxMap
+ appendPwdInSenderLoginMap
];
+ };
- # Extra Config
- mydestination = "";
- recipient_delimiter = cfg.recipientDelimiter;
- smtpd_banner = "${fqdn} ESMTP NO UCE";
- disable_vrfy_command = true;
- message_size_limit = toString cfg.messageSizeLimit;
-
- # virtual mail system
- virtual_uid_maps = "static:5000";
- virtual_gid_maps = "static:5000";
- virtual_mailbox_base = mailDirectory;
- virtual_mailbox_domains = vhosts_file;
- virtual_mailbox_maps = [
- (mappedFile "valias")
- ] ++ lib.optionals (cfg.ldap.enable) [
- "ldap:${ldapVirtualMailboxMapFile}"
- ] ++ lib.optionals (regex_valiases_postfix != {}) [
- (mappedRegexFile "regex_valias")
- ];
- virtual_alias_maps = lib.mkAfter (lib.optionals (regex_valiases_postfix != {}) [
- (mappedRegexFile "regex_valias")
+ services.postfix = {
+ enable = true;
+ hostname = "${sendingFqdn}";
+ networksStyle = "host";
+ mapFiles."valias" = valiases_file;
+ mapFiles."regex_valias" = regex_valiases_file;
+ mapFiles."vaccounts" = vaccounts_file;
+ mapFiles."regex_vaccounts" = regex_vaccounts_file;
+ mapFiles."denied_recipients" = denied_recipients_file;
+ mapFiles."reject_senders" = reject_senders_file;
+ mapFiles."reject_recipients" = reject_recipients_file;
+ enableSubmission = cfg.enableSubmission;
+ enableSubmissions = cfg.enableSubmissionSsl;
+ virtual = lookupTableToString (mergeLookupTables [
+ all_valiases_postfix
+ catchAllPostfix
+ forwards
]);
- 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";
- # sasl with dovecot
- smtpd_sasl_type = "dovecot";
- smtpd_sasl_path = "/run/dovecot2/auth";
- smtpd_sasl_auth_enable = true;
- smtpd_relay_restrictions = [
- "permit_mynetworks" "permit_sasl_authenticated" "reject_unauth_destination"
- ];
+ config = {
+ smtpd_tls_chain_files = [
+ "${keyPath}"
+ "${certificatePath}"
+ ];
- # reject selected senders
- smtpd_sender_restrictions = [
- "check_sender_access ${mappedFile "reject_senders"}"
- ];
+ # Extra Config
+ mydestination = "";
+ recipient_delimiter = cfg.recipientDelimiter;
+ smtpd_banner = "${fqdn} ESMTP NO UCE";
+ disable_vrfy_command = true;
+ message_size_limit = toString cfg.messageSizeLimit;
- smtpd_recipient_restrictions = [
- # reject selected recipients
- "check_recipient_access ${mappedFile "denied_recipients"}"
- "check_recipient_access ${mappedFile "reject_recipients"}"
- # quota checking
- "check_policy_service unix:/run/dovecot2/quota-status"
- ];
+ # virtual mail system
+ virtual_uid_maps = "static:5000";
+ virtual_gid_maps = "static:5000";
+ virtual_mailbox_base = mailDirectory;
+ virtual_mailbox_domains = vhosts_file;
+ virtual_mailbox_maps =
+ [
+ (mappedFile "valias")
+ ]
+ ++ lib.optionals cfg.ldap.enable [
+ "ldap:${ldapVirtualMailboxMapFile}"
+ ]
+ ++ lib.optionals (regex_valiases_postfix != { }) [
+ (mappedRegexFile "regex_valias")
+ ];
+ virtual_alias_maps = lib.mkAfter (
+ lib.optionals (regex_valiases_postfix != { }) [
+ (mappedRegexFile "regex_valias")
+ ]
+ );
+ virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
+ # Avoid leakage of X-Original-To, X-Delivered-To headers between recipients
+ lmtp_destination_recipient_limit = "1";
- # TLS for incoming mail is optional
- smtpd_tls_security_level = "may";
+ # sasl with dovecot
+ smtpd_sasl_type = "dovecot";
+ smtpd_sasl_path = "/run/dovecot2/auth";
+ smtpd_sasl_auth_enable = true;
+ smtpd_relay_restrictions = [
+ "permit_mynetworks"
+ "permit_sasl_authenticated"
+ "reject_unauth_destination"
+ ];
- # But required for authentication attempts
- smtpd_tls_auth_only = true;
+ # reject selected senders
+ smtpd_sender_restrictions = [
+ "check_sender_access ${mappedFile "reject_senders"}"
+ ];
- # TLS versions supported for the SMTP server
- smtpd_tls_protocols = ">=TLSv1.2";
- smtpd_tls_mandatory_protocols = ">=TLSv1.2";
+ smtpd_recipient_restrictions = [
+ # reject selected recipients
+ "check_recipient_access ${mappedFile "denied_recipients"}"
+ "check_recipient_access ${mappedFile "reject_recipients"}"
+ # quota checking
+ "check_policy_service unix:/run/dovecot2/quota-status"
+ ];
- # Require ciphersuites that OpenSSL classifies as "High"
- smtpd_tls_ciphers = "high";
- smtpd_tls_mandatory_ciphers = "high";
+ # TLS for incoming mail is optional
+ smtpd_tls_security_level = "may";
- # Exclude cipher suites with undesirable properties
- smtpd_tls_exclude_ciphers = "eNULL, aNULL";
- smtpd_tls_mandatory_exclude_ciphers = "eNULL, aNULL";
+ # But required for authentication attempts
+ smtpd_tls_auth_only = true;
- # Opportunistic DANE support when delivering mail to other servers
- # https://www.postfix.org/postconf.5.html#smtp_tls_security_level
- smtp_dns_support_level = "dnssec";
- smtp_tls_security_level = "dane";
+ # TLS versions supported for the SMTP server
+ smtpd_tls_protocols = ">=TLSv1.2";
+ smtpd_tls_mandatory_protocols = ">=TLSv1.2";
- # TLS versions supported for the SMTP client
- smtp_tls_protocols = ">=TLSv1.2";
- smtp_tls_mandatory_protocols = ">=TLSv1.2";
+ # Require ciphersuites that OpenSSL classifies as "High"
+ smtpd_tls_ciphers = "high";
+ smtpd_tls_mandatory_ciphers = "high";
- # Require ciphersuites that OpenSSL classifies as "High"
- smtp_tls_ciphers = "high";
- smtp_tls_mandatory_ciphers = "high";
+ # Exclude cipher suites with undesirable properties
+ smtpd_tls_exclude_ciphers = "eNULL, aNULL";
+ smtpd_tls_mandatory_exclude_ciphers = "eNULL, aNULL";
- # Exclude ciphersuites with undesirable properties
- smtp_tls_exclude_ciphers = "eNULL, aNULL";
- smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL";
+ # Opportunistic DANE support when delivering mail to other servers
+ # https://www.postfix.org/postconf.5.html#smtp_tls_security_level
+ smtp_dns_support_level = "dnssec";
+ smtp_tls_security_level = "dane";
- # Restrict and prioritize the following curves in the given order
- # Excludes curves that have no widespread support, so we don't bloat the handshake needlessly.
- # https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves
- # https://ssl-config.mozilla.org/#server=postfix&version=3.10&config=intermediate&openssl=3.4.1&guideline=5.7
- tls_eecdh_auto_curves = [
- "X25519"
- "prime256v1"
- "secp384r1"
- ];
+ # TLS versions supported for the SMTP client
+ smtp_tls_protocols = ">=TLSv1.2";
+ smtp_tls_mandatory_protocols = ">=TLSv1.2";
- # Disable FFDHE on TLSv1.3 because it is slower than elliptic curves
- # https://www.postfix.org/postconf.5.html#tls_ffdhe_auto_groups
- tls_ffdhe_auto_groups = [ ];
+ # Require ciphersuites that OpenSSL classifies as "High"
+ smtp_tls_ciphers = "high";
+ smtp_tls_mandatory_ciphers = "high";
- # As long as all cipher suites are considered safe, let the client use its preferred cipher
- tls_preempt_cipherlist = false;
+ # Exclude ciphersuites with undesirable properties
+ smtp_tls_exclude_ciphers = "eNULL, aNULL";
+ smtp_tls_mandatory_exclude_ciphers = "eNULL, aNULL";
- # Log only a summary message on TLS handshake completion
- smtp_tls_loglevel = "1";
- smtpd_tls_loglevel = "1";
+ # Restrict and prioritize the following curves in the given order
+ # Excludes curves that have no widespread support, so we don't bloat the handshake needlessly.
+ # https://www.postfix.org/postconf.5.html#tls_eecdh_auto_curves
+ # https://ssl-config.mozilla.org/#server=postfix&version=3.10&config=intermediate&openssl=3.4.1&guideline=5.7
+ tls_eecdh_auto_curves = [
+ "X25519"
+ "prime256v1"
+ "secp384r1"
+ ];
- smtpd_milters = smtpdMilters;
- non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ];
- milter_protocol = "6";
- milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}";
- };
+ # Disable FFDHE on TLSv1.3 because it is slower than elliptic curves
+ # https://www.postfix.org/postconf.5.html#tls_ffdhe_auto_groups
+ tls_ffdhe_auto_groups = [ ];
- submissionOptions = submissionOptions;
- submissionsOptions = submissionOptions;
+ # As long as all cipher suites are considered safe, let the client use its preferred cipher
+ tls_preempt_cipherlist = false;
- masterConfig = {
- "lmtp" = {
- # Add headers when delivering, see http://www.postfix.org/smtp.8.html
- # D => Delivered-To, O => X-Original-To, R => Return-Path
- args = [ "flags=O" ];
+ # Log only a summary message on TLS handshake completion
+ smtp_tls_loglevel = "1";
+ smtpd_tls_loglevel = "1";
+
+ smtpd_milters = smtpdMilters;
+ non_smtpd_milters = lib.mkIf cfg.dkimSigning [ "unix:/run/rspamd/rspamd-milter.sock" ];
+ milter_protocol = "6";
+ milter_mail_macros = "i {mail_addr} {client_addr} {client_name} {auth_authen}";
};
- "submission-header-cleanup" = {
- type = "unix";
- private = false;
- chroot = false;
- maxproc = 0;
- command = "cleanup";
- args = ["-o" "header_checks=pcre:${submissionHeaderCleanupRules}"];
+
+ submissionOptions = submissionOptions;
+ submissionsOptions = submissionOptions;
+
+ masterConfig = {
+ "lmtp" = {
+ # Add headers when delivering, see http://www.postfix.org/smtp.8.html
+ # D => Delivered-To, O => X-Original-To, R => Return-Path
+ args = [ "flags=O" ];
+ };
+ "submission-header-cleanup" = {
+ type = "unix";
+ private = false;
+ chroot = false;
+ maxproc = 0;
+ command = "cleanup";
+ args = [
+ "-o"
+ "header_checks=pcre:${submissionHeaderCleanupRules}"
+ ];
+ };
};
};
};
- };
}
diff --git a/mail-server/rsnapshot.nix b/mail-server/rsnapshot.nix
index a801c24..de4f13e 100644
--- a/mail-server/rsnapshot.nix
+++ b/mail-server/rsnapshot.nix
@@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-{ 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;
diff --git a/mail-server/rspamd.nix b/mail-server/rspamd.nix
index 0e37c20..a4fcdce 100644
--- a/mail-server/rspamd.nix
+++ b/mail-server/rspamd.nix
@@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-{ config, pkgs, lib, ... }:
+{
+ config,
+ pkgs,
+ lib,
+ ...
+}:
let
cfg = config.mailserver;
@@ -26,56 +31,74 @@ let
rspamdUser = config.services.rspamd.user;
rspamdGroup = config.services.rspamd.group;
- createDkimKeypair = domain: let
- privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
- publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
- in pkgs.writeShellScript "dkim-keygen-${domain}" ''
- if [ ! -f "${privateKey}" ]
- then
- ${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
- --domain "${domain}" \
- --selector "${cfg.dkimSelector}" \
- --type "${cfg.dkimKeyType}" \
- --bits ${toString cfg.dkimKeyBits} \
- --privkey "${privateKey}" > "${publicKey}"
- chmod 0644 "${publicKey}"
- echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
- fi
- '';
+ createDkimKeypair =
+ domain:
+ let
+ privateKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.key";
+ publicKey = "${cfg.dkimKeyDirectory}/${domain}.${cfg.dkimSelector}.txt";
+ in
+ pkgs.writeShellScript "dkim-keygen-${domain}" ''
+ if [ ! -f "${privateKey}" ]
+ then
+ ${lib.getExe' pkgs.rspamd "rspamadm"} dkim_keygen \
+ --domain "${domain}" \
+ --selector "${cfg.dkimSelector}" \
+ --type "${cfg.dkimKeyType}" \
+ --bits ${toString cfg.dkimKeyBits} \
+ --privkey "${privateKey}" > "${publicKey}"
+ chmod 0644 "${publicKey}"
+ echo "Generated key for domain ${domain} and selector ${cfg.dkimSelector}"
+ fi
+ '';
in
{
- config = with cfg; lib.mkIf enable {
- environment.systemPackages = lib.mkBefore [
- (pkgs.runCommand "rspamc-wrapped" {
- nativeBuildInputs = with pkgs; [ makeWrapper ];
- }''
- makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \
- --add-flags "-h /run/rspamd/worker-controller.sock"
- '')
- ];
+ config =
+ with cfg;
+ lib.mkIf enable {
+ environment.systemPackages = lib.mkBefore [
+ (pkgs.runCommand "rspamc-wrapped"
+ {
+ nativeBuildInputs = with pkgs; [ makeWrapper ];
+ }
+ ''
+ makeWrapper ${pkgs.rspamd}/bin/rspamc $out/bin/rspamc \
+ --add-flags "-h /run/rspamd/worker-controller.sock"
+ ''
+ )
+ ];
- services.rspamd = {
- enable = true;
- inherit debug;
- locals = {
- "milter_headers.conf" = { text = ''
+ services.rspamd = {
+ enable = true;
+ inherit debug;
+ locals = {
+ "milter_headers.conf" = {
+ text = ''
extended_spam_headers = true;
- ''; };
- "redis.conf" = { text = ''
- servers = "${if cfg.redis.port == null
- then
- cfg.redis.address
- else
- "${cfg.redis.address}:${toString cfg.redis.port}"}";
- '' + (lib.optionalString (cfg.redis.password != null) ''
- password = "${cfg.redis.password}";
- ''); };
- "classifier-bayes.conf" = { text = ''
+ '';
+ };
+ "redis.conf" = {
+ text =
+ ''
+ servers = "${
+ if cfg.redis.port == null then
+ cfg.redis.address
+ else
+ "${cfg.redis.address}:${toString cfg.redis.port}"
+ }";
+ ''
+ + (lib.optionalString (cfg.redis.password != null) ''
+ password = "${cfg.redis.password}";
+ '');
+ };
+ "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,157 +107,168 @@ 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 = ''
- 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 = ''
+ '';
+ };
+ "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 = ''
${lib.optionalString cfg.dmarcReporting.enable ''
- reporting {
- enabled = true;
- email = "${cfg.dmarcReporting.email}";
- domain = "${cfg.dmarcReporting.domain}";
- org_name = "${cfg.dmarcReporting.organizationName}";
- from_name = "${cfg.dmarcReporting.fromName}";
- msgid_from = "${cfg.dmarcReporting.domain}";
- ${lib.optionalString (cfg.dmarcReporting.excludeDomains != []) ''
- exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
- ''}
- }''}
- ''; };
+ reporting {
+ enabled = true;
+ email = "${cfg.dmarcReporting.email}";
+ domain = "${cfg.dmarcReporting.domain}";
+ org_name = "${cfg.dmarcReporting.organizationName}";
+ from_name = "${cfg.dmarcReporting.fromName}";
+ msgid_from = "${cfg.dmarcReporting.domain}";
+ ${lib.optionalString (cfg.dmarcReporting.excludeDomains != [ ]) ''
+ exclude_domains = ${builtins.toJSON cfg.dmarcReporting.excludeDomains};
+ ''}
+ }''}
+ '';
+ };
+ };
+
+ workers.rspamd_proxy = {
+ type = "rspamd_proxy";
+ 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
+ timeout = 120s; # Needed for Milter usually
+
+ upstream "local" {
+ default = yes; # Self-scan upstreams are always default
+ self_scan = yes; # Enable self-scan
+ }
+ '';
+ };
+ workers.controller = {
+ type = "controller";
+ count = 1;
+ bindSockets = [
+ {
+ socket = "/run/rspamd/worker-controller.sock";
+ mode = "0666";
+ }
+ ];
+ includes = [ ];
+ extraConfig = ''
+ static_dir = "''${WWWDIR}"; # Serve the web UI static assets
+ '';
+ };
+
};
- workers.rspamd_proxy = {
- type = "rspamd_proxy";
- 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
- timeout = 120s; # Needed for Milter usually
+ services.redis.servers.rspamd.enable = lib.mkDefault true;
- upstream "local" {
- default = yes; # Self-scan upstreams are always default
- self_scan = yes; # Enable self-scan
+ systemd.tmpfiles.settings."10-rspamd.conf" = {
+ "${cfg.dkimKeyDirectory}" = {
+ d = {
+ # Create /var/dkim owned by rspamd user/group
+ user = rspamdUser;
+ group = rspamdGroup;
+ };
+ Z = {
+ # Recursively adjust permissions in /var/dkim
+ user = rspamdUser;
+ group = rspamdGroup;
+ };
+ };
+ };
+
+ systemd.services.rspamd = {
+ requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
+ after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
+ serviceConfig = lib.mkMerge [
+ {
+ SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
}
- '';
- };
- workers.controller = {
- type = "controller";
- count = 1;
- bindSockets = [{
- socket = "/run/rspamd/worker-controller.sock";
- mode = "0666";
- }];
- includes = [];
- extraConfig = ''
- static_dir = "''${WWWDIR}"; # Serve the web UI static assets
- '';
- };
-
- };
-
- services.redis.servers.rspamd.enable = lib.mkDefault true;
-
- systemd.tmpfiles.settings."10-rspamd.conf" = {
- "${cfg.dkimKeyDirectory}" = {
- d = {
- # Create /var/dkim owned by rspamd user/group
- user = rspamdUser;
- group = rspamdGroup;
- };
- Z = {
- # Recursively adjust permissions in /var/dkim
- user = rspamdUser;
- group = rspamdGroup;
- };
- };
- };
-
- systemd.services.rspamd = {
- requires = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
- after = [ "redis-rspamd.service" ] ++ (lib.optional cfg.virusScanning "clamav-daemon.service");
- serviceConfig = lib.mkMerge [
- {
- SupplementaryGroups = [ config.services.redis.servers.rspamd.group ];
- }
- (lib.optionalAttrs cfg.dkimSigning {
- ExecStartPre = map createDkimKeypair cfg.domains;
- ReadWritePaths = [ cfg.dkimKeyDirectory ];
- })
- ];
- };
-
- systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
- # Explicitly select yesterday's date to work around broken
- # default behaviour when called without a date.
- # https://github.com/rspamd/rspamd/issues/4062
- script = ''
- ${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
- '';
- serviceConfig = {
- User = "${config.services.rspamd.user}";
- Group = "${config.services.rspamd.group}";
-
- AmbientCapabilities = [];
- CapabilityBoundingSet = "";
- DevicePolicy = "closed";
- IPAddressAllow = "localhost";
- LockPersonality = true;
- NoNewPrivileges = true;
- PrivateDevices = true;
- PrivateMounts = true;
- PrivateTmp = true;
- PrivateUsers = true;
- ProtectClock = true;
- ProtectControlGroups = true;
- ProtectHome = true;
- ProtectHostname = true;
- ProtectKernelLogs = true;
- ProtectKernelModules = true;
- ProtectKernelTunables = true;
- ProtectProc = "invisible";
- ProcSubset = "pid";
- ProtectSystem = "strict";
- RemoveIPC = true;
- RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ];
- RestrictNamespaces = true;
- RestrictRealtime = true;
- RestrictSUIDSGID = true;
- SystemCallArchitectures = "native";
- SystemCallFilter = [
- "@system-service"
- "~@privileged"
+ (lib.optionalAttrs cfg.dkimSigning {
+ ExecStartPre = map createDkimKeypair cfg.domains;
+ ReadWritePaths = [ cfg.dkimKeyDirectory ];
+ })
];
- UMask = "0077";
};
- };
- systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs (cfg.dmarcReporting.enable) {
- description = "Daily delivery of aggregated DMARC reports";
- wantedBy = [
- "timers.target"
- ];
- timerConfig = {
- OnCalendar = "daily";
- Persistent = true;
- RandomizedDelaySec = 86400;
- FixedRandomDelay = true;
+ systemd.services.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
+ # Explicitly select yesterday's date to work around broken
+ # default behaviour when called without a date.
+ # https://github.com/rspamd/rspamd/issues/4062
+ script = ''
+ ${pkgs.rspamd}/bin/rspamadm dmarc_report $(date -d "yesterday" "+%Y%m%d")
+ '';
+ serviceConfig = {
+ User = "${config.services.rspamd.user}";
+ Group = "${config.services.rspamd.group}";
+
+ AmbientCapabilities = [ ];
+ CapabilityBoundingSet = "";
+ DevicePolicy = "closed";
+ IPAddressAllow = "localhost";
+ LockPersonality = true;
+ NoNewPrivileges = true;
+ PrivateDevices = true;
+ PrivateMounts = true;
+ PrivateTmp = true;
+ PrivateUsers = true;
+ ProtectClock = true;
+ ProtectControlGroups = true;
+ ProtectHome = true;
+ ProtectHostname = true;
+ ProtectKernelLogs = true;
+ ProtectKernelModules = true;
+ ProtectKernelTunables = true;
+ ProtectProc = "invisible";
+ ProcSubset = "pid";
+ ProtectSystem = "strict";
+ RemoveIPC = true;
+ RestrictAddressFamilies = [
+ "AF_INET"
+ "AF_INET6"
+ ];
+ RestrictNamespaces = true;
+ RestrictRealtime = true;
+ RestrictSUIDSGID = true;
+ SystemCallArchitectures = "native";
+ SystemCallFilter = [
+ "@system-service"
+ "~@privileged"
+ ];
+ UMask = "0077";
+ };
};
- };
- systemd.services.postfix = {
- after = [ rspamdSocket ];
- requires = [ rspamdSocket ];
- };
+ systemd.timers.rspamd-dmarc-reporter = lib.optionalAttrs cfg.dmarcReporting.enable {
+ description = "Daily delivery of aggregated DMARC reports";
+ wantedBy = [
+ "timers.target"
+ ];
+ timerConfig = {
+ OnCalendar = "daily";
+ Persistent = true;
+ RandomizedDelaySec = 86400;
+ FixedRandomDelay = true;
+ };
+ };
- users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
- };
+ systemd.services.postfix = {
+ after = [ rspamdSocket ];
+ requires = [ rspamdSocket ];
+ };
+
+ users.extraUsers.${postfixCfg.user}.extraGroups = [ rspamdCfg.group ];
+ };
}
-
diff --git a/mail-server/systemd.nix b/mail-server/systemd.nix
index c411441..dd4bd63 100644
--- a/mail-server/systemd.nix
+++ b/mail-server/systemd.nix
@@ -14,72 +14,79 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-{ 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 {
- # Create self signed certificate
- systemd.services.mailserver-selfsigned-certificate = lib.mkIf (cfg.certificateScheme == "selfsigned") {
- after = [ "local-fs.target" ];
- script = ''
- # Create certificates if they do not exist yet
- dir="${cfg.certificateDirectory}"
- fqdn="${cfg.fqdn}"
- [[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
- key="$dir/key-${cfg.fqdn}.pem";
- cert="$dir/cert-${cfg.fqdn}.pem";
+ config =
+ with cfg;
+ lib.mkIf enable {
+ # Create self signed certificate
+ systemd.services.mailserver-selfsigned-certificate =
+ lib.mkIf (cfg.certificateScheme == "selfsigned")
+ {
+ after = [ "local-fs.target" ];
+ script = ''
+ # Create certificates if they do not exist yet
+ dir="${cfg.certificateDirectory}"
+ fqdn="${cfg.fqdn}"
+ [[ $fqdn == /* ]] && fqdn=$(< "$fqdn")
+ key="$dir/key-${cfg.fqdn}.pem";
+ cert="$dir/cert-${cfg.fqdn}.pem";
- if [[ ! -f $key || ! -f $cert ]]; then
- mkdir -p "${cfg.certificateDirectory}"
- (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) &&
- "${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \
- -days 3650 -out "$cert"
- fi
- '';
- serviceConfig = {
- Type = "oneshot";
- PrivateTmp = true;
+ if [[ ! -f $key || ! -f $cert ]]; then
+ mkdir -p "${cfg.certificateDirectory}"
+ (umask 077; "${pkgs.openssl}/bin/openssl" genrsa -out "$key" 2048) &&
+ "${pkgs.openssl}/bin/openssl" req -new -key "$key" -x509 -subj "/CN=$fqdn" \
+ -days 3650 -out "$cert"
+ fi
+ '';
+ serviceConfig = {
+ Type = "oneshot";
+ PrivateTmp = true;
+ };
+ };
+
+ # Create maildir folder before dovecot startup
+ systemd.services.dovecot2 = {
+ wants = certificatesDeps;
+ after = certificatesDeps;
+ preStart =
+ let
+ directories = lib.strings.escapeShellArgs (
+ [ mailDirectory ] ++ lib.optional (cfg.indexDir != null) cfg.indexDir
+ );
+ in
+ ''
+ # Create mail directory and set permissions. See
+ # .
+ # Prevent world-readable paths, even temporarily.
+ umask 007
+ mkdir -p ${directories}
+ chgrp "${vmailGroupName}" ${directories}
+ chmod 02770 ${directories}
+ '';
+ };
+
+ # 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";
};
};
-
- # Create maildir folder before dovecot startup
- systemd.services.dovecot2 = {
- wants = certificatesDeps;
- after = certificatesDeps;
- preStart = let
- directories = lib.strings.escapeShellArgs (
- [ mailDirectory ]
- ++ lib.optional (cfg.indexDir != null) cfg.indexDir
- );
- in ''
- # Create mail directory and set permissions. See
- # .
- # Prevent world-readable paths, even temporarily.
- umask 007
- mkdir -p ${directories}
- chgrp "${vmailGroupName}" ${directories}
- chmod 02770 ${directories}
- '';
- };
-
- # 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";
- };
- };
}
diff --git a/mail-server/users.nix b/mail-server/users.nix
index 17196fc..e9af05a 100644
--- a/mail-server/users.nix
+++ b/mail-server/users.nix
@@ -14,7 +14,12 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
-{ 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,45 +50,54 @@ 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 ''
- if (! test -d "${sieveDirectory}/${name}"); then
- mkdir -p "${sieveDirectory}/${name}"
- chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}"
- chmod 770 "${sieveDirectory}/${name}"
- fi
- cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
- ${sieveScript}
- EOF
- chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
- '' 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))}
+ ${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}"
+ chmod 770 "${sieveDirectory}/${name}"
+ fi
+ cat << 'EOF' > "${sieveDirectory}/${name}/default.sieve"
+ ${sieveScript}
+ EOF
+ chown "${vmailUserName}:${vmailGroupName}" "${sieveDirectory}/${name}/default.sieve"
+ ''
+ 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))}
'';
-in {
+in
+{
config = lib.mkIf enable {
# assert that all accounts provide a password
- assertions = (map (acct: {
- assertion = (acct.hashedPassword != null || acct.hashedPasswordFile != null);
+ assertions = map (acct: {
+ assertion = acct.hashedPassword != null || acct.hashedPasswordFile != null;
message = "${acct.name} must provide either a hashed password or a password hash file";
- }) (lib.attrValues loginAccounts));
+ }) (lib.attrValues loginAccounts);
# warn for accounts that specify both password and file
- warnings = (map
- (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
- (lib.filter
- (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null))
- (lib.attrValues loginAccounts)));
+ warnings =
+ map (acct: "${acct.name} specifies both a password hash and hash file; hash file will be used")
+ (
+ lib.filter (acct: (acct.hashedPassword != null && acct.hashedPasswordFile != null)) (
+ lib.attrValues loginAccounts
+ )
+ );
# set the vmail gid to a specific value
users.groups = {
- "${vmailGroupName}" = { gid = vmailUID; };
+ "${vmailGroupName}" = {
+ gid = vmailUID;
+ };
};
# define all users
diff --git a/shell.nix b/shell.nix
index 6234bb4..493783d 100644
--- a/shell.nix
+++ b/shell.nix
@@ -1,10 +1,9 @@
-(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
+(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
diff --git a/tests/clamav.nix b/tests/clamav.nix
index 19b799f..209e91e 100644
--- a/tests/clamav.nix
+++ b/tests/clamav.nix
@@ -24,73 +24,79 @@
name = "clamav";
nodes = {
- server = { pkgs, ... }:
- {
- imports = [
- ../default.nix
- ./lib/config.nix
- ];
+ server =
+ { pkgs, ... }:
+ {
+ imports = [
+ ../default.nix
+ ./lib/config.nix
+ ];
- virtualisation.memorySize = 1500;
+ virtualisation.memorySize = 1500;
- environment.systemPackages = with pkgs; [ netcat ];
+ environment.systemPackages = with pkgs; [ netcat ];
- services.rsyslogd = {
- enable = true;
- defaultConfig = ''
- *.* /dev/console
- '';
- };
-
- services.clamav.updater.enable = lib.mkForce false;
- systemd.services.old-clam = {
- before = [ "clamav-daemon.service" ];
- requiredBy = [ "clamav-daemon.service" ];
- description = "ClamAV virus database";
-
- preStart = ''
- mkdir -m 0755 -p /var/lib/clamav
- chown clamav:clamav /var/lib/clamav
- '';
-
- script = ''
- cp ${blobs}/clamav/main.cvd /var/lib/clamav/
- cp ${blobs}/clamav/daily.cvd /var/lib/clamav/
- cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/
- chown clamav:clamav /var/lib/clamav/*
- '';
-
- serviceConfig = {
- Type = "oneshot";
- PrivateTmp = "yes";
- PrivateDevices = "yes";
- };
- };
-
- mailserver = {
- enable = true;
- fqdn = "mail.example.com";
- domains = [ "example.com" "example2.com" ];
- virusScanning = true;
-
- loginAccounts = {
- "user1@example.com" = {
- hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
- aliases = [ "postmaster@example.com" ];
- catchAll = [ "example.com" ];
- };
- "user@example2.com" = {
- hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
- };
- };
- enableImap = true;
- };
-
- environment.etc = {
- "root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
- };
+ services.rsyslogd = {
+ enable = true;
+ defaultConfig = ''
+ *.* /dev/console
+ '';
};
- client = { nodes, pkgs, ... }: let
+
+ services.clamav.updater.enable = lib.mkForce false;
+ systemd.services.old-clam = {
+ before = [ "clamav-daemon.service" ];
+ requiredBy = [ "clamav-daemon.service" ];
+ description = "ClamAV virus database";
+
+ preStart = ''
+ mkdir -m 0755 -p /var/lib/clamav
+ chown clamav:clamav /var/lib/clamav
+ '';
+
+ script = ''
+ cp ${blobs}/clamav/main.cvd /var/lib/clamav/
+ cp ${blobs}/clamav/daily.cvd /var/lib/clamav/
+ cp ${blobs}/clamav/bytecode.cvd /var/lib/clamav/
+ chown clamav:clamav /var/lib/clamav/*
+ '';
+
+ serviceConfig = {
+ Type = "oneshot";
+ PrivateTmp = "yes";
+ PrivateDevices = "yes";
+ };
+ };
+
+ mailserver = {
+ enable = true;
+ fqdn = "mail.example.com";
+ domains = [
+ "example.com"
+ "example2.com"
+ ];
+ virusScanning = true;
+
+ loginAccounts = {
+ "user1@example.com" = {
+ hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
+ aliases = [ "postmaster@example.com" ];
+ catchAll = [ "example.com" ];
+ };
+ "user@example2.com" = {
+ hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
+ };
+ };
+ enableImap = true;
+ };
+
+ environment.etc = {
+ "root/eicar.com.txt".text = "X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*";
+ };
+ };
+ client =
+ { nodes, pkgs, ... }:
+ let
serverIP = nodes.server.networking.primaryIPAddress;
clientIP = nodes.client.networking.primaryIPAddress;
grep-ip = pkgs.writeScriptBin "grep-ip" ''
@@ -98,20 +104,25 @@
echo grep '${clientIP}' "$@" >&2
exec grep '${clientIP}' "$@"
'';
- in {
+ in
+ {
imports = [
- ./lib/config.nix
+ ./lib/config.nix
];
environment.systemPackages = with pkgs; [
- fetchmail msmtp procmail findutils grep-ip
+ fetchmail
+ msmtp
+ procmail
+ findutils
+ grep-ip
];
environment.etc = {
"root/.fetchmailrc" = {
text = ''
- poll ${serverIP} with proto IMAP
- user 'user1@example.com' there with password 'user1' is 'root' here
- mda procmail
+ poll ${serverIP} with proto IMAP
+ user 'user1@example.com' there with password 'user1' is 'root' here
+ mda procmail
'';
mode = "0700";
};
@@ -185,59 +196,59 @@
'';
};
};
- };
+ };
testScript = ''
- start_all()
+ start_all()
- server.wait_for_unit("multi-user.target")
- client.wait_for_unit("multi-user.target")
+ server.wait_for_unit("multi-user.target")
+ client.wait_for_unit("multi-user.target")
- # TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
- server.wait_until_succeeds(
- "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
- )
- server.wait_until_succeeds(
- "set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
- )
+ # TODO put this blocking into the systemd units? I am not sure if rspamd already waits for the clamd socket.
+ server.wait_until_succeeds(
+ "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
+ )
+ server.wait_until_succeeds(
+ "set +e; timeout 1 nc -U /run/clamav/clamd.ctl < /dev/null; [ $? -eq 124 ]"
+ )
- client.execute("cp -p /etc/root/.* ~/")
- client.succeed("mkdir -p ~/mail")
- client.succeed("ls -la ~/ >&2")
- client.succeed("cat ~/.fetchmailrc >&2")
- client.succeed("cat ~/.procmailrc >&2")
- client.succeed("cat ~/.msmtprc >&2")
+ client.execute("cp -p /etc/root/.* ~/")
+ client.succeed("mkdir -p ~/mail")
+ client.succeed("ls -la ~/ >&2")
+ client.succeed("cat ~/.fetchmailrc >&2")
+ client.succeed("cat ~/.procmailrc >&2")
+ client.succeed("cat ~/.msmtprc >&2")
- # fetchmail returns EXIT_CODE 1 when no new mail
- client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
+ # fetchmail returns EXIT_CODE 1 when no new mail
+ client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
- # Verify that mail can be sent and received before testing virus scanner
- client.execute("rm ~/mail/*")
- client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2")
- # give the mail server some time to process the mail
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- client.execute("rm ~/mail/*")
- # fetchmail returns EXIT_CODE 0 when it retrieves mail
- client.succeed("fetchmail --nosslcertck -v >&2")
- client.execute("rm ~/mail/*")
+ # Verify that mail can be sent and received before testing virus scanner
+ client.execute("rm ~/mail/*")
+ client.succeed("msmtp -a user2 user1@example.com < /etc/root/safe-email >&2")
+ # give the mail server some time to process the mail
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ client.execute("rm ~/mail/*")
+ # fetchmail returns EXIT_CODE 0 when it retrieves mail
+ client.succeed("fetchmail --nosslcertck -v >&2")
+ client.execute("rm ~/mail/*")
- with subtest("virus scan file"):
- server.succeed(
- 'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2'
- )
+ with subtest("virus scan file"):
+ server.succeed(
+ 'set +o pipefail; clamdscan $(readlink -f /etc/root/eicar.com.txt) | grep "Txt\\.Malware\\.Agent-1787597 FOUND" >&2'
+ )
- with subtest("virus scan email"):
- client.succeed(
- 'set +o pipefail; msmtp -a user2 user1@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
- )
- server.succeed("journalctl -u rspamd | grep -i eicar")
- # give the mail server some time to process the mail
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ with subtest("virus scan email"):
+ client.succeed(
+ 'set +o pipefail; msmtp -a user2 user1@example.com < /etc/root/virus-email 2>&1 | tee /dev/stderr | grep "server message: 554 5\\.7\\.1" >&2'
+ )
+ server.succeed("journalctl -u rspamd | grep -i eicar")
+ # give the mail server some time to process the mail
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- with subtest("no warnings or errors"):
- server.fail("journalctl -u postfix | grep -i error >&2")
- server.fail("journalctl -u postfix | grep -i warning >&2")
- server.fail("journalctl -u dovecot2 | grep -i error >&2")
- server.fail("journalctl -u dovecot2 | grep -i warning >&2")
- '';
+ with subtest("no warnings or errors"):
+ server.fail("journalctl -u postfix | grep -i error >&2")
+ server.fail("journalctl -u postfix | grep -i warning >&2")
+ server.fail("journalctl -u dovecot2 | grep -i error >&2")
+ server.fail("journalctl -u dovecot2 | grep -i warning >&2")
+ '';
}
diff --git a/tests/external.nix b/tests/external.nix
index a65f0ce..82abb65 100644
--- a/tests/external.nix
+++ b/tests/external.nix
@@ -18,74 +18,84 @@
name = "external";
nodes = {
- server = { pkgs, ... }:
- {
- imports = [
- ../default.nix
- ./lib/config.nix
- ];
+ server =
+ { pkgs, ... }:
+ {
+ imports = [
+ ../default.nix
+ ./lib/config.nix
+ ];
- environment.systemPackages = with pkgs; [ netcat ];
+ environment.systemPackages = with pkgs; [ netcat ];
- virtualisation.memorySize = 1024;
+ virtualisation.memorySize = 1024;
- services.rsyslogd = {
- enable = true;
- defaultConfig = ''
- *.* /dev/console
- '';
- };
-
-
- mailserver = {
- enable = true;
- debug = true;
- fqdn = "mail.example.com";
- domains = [ "example.com" "example2.com" ];
- rewriteMessageId = true;
- dkimKeyBits = 1535;
- dmarcReporting = {
- enable = true;
- domain = "example.com";
- organizationName = "ACME Corp";
- };
-
- loginAccounts = {
- "user1@example.com" = {
- hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
- aliases = [ "postmaster@example.com" ];
- catchAll = [ "example.com" ];
- };
- "user2@example.com" = {
- hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
- aliases = [ "chuck@example.com" ];
- };
- "user@example2.com" = {
- hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
- };
- "lowquota@example.com" = {
- hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
- quota = "1B";
- };
- };
-
- extraVirtualAliases = {
- "single-alias@example.com" = "user1@example.com";
- "multi-alias@example.com" = [ "user1@example.com" "user2@example.com" ];
- };
-
- enableImap = true;
- enableImapSsl = true;
- fullTextSearch = {
- 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") ];
- enforced = "yes";
- };
- };
+ services.rsyslogd = {
+ enable = true;
+ defaultConfig = ''
+ *.* /dev/console
+ '';
};
- client = { nodes, pkgs, ... }: let
+
+ mailserver = {
+ enable = true;
+ debug = true;
+ fqdn = "mail.example.com";
+ domains = [
+ "example.com"
+ "example2.com"
+ ];
+ rewriteMessageId = true;
+ dkimKeyBits = 1535;
+ dmarcReporting = {
+ enable = true;
+ domain = "example.com";
+ organizationName = "ACME Corp";
+ };
+
+ loginAccounts = {
+ "user1@example.com" = {
+ hashedPassword = "$6$/z4n8AQl6K$kiOkBTWlZfBd7PvF5GsJ8PmPgdZsFGN1jPGZufxxr60PoR0oUsrvzm2oQiflyz5ir9fFJ.d/zKm/NgLXNUsNX/";
+ aliases = [ "postmaster@example.com" ];
+ catchAll = [ "example.com" ];
+ };
+ "user2@example.com" = {
+ hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
+ aliases = [ "chuck@example.com" ];
+ };
+ "user@example2.com" = {
+ hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
+ };
+ "lowquota@example.com" = {
+ hashedPassword = "$6$u61JrAtuI0a$nGEEfTP5.eefxoScUGVG/Tl0alqla2aGax4oTd85v3j3xSmhv/02gNfSemv/aaMinlv9j/ZABosVKBrRvN5Qv0";
+ quota = "1B";
+ };
+ };
+
+ extraVirtualAliases = {
+ "single-alias@example.com" = "user1@example.com";
+ "multi-alias@example.com" = [
+ "user1@example.com"
+ "user2@example.com"
+ ];
+ };
+
+ enableImap = true;
+ enableImapSsl = true;
+ fullTextSearch = {
+ 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")
+ ];
+ enforced = "yes";
+ };
+ };
+ };
+ client =
+ { nodes, pkgs, ... }:
+ let
serverIP = nodes.server.networking.primaryIPAddress;
clientIP = nodes.client.networking.primaryIPAddress;
grep-ip = pkgs.writeScriptBin "grep-ip" ''
@@ -172,27 +182,36 @@
assert needle in repr(response)
imap.close()
'';
- in {
+ in
+ {
imports = [
- ./lib/config.nix
+ ./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" = {
text = ''
- poll ${serverIP} with proto IMAP
- user 'user1@example.com' there with password 'user1' is 'root' here
- mda procmail
+ poll ${serverIP} with proto IMAP
+ user 'user1@example.com' there with password 'user1' is 'root' here
+ mda procmail
'';
mode = "0700";
};
"root/.fetchmailRcLowQuota" = {
text = ''
- poll ${serverIP} with proto IMAP
- user 'lowquota@example.com' there with password 'user2' is 'root' here
- mda procmail
+ poll ${serverIP} with proto IMAP
+ user 'lowquota@example.com' there with password 'user2' is 'root' here
+ mda procmail
'';
mode = "0700";
};
@@ -338,176 +357,176 @@
'';
};
};
- };
+ };
testScript = ''
- start_all()
+ start_all()
- server.wait_for_unit("multi-user.target")
- client.wait_for_unit("multi-user.target")
+ server.wait_for_unit("multi-user.target")
+ client.wait_for_unit("multi-user.target")
- # TODO put this blocking into the systemd units?
- server.wait_until_succeeds(
- "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
- )
+ # TODO put this blocking into the systemd units?
+ server.wait_until_succeeds(
+ "set +e; timeout 1 nc -U /run/rspamd/rspamd-milter.sock < /dev/null; [ $? -eq 124 ]"
+ )
- client.execute("cp -p /etc/root/.* ~/")
- client.succeed("mkdir -p ~/mail")
- client.succeed("ls -la ~/ >&2")
- client.succeed("cat ~/.fetchmailrc >&2")
- client.succeed("cat ~/.procmailrc >&2")
- client.succeed("cat ~/.msmtprc >&2")
+ client.execute("cp -p /etc/root/.* ~/")
+ client.succeed("mkdir -p ~/mail")
+ client.succeed("ls -la ~/ >&2")
+ client.succeed("cat ~/.fetchmailrc >&2")
+ client.succeed("cat ~/.procmailrc >&2")
+ client.succeed("cat ~/.msmtprc >&2")
- with subtest("imap retrieving mail"):
- # fetchmail returns EXIT_CODE 1 when no new mail
- client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
+ with subtest("imap retrieving mail"):
+ # fetchmail returns EXIT_CODE 1 when no new mail
+ client.succeed("fetchmail --nosslcertck -v || [ $? -eq 1 ] >&2")
- with subtest("submission port send mail"):
- # send email from user2 to user1
- client.succeed(
- "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2"
- )
- # give the mail server some time to process the mail
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ with subtest("submission port send mail"):
+ # send email from user2 to user1
+ client.succeed(
+ "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2"
+ )
+ # give the mail server some time to process the mail
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- with subtest("imap retrieving mail 2"):
- client.execute("rm ~/mail/*")
- # fetchmail returns EXIT_CODE 0 when it retrieves mail
- client.succeed("fetchmail --nosslcertck -v >&2")
+ with subtest("imap retrieving mail 2"):
+ client.execute("rm ~/mail/*")
+ # fetchmail returns EXIT_CODE 0 when it retrieves mail
+ client.succeed("fetchmail --nosslcertck -v >&2")
- with subtest("remove sensitive information on submission port"):
- client.succeed("cat ~/mail/* >&2")
- ## make sure our IP is _not_ in the email header
- client.fail("grep-ip ~/mail/*")
- client.succeed("check-mail-id ~/mail/*")
+ with subtest("remove sensitive information on submission port"):
+ client.succeed("cat ~/mail/* >&2")
+ ## make sure our IP is _not_ in the email header
+ client.fail("grep-ip ~/mail/*")
+ client.succeed("check-mail-id ~/mail/*")
- with subtest("have correct fqdn as sender"):
- client.succeed("grep 'Received: from mail.example.com' ~/mail/*")
+ with subtest("have correct fqdn as sender"):
+ client.succeed("grep 'Received: from mail.example.com' ~/mail/*")
- with subtest("dkim has user-specified size"):
- server.succeed(
- "openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'"
- )
+ with subtest("dkim has user-specified size"):
+ server.succeed(
+ "openssl rsa -in /var/dkim/example.com.mail.key -text -noout | grep 'Private-Key: (1535 bit'"
+ )
- with subtest("dkim singing, multiple domains"):
- client.execute("rm ~/mail/*")
- # send email from user2 to user1
- client.succeed(
- "msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email2 >&2"
- )
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- # fetchmail returns EXIT_CODE 0 when it retrieves mail
- client.succeed("fetchmail --nosslcertck -v")
- client.succeed("cat ~/mail/* >&2")
- # make sure it is dkim signed
- client.succeed("grep DKIM-Signature: ~/mail/*")
+ with subtest("dkim singing, multiple domains"):
+ client.execute("rm ~/mail/*")
+ # send email from user2 to user1
+ client.succeed(
+ "msmtp -a test2 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email2 >&2"
+ )
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ # fetchmail returns EXIT_CODE 0 when it retrieves mail
+ client.succeed("fetchmail --nosslcertck -v")
+ client.succeed("cat ~/mail/* >&2")
+ # make sure it is dkim signed
+ client.succeed("grep DKIM-Signature: ~/mail/*")
- with subtest("aliases"):
- client.execute("rm ~/mail/*")
- # send email from chuck to postmaster
- client.succeed(
- "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster@example.com < /etc/root/email2 >&2"
- )
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- # fetchmail returns EXIT_CODE 0 when it retrieves mail
- client.succeed("fetchmail --nosslcertck -v")
+ with subtest("aliases"):
+ client.execute("rm ~/mail/*")
+ # send email from chuck to postmaster
+ client.succeed(
+ "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on postmaster@example.com < /etc/root/email2 >&2"
+ )
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ # fetchmail returns EXIT_CODE 0 when it retrieves mail
+ client.succeed("fetchmail --nosslcertck -v")
- with subtest("catchAlls"):
- client.execute("rm ~/mail/*")
- # send email from chuck to non exsitent account
- client.succeed(
- "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol@example.com < /etc/root/email2 >&2"
- )
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- # fetchmail returns EXIT_CODE 0 when it retrieves mail
- client.succeed("fetchmail --nosslcertck -v")
+ with subtest("catchAlls"):
+ client.execute("rm ~/mail/*")
+ # send email from chuck to non exsitent account
+ client.succeed(
+ "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lol@example.com < /etc/root/email2 >&2"
+ )
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ # fetchmail returns EXIT_CODE 0 when it retrieves mail
+ client.succeed("fetchmail --nosslcertck -v")
- client.execute("rm ~/mail/*")
- # send email from user1 to chuck
- client.succeed(
- "msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck@example.com < /etc/root/email2 >&2"
- )
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- # fetchmail returns EXIT_CODE 1 when no new mail
- # if this succeeds, it means that user1 recieved the mail that was intended for chuck.
- client.fail("fetchmail --nosslcertck -v")
+ client.execute("rm ~/mail/*")
+ # send email from user1 to chuck
+ client.succeed(
+ "msmtp -a test4 --tls=on --tls-certcheck=off --auth=on chuck@example.com < /etc/root/email2 >&2"
+ )
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ # fetchmail returns EXIT_CODE 1 when no new mail
+ # if this succeeds, it means that user1 recieved the mail that was intended for chuck.
+ client.fail("fetchmail --nosslcertck -v")
- with subtest("extraVirtualAliases"):
- client.execute("rm ~/mail/*")
- # send email from single-alias to user1
- client.succeed(
- "msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email4 >&2"
- )
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- # fetchmail returns EXIT_CODE 0 when it retrieves mail
- client.succeed("fetchmail --nosslcertck -v")
+ with subtest("extraVirtualAliases"):
+ client.execute("rm ~/mail/*")
+ # send email from single-alias to user1
+ client.succeed(
+ "msmtp -a test5 --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email4 >&2"
+ )
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ # fetchmail returns EXIT_CODE 0 when it retrieves mail
+ client.succeed("fetchmail --nosslcertck -v")
- client.execute("rm ~/mail/*")
- # send email from user1 to multi-alias (user{1,2}@example.com)
- client.succeed(
- "msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias@example.com < /etc/root/email5 >&2"
- )
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- # fetchmail returns EXIT_CODE 0 when it retrieves mail
- client.succeed("fetchmail --nosslcertck -v")
+ client.execute("rm ~/mail/*")
+ # send email from user1 to multi-alias (user{1,2}@example.com)
+ client.succeed(
+ "msmtp -a test --tls=on --tls-certcheck=off --auth=on multi-alias@example.com < /etc/root/email5 >&2"
+ )
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ # fetchmail returns EXIT_CODE 0 when it retrieves mail
+ client.succeed("fetchmail --nosslcertck -v")
- with subtest("quota"):
- client.execute("rm ~/mail/*")
- client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
+ with subtest("quota"):
+ client.execute("rm ~/mail/*")
+ client.execute("mv ~/.fetchmailRcLowQuota ~/.fetchmailrc")
- client.succeed(
- "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota@example.com < /etc/root/email2 >&2"
- )
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- # fetchmail returns EXIT_CODE 0 when it retrieves mail
- client.fail("fetchmail --nosslcertck -v")
+ client.succeed(
+ "msmtp -a test3 --tls=on --tls-certcheck=off --auth=on lowquota@example.com < /etc/root/email2 >&2"
+ )
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ # fetchmail returns EXIT_CODE 0 when it retrieves mail
+ client.fail("fetchmail --nosslcertck -v")
- with subtest("imap sieve junk trainer"):
- # send email from user2 to user1
- client.succeed(
- "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2"
- )
- # give the mail server some time to process the mail
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ with subtest("imap sieve junk trainer"):
+ # send email from user2 to user1
+ client.succeed(
+ "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email1 >&2"
+ )
+ # give the mail server some time to process the mail
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- client.succeed("imap-mark-spam >&2")
- server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-spam.sh >&2")
- client.succeed("imap-mark-ham >&2")
- server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-ham.sh >&2")
+ client.succeed("imap-mark-spam >&2")
+ server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-spam.sh >&2")
+ client.succeed("imap-mark-ham >&2")
+ server.wait_until_succeeds("journalctl -u dovecot2 | grep -i rspamd-learn-ham.sh >&2")
- with subtest("full text search and indexation"):
- # send 2 email from user2 to user1
- client.succeed(
- "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email6 >&2"
- )
- client.succeed(
- "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email7 >&2"
- )
- # give the mail server some time to process the mail
- server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
+ with subtest("full text search and indexation"):
+ # send 2 email from user2 to user1
+ client.succeed(
+ "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email6 >&2"
+ )
+ client.succeed(
+ "msmtp -a test --tls=on --tls-certcheck=off --auth=on user1@example.com < /etc/root/email7 >&2"
+ )
+ # give the mail server some time to process the mail
+ server.wait_until_fails('[ "$(postqueue -p)" != "Mail queue is empty" ]')
- # should find exactly one email containing this
- client.succeed("search INBOX 576a4565b70f5a4c1a0925cabdb587a6 >&2")
- # should fail because this folder is not indexed
- client.fail("search Junk a >&2")
- # check that search really goes through the indexer
- server.succeed("journalctl -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2")
- # check that Junk is not indexed
- server.fail("journalctl -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2")
+ # should find exactly one email containing this
+ client.succeed("search INBOX 576a4565b70f5a4c1a0925cabdb587a6 >&2")
+ # should fail because this folder is not indexed
+ client.fail("search Junk a >&2")
+ # check that search really goes through the indexer
+ server.succeed("journalctl -u dovecot2 | grep 'fts-flatcurve(INBOX): Query ' >&2")
+ # check that Junk is not indexed
+ server.fail("journalctl -u dovecot2 | grep 'fts-flatcurve(JUNK): Indexing ' >&2")
- with subtest("dmarc reporting"):
- server.systemctl("start rspamd-dmarc-reporter.service")
+ with subtest("dmarc reporting"):
+ server.systemctl("start rspamd-dmarc-reporter.service")
- with subtest("no warnings or errors"):
- server.fail("journalctl -u postfix | grep -i error >&2")
- server.fail("journalctl -u postfix | grep -i warning >&2")
- server.fail("journalctl -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2")
- # harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html
- server.fail(
- "journalctl -u dovecot2 | \
- grep -v 'Expunged message reappeared, giving a new UID' | \
- grep -v 'Time moved forwards' | \
- grep -i warning >&2"
- )
- '';
+ with subtest("no warnings or errors"):
+ server.fail("journalctl -u postfix | grep -i error >&2")
+ server.fail("journalctl -u postfix | grep -i warning >&2")
+ server.fail("journalctl -u dovecot2 | grep -v 'imap-login: Debug: SSL error: Connection closed' | grep -i error >&2")
+ # harmless ? https://dovecot.org/pipermail/dovecot/2020-August/119575.html
+ server.fail(
+ "journalctl -u dovecot2 | \
+ grep -v 'Expunged message reappeared, giving a new UID' | \
+ grep -v 'Time moved forwards' | \
+ grep -i warning >&2"
+ )
+ '';
}
diff --git a/tests/internal.nix b/tests/internal.nix
index 8f47e70..af552c3 100644
--- a/tests/internal.nix
+++ b/tests/internal.nix
@@ -30,11 +30,16 @@ let
'';
};
- hashPassword = password: pkgs.runCommand
- "password-${password}-hashed"
- { buildInputs = [ pkgs.mkpasswd ]; inherit password; } ''
- mkpasswd -sm bcrypt <<<"$password" > $out
- '';
+ hashPassword =
+ password:
+ pkgs.runCommand "password-${password}-hashed"
+ {
+ buildInputs = [ pkgs.mkpasswd ];
+ inherit password;
+ }
+ ''
+ mkpasswd -sm bcrypt <<<"$password" > $out
+ '';
hashedPasswordFile = hashPassword "my-password";
passwordFile = pkgs.writeText "password" "my-password";
@@ -43,55 +48,62 @@ in
name = "internal";
nodes = {
- machine = { pkgs, ... }: {
- imports = [
- ./../default.nix
- ./lib/config.nix
- ];
+ machine =
+ { pkgs, ... }:
+ {
+ imports = [
+ ./../default.nix
+ ./lib/config.nix
+ ];
- virtualisation.memorySize = 1024;
+ virtualisation.memorySize = 1024;
- environment.systemPackages = [
- (pkgs.writeScriptBin "mail-check" ''
- ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
- '')
- ] ++ (with pkgs; [
- curl
- openssl
- netcat
- ]);
+ environment.systemPackages =
+ [
+ (pkgs.writeScriptBin "mail-check" ''
+ ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
+ '')
+ ]
+ ++ (with pkgs; [
+ curl
+ openssl
+ netcat
+ ]);
- mailserver = {
- enable = true;
- fqdn = "mail.example.com";
- domains = [ "example.com" "domain.com" ];
- localDnsResolver = false;
+ mailserver = {
+ enable = true;
+ fqdn = "mail.example.com";
+ domains = [
+ "example.com"
+ "domain.com"
+ ];
+ localDnsResolver = false;
- loginAccounts = {
- "user1@example.com" = {
- hashedPasswordFile = hashedPasswordFile;
+ loginAccounts = {
+ "user1@example.com" = {
+ hashedPasswordFile = hashedPasswordFile;
+ };
+ "user2@example.com" = {
+ hashedPasswordFile = hashedPasswordFile;
+ aliasesRegexp = [ ''/^user2.*@domain\.com$/'' ];
+ };
+ "send-only@example.com" = {
+ hashedPasswordFile = hashPassword "send-only";
+ sendOnly = true;
+ };
};
- "user2@example.com" = {
- hashedPasswordFile = hashedPasswordFile;
- aliasesRegexp = [''/^user2.*@domain\.com$/''];
- };
- "send-only@example.com" = {
- hashedPasswordFile = hashPassword "send-only";
- sendOnly = true;
+ forwards = {
+ # user2@example.com is a local account and its mails are
+ # also forwarded to user1@example.com
+ "user2@example.com" = "user1@example.com";
};
+
+ vmailGroupName = "vmail";
+ vmailUID = 5000;
+
+ enableImap = false;
};
- forwards = {
- # user2@example.com is a local account and its mails are
- # also forwarded to user1@example.com
- "user2@example.com" = "user1@example.com";
- };
-
- vmailGroupName = "vmail";
- vmailUID = 5000;
-
- enableImap = false;
};
- };
};
testScript = ''
machine.start()
diff --git a/tests/ldap.nix b/tests/ldap.nix
index 8187d7d..1c92572 100644
--- a/tests/ldap.nix
+++ b/tests/ldap.nix
@@ -7,110 +7,113 @@ in
name = "ldap";
nodes = {
- machine = { pkgs, ... }: {
- imports = [
- ./../default.nix
- ./lib/config.nix
- ];
+ machine =
+ { pkgs, ... }:
+ {
+ imports = [
+ ./../default.nix
+ ./lib/config.nix
+ ];
- virtualisation.memorySize = 1024;
+ virtualisation.memorySize = 1024;
- services.openssh = {
- enable = true;
- settings.PermitRootLogin = "yes";
- };
+ services.openssh = {
+ enable = true;
+ settings.PermitRootLogin = "yes";
+ };
- environment.systemPackages = [
- (pkgs.writeScriptBin "mail-check" ''
- ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
- '')];
+ environment.systemPackages = [
+ (pkgs.writeScriptBin "mail-check" ''
+ ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
+ '')
+ ];
- environment.etc.bind-password.text = bindPassword;
+ environment.etc.bind-password.text = bindPassword;
- services.openldap = {
- enable = true;
- settings = {
- children = {
- "cn=schema".includes = [
- "${pkgs.openldap}/etc/schema/core.ldif"
- "${pkgs.openldap}/etc/schema/cosine.ldif"
- "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
- "${pkgs.openldap}/etc/schema/nis.ldif"
- ];
- "olcDatabase={1}mdb" = {
- attrs = {
- objectClass = [
- "olcDatabaseConfig"
- "olcMdbConfig"
- ];
- olcDatabase = "{1}mdb";
- olcDbDirectory = "/var/lib/openldap/example";
- olcSuffix = "dc=example";
+ services.openldap = {
+ enable = true;
+ settings = {
+ children = {
+ "cn=schema".includes = [
+ "${pkgs.openldap}/etc/schema/core.ldif"
+ "${pkgs.openldap}/etc/schema/cosine.ldif"
+ "${pkgs.openldap}/etc/schema/inetorgperson.ldif"
+ "${pkgs.openldap}/etc/schema/nis.ldif"
+ ];
+ "olcDatabase={1}mdb" = {
+ attrs = {
+ objectClass = [
+ "olcDatabaseConfig"
+ "olcMdbConfig"
+ ];
+ olcDatabase = "{1}mdb";
+ olcDbDirectory = "/var/lib/openldap/example";
+ olcSuffix = "dc=example";
+ };
};
};
};
+ declarativeContents."dc=example" = ''
+ dn: dc=example
+ objectClass: domain
+ dc: example
+
+ dn: cn=mail,dc=example
+ objectClass: organizationalRole
+ objectClass: simpleSecurityObject
+ objectClass: top
+ cn: mail
+ userPassword: ${bindPassword}
+
+ dn: ou=users,dc=example
+ objectClass: organizationalUnit
+ ou: users
+
+ dn: cn=alice,ou=users,dc=example
+ objectClass: inetOrgPerson
+ cn: alice
+ sn: Foo
+ mail: alice@example.com
+ userPassword: ${alicePassword}
+
+ dn: cn=bob,ou=users,dc=example
+ objectClass: inetOrgPerson
+ cn: bob
+ sn: Bar
+ mail: bob@example.com
+ userPassword: ${bobPassword}
+ '';
};
- declarativeContents."dc=example" = ''
- dn: dc=example
- objectClass: domain
- dc: example
- dn: cn=mail,dc=example
- objectClass: organizationalRole
- objectClass: simpleSecurityObject
- objectClass: top
- cn: mail
- userPassword: ${bindPassword}
-
- dn: ou=users,dc=example
- objectClass: organizationalUnit
- ou: users
-
- dn: cn=alice,ou=users,dc=example
- objectClass: inetOrgPerson
- cn: alice
- sn: Foo
- mail: alice@example.com
- userPassword: ${alicePassword}
-
- dn: cn=bob,ou=users,dc=example
- objectClass: inetOrgPerson
- cn: bob
- sn: Bar
- mail: bob@example.com
- userPassword: ${bobPassword}
- '';
- };
-
- mailserver = {
- enable = true;
- fqdn = "mail.example.com";
- domains = [ "example.com" ];
- localDnsResolver = false;
-
- ldap = {
+ mailserver = {
enable = true;
- uris = [
- "ldap://"
- ];
- bind = {
- dn = "cn=mail,dc=example";
- passwordFile = "/etc/bind-password";
+ fqdn = "mail.example.com";
+ domains = [ "example.com" ];
+ localDnsResolver = false;
+
+ ldap = {
+ enable = true;
+ uris = [
+ "ldap://"
+ ];
+ bind = {
+ dn = "cn=mail,dc=example";
+ passwordFile = "/etc/bind-password";
+ };
+ searchBase = "ou=users,dc=example";
+ searchScope = "sub";
};
- searchBase = "ou=users,dc=example";
- searchScope = "sub";
+
+ forwards = {
+ "bob_fw@example.com" = "bob@example.com";
+ };
+
+ vmailGroupName = "vmail";
+ vmailUID = 5000;
+
+ enableImap = false;
};
-
- forwards = {
- "bob_fw@example.com" = "bob@example.com";
- };
-
- vmailGroupName = "vmail";
- vmailUID = 5000;
-
- enableImap = false;
};
- };
};
testScript = ''
import sys
diff --git a/tests/multiple.nix b/tests/multiple.nix
index 3e71cd6..2c6d0fc 100644
--- a/tests/multiple.nix
+++ b/tests/multiple.nix
@@ -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";
+ 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,23 +57,34 @@ in
name = "multiple";
nodes = {
- 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"];
+ 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"
+ ];
+ };
};
- };
domain2 = domainGenerator "domain2.com";
- client = { pkgs, ... }: {
- environment.systemPackages = [
- (pkgs.writeScriptBin "mail-check" ''
- ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
- '')];
- };
+ client =
+ { pkgs, ... }:
+ {
+ environment.systemPackages = [
+ (pkgs.writeScriptBin "mail-check" ''
+ ${pkgs.python3}/bin/python ${../scripts/mail-check.py} $@
+ '')
+ ];
+ };
};
testScript = ''
start_all()