mail: Use structured sieve configuration

This commit is contained in:
Charlotte Van Petegem 2024-01-29 10:17:20 +01:00
parent 1cd090c936
commit b0c95f0d40
No known key found for this signature in database
GPG key ID: 019E764B7184435A
4 changed files with 295 additions and 25 deletions

12
flake.lock generated
View file

@ -460,16 +460,16 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1701529057, "lastModified": 1705361146,
"narHash": "sha256-/R+ADRDHR3M1n2GSOjC7LjwBzijA1JGUt1kl15pUnNU=", "narHash": "sha256-oN6fJdswGueekl3VYLyP8GyZR76In8Au338JdNYX88Q=",
"owner": "robbevp", "owner": "dotlambda",
"repo": "nixos-mailserver", "repo": "nixos-mailserver",
"rev": "60132b990f4086f2ef004daa430941ed2b729e14", "rev": "c0c812117d67e346a94c3b546d04ab4820ed38d3",
"type": "gitlab" "type": "gitlab"
}, },
"original": { "original": {
"owner": "robbevp", "owner": "dotlambda",
"ref": "customize-sieve-extentions", "ref": "sieve-fix",
"repo": "nixos-mailserver", "repo": "nixos-mailserver",
"type": "gitlab" "type": "gitlab"
} }

View file

@ -63,7 +63,7 @@
}; };
}; };
nixos-mailserver = { nixos-mailserver = {
url = "gitlab:robbevp/nixos-mailserver/customize-sieve-extentions"; url = "gitlab:dotlambda/nixos-mailserver/sieve-fix";
inputs = { inputs = {
nixpkgs.follows = "nixpkgs"; nixpkgs.follows = "nixpkgs";
utils.follows = "flake-utils"; utils.follows = "flake-utils";

View file

@ -107,7 +107,6 @@ in
"sarina@vanpetegem.be" "sarina@vanpetegem.be"
]; ];
mailDirectory = "${config.chvp.dataPrefix}/var/vmail"; mailDirectory = "${config.chvp.dataPrefix}/var/vmail";
sieveExtensions = [ "+editheader" ];
useFsLayout = false; useFsLayout = false;
certificateScheme = "manual"; certificateScheme = "manual";
certificateFile = certFile; certificateFile = certFile;
@ -115,24 +114,27 @@ in
dkimKeyDirectory = "${config.chvp.dataPrefix}/var/dkim"; dkimKeyDirectory = "${config.chvp.dataPrefix}/var/dkim";
}; };
services.dovecot2.sieveScripts = { services.dovecot2 = {
after2 = pkgs.writeText "custom-spam.sieve" '' sieve = {
require ["fileinto", "regex"]; extensions = [ "+editheader" ];
scripts.after2 = pkgs.writeText "custom-spam.sieve" ''
require ["fileinto", "regex"];
if anyof(address :is "From" ["junjunggaming07@gmail.com", if anyof(address :is "From" ["junjunggaming07@gmail.com",
"censysnetbackup@gmail.com", "censysnetbackup@gmail.com",
"vitor.carvalheiro@escola.pr.gov.br", "vitor.carvalheiro@escola.pr.gov.br",
"spam@vuztc.ru", "spam@vuztc.ru",
# itsme spam # itsme spam
"noreply.mailing.itsme@online.be", "noreply.mailing.itsme@online.be",
"mailing.itsme@online.be"], "mailing.itsme@online.be"],
# Freshdesk is often used to sent spam from emails like `support@info5813.freshdesk.com` # Freshdesk is often used to sent spam from emails like `support@info5813.freshdesk.com`
address :regex "From" "[a-z\d]+@[a-z\d]+\.freshdesk\.com", address :regex "From" "[a-z\d]+@[a-z\d]+\.freshdesk\.com",
header :contains "From" ["jakubbielec", "Jakub Bielec"]) { header :contains "From" ["jakubbielec", "Jakub Bielec"]) {
fileinto "Junk"; fileinto "Junk";
stop; stop;
} }
''; '';
};
}; };
services.rspamd.extraConfig = '' services.rspamd.extraConfig = ''

268
patches/281001.patch Normal file
View file

@ -0,0 +1,268 @@
diff --git a/nixos/modules/services/mail/dovecot.nix b/nixos/modules/services/mail/dovecot.nix
index 25c7017a1d2589..8d298de6945bbc 100644
--- a/nixos/modules/services/mail/dovecot.nix
+++ b/nixos/modules/services/mail/dovecot.nix
@@ -4,7 +4,9 @@ let
inherit (lib) any attrValues concatMapStringsSep concatStrings
concatStringsSep flatten imap1 isList literalExpression mapAttrsToList
mkEnableOption mkIf mkOption mkRemovedOptionModule optional optionalAttrs
- optionalString singleton types;
+ optionalString singleton types mkRenamedOptionModule nameValuePair
+ mapAttrs' listToAttrs filter;
+ inherit (lib.strings) match;
cfg = config.services.dovecot2;
dovecotPkg = pkgs.dovecot;
@@ -12,6 +14,58 @@ let
baseDir = "/run/dovecot2";
stateDir = "/var/lib/dovecot";
+ sieveScriptSettings = mapAttrs' (to: from: nameValuePair "sieve_${to}" "${stateDir}/sieve/${from}") cfg.sieve.scripts;
+ imapSieveMailboxSettings = listToAttrs (flatten (imap1 (idx: el:
+ singleton {
+ name = "imapsieve_mailbox${toString idx}_name";
+ value = el.name;
+ } ++ optional (el.from != null) {
+ name = "imapsieve_mailbox${toString idx}_from";
+ value = el.from;
+ } ++ optional (el.causes != []) {
+ name = "imapsieve_mailbox${toString idx}_causes";
+ value = concatStringsSep "," el.causes;
+ } ++ optional (el.before != null) {
+ name = "imapsieve_mailbox${toString idx}_before";
+ value = "file:${stateDir}/imapsieve/before/${baseNameOf el.before}";
+ } ++ optional (el.after != null) {
+ name = "imapsieve_mailbox${toString idx}_after";
+ value = "file:${stateDir}/imapsieve/after/${baseNameOf el.after}";
+ }
+ ) cfg.imapsieve.mailbox));
+
+ mkExtraConfigCollisionWarning = term: ''
+ You referred to ${term} in `services.dovecot2.extraConfig`.
+
+ Due to gradual transition to structured configuration for plugin configuration, it is possible
+ this will cause your plugin configuration to be ignored.
+
+ Consider setting `services.dovecot2.pluginSettings.${term}` instead.
+ '';
+
+ # Those settings are automatically set based on other parts
+ # of this module.
+ automaticallySetPluginSettings = [
+ "sieve_plugins"
+ "sieve_extensions"
+ "sieve_global_extensions"
+ "sieve_pipe_bin_dir"
+ ]
+ ++ (builtins.attrNames sieveScriptSettings)
+ ++ (builtins.attrNames imapSieveMailboxSettings);
+
+ # The idea is to match everything that looks like `$term =`
+ # but not `# $term something something`
+ # or `# $term = some value` because those are comments.
+ configContainsSetting = lines: term: (match "^[^#]*\b${term}\b.*=" lines) != null;
+
+ warnAboutExtraConfigCollisions = map mkExtraConfigCollisionWarning (filter (configContainsSetting cfg.extraConfig) automaticallySetPluginSettings);
+
+ sievePipeBinScriptDirectory = pkgs.linkFarm "sieve-pipe-bins" (map (el: {
+ name = builtins.unsafeDiscardStringContext (baseNameOf el);
+ path = el;
+ }) cfg.sieve.pipeBins);
+
dovecotConf = concatStrings [
''
base_dir = ${baseDir}
@@ -77,14 +131,6 @@ let
''
)
- (
- optionalString (cfg.sieveScripts != {}) ''
- plugin {
- ${concatStringsSep "\n" (mapAttrsToList (to: from: "sieve_${to} = ${stateDir}/sieve/${to}") cfg.sieveScripts)}
- }
- ''
- )
-
(
optionalString (cfg.mailboxes != {}) ''
namespace inbox {
@@ -116,33 +162,12 @@ let
''
)
+ # General plugin settings:
+ # - sieve is mostly generated here, refer to `pluginSettings` to follow
+ # the control flow.
''
plugin {
- sieve_plugins = ${concatStringsSep " " cfg.sieve.plugins}
- sieve_extensions = ${concatStringsSep " " (map (el: "+${el}") cfg.sieve.extensions)}
- sieve_global_extensions = ${concatStringsSep " " (map (el: "+${el}") cfg.sieve.globalExtensions)}
- ''
- (optionalString (cfg.imapsieve.mailbox != []) ''
- ${
- concatStringsSep "\n" (flatten (imap1 (
- idx: el:
- singleton "imapsieve_mailbox${toString idx}_name = ${el.name}"
- ++ optional (el.from != null) "imapsieve_mailbox${toString idx}_from = ${el.from}"
- ++ optional (el.causes != null) "imapsieve_mailbox${toString idx}_causes = ${el.causes}"
- ++ optional (el.before != null) "imapsieve_mailbox${toString idx}_before = file:${stateDir}/imapsieve/before/${baseNameOf el.before}"
- ++ optional (el.after != null) "imapsieve_mailbox${toString idx}_after = file:${stateDir}/imapsieve/after/${baseNameOf el.after}"
- )
- cfg.imapsieve.mailbox))
- }
- '')
- (optionalString (cfg.sieve.pipeBins != []) ''
- sieve_pipe_bin_dir = ${pkgs.linkFarm "sieve-pipe-bins" (map (el: {
- name = builtins.unsafeDiscardStringContext (baseNameOf el);
- path = el;
- })
- cfg.sieve.pipeBins)}
- '')
- ''
+ ${concatStringsSep "\n" (mapAttrsToList (key: value: " ${key} = ${value}") cfg.pluginSettings)}
}
''
@@ -199,6 +224,7 @@ in
{
imports = [
(mkRemovedOptionModule [ "services" "dovecot2" "package" ] "")
+ (mkRenamedOptionModule [ "services" "dovecot2" "sieveScripts" ] [ "services" "dovecot2" "sieve" "scripts" ])
];
options.services.dovecot2 = {
@@ -337,12 +363,6 @@ in
enableDHE = mkEnableOption (lib.mdDoc "ssl_dh and generation of primes for the key exchange") // { default = true; };
- sieveScripts = mkOption {
- type = types.attrsOf types.path;
- default = {};
- description = lib.mdDoc "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
- };
-
showPAMFailure = mkEnableOption (lib.mdDoc "showing the PAM failure message on authentication error (useful for OTPW)");
mailboxes = mkOption {
@@ -376,6 +396,26 @@ in
description = lib.mdDoc "Quota limit for the user in bytes. Supports suffixes b, k, M, G, T and %.";
};
+
+ pluginSettings = mkOption {
+ # types.str does not coerce from packages, like `sievePipeBinScriptDirectory`.
+ type = types.attrsOf (types.oneOf [ types.str types.package ]);
+ default = {};
+ example = literalExpression ''
+ {
+ sieve = "file:~/sieve;active=~/.dovecot.sieve";
+ }
+ '';
+ description = ''
+ Plugin settings for dovecot in general, e.g. `sieve`, `sieve_default`, etc.
+
+ Some of the other knobs of this module will influence by default the plugin settings, but you
+ can still override any plugin settings.
+
+ If you override a plugin setting, its value is cleared and you have to copy over the defaults.
+ '';
+ };
+
imapsieve.mailbox = mkOption {
default = [];
description = "Configure Sieve filtering rules on IMAP actions";
@@ -405,14 +445,14 @@ in
};
causes = mkOption {
- default = null;
+ default = [ ];
description = ''
Only execute the administrator Sieve scripts for the mailbox configured with services.dovecot2.imapsieve.mailbox.<name>.name when one of the listed IMAPSIEVE causes apply.
This has no effect on the user script, which is always executed no matter the cause.
'';
- example = "COPY";
- type = types.nullOr (types.enum [ "APPEND" "COPY" "FLAG" ]);
+ example = [ "COPY" "APPEND" ];
+ type = types.listOf (types.enum [ "APPEND" "COPY" "FLAG" ]);
};
before = mkOption {
@@ -462,6 +502,12 @@ in
type = types.listOf types.str;
};
+ scripts = mkOption {
+ type = types.attrsOf types.path;
+ default = {};
+ description = lib.mdDoc "Sieve scripts to be executed. Key is a sequence, e.g. 'before2', 'after' etc.";
+ };
+
pipeBins = mkOption {
default = [];
example = literalExpression ''
@@ -476,7 +522,6 @@ in
};
};
-
config = mkIf cfg.enable {
security.pam.services.dovecot2 = mkIf cfg.enablePAM {};
@@ -501,6 +546,13 @@ in
++ optional (cfg.sieve.pipeBins != []) "sieve_extprograms";
sieve.globalExtensions = optional (cfg.sieve.pipeBins != []) "vnd.dovecot.pipe";
+
+ pluginSettings = lib.mapAttrs (n: lib.mkDefault) ({
+ sieve_plugins = concatStringsSep " " cfg.sieve.plugins;
+ sieve_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.extensions);
+ sieve_global_extensions = concatStringsSep " " (map (el: "+${el}") cfg.sieve.globalExtensions);
+ sieve_pipe_bin_dir = sievePipeBinScriptDirectory;
+ } // sieveScriptSettings // imapSieveMailboxSettings);
};
users.users = {
@@ -556,7 +608,7 @@ in
# the source file and Dovecot won't try to compile it.
preStart = ''
rm -rf ${stateDir}/sieve ${stateDir}/imapsieve
- '' + optionalString (cfg.sieveScripts != {}) ''
+ '' + optionalString (cfg.sieve.scripts != {}) ''
mkdir -p ${stateDir}/sieve
${concatStringsSep "\n" (
mapAttrsToList (
@@ -569,7 +621,7 @@ in
fi
${pkgs.dovecot_pigeonhole}/bin/sievec '${stateDir}/sieve/${to}'
''
- ) cfg.sieveScripts
+ ) cfg.sieve.scripts
)}
chown -R '${cfg.mailUser}:${cfg.mailGroup}' '${stateDir}/sieve'
''
@@ -600,9 +652,7 @@ in
environment.systemPackages = [ dovecotPkg ];
- warnings = mkIf (any isList options.services.dovecot2.mailboxes.definitions) [
- "Declaring `services.dovecot2.mailboxes' as a list is deprecated and will break eval in 21.05! See the release notes for more info for migration."
- ];
+ warnings = warnAboutExtraConfigCollisions;
assertions = [
{
@@ -615,8 +665,8 @@ in
message = "dovecot is configured with showPAMFailure while enablePAM is disabled";
}
{
- assertion = cfg.sieveScripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
- message = "dovecot requires mailUser and mailGroup to be set when sieveScripts is set";
+ assertion = cfg.sieve.scripts != {} -> (cfg.mailUser != null && cfg.mailGroup != null);
+ message = "dovecot requires mailUser and mailGroup to be set when `sieve.scripts` is set";
}
];