From 0da8e2b1971732516c20619e32d76e1a8cf5b5b5 Mon Sep 17 00:00:00 2001 From: Martin Weinelt Date: Tue, 17 Mar 2026 02:21:10 +0100 Subject: [PATCH] quota: expose global quota settings With the options in the upstream dovecot module gone the quota support and its option now live in our downstream module. The only behavior change this introduces is not setting a global per user default instead of the previous 100G per user. Diabling quota support and setting per user quotas now raises an assertion: ```` Failed assertions: - Without quota support enabled, per-user quotas cannot be applied to the following accounts: - lowquota@example.com Either remove per user quota settings or re-enable `mailserver.quota.enable`. ```` --- default.nix | 36 +++++++++++++++++++++++++++-- mail-server/dovecot.nix | 46 ++++++++++++++++++++++++++++--------- mail-server/postfix.nix | 2 ++ scripts/generate-options.py | 1 + 4 files changed, 72 insertions(+), 13 deletions(-) diff --git a/default.nix b/default.nix index cb1f4d5..72317e4 100644 --- a/default.nix +++ b/default.nix @@ -137,6 +137,35 @@ in description = "Message size limit enforced by Postfix."; }; + quota = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Whether to enable quota support. + + When enabled, incoming mail can be rejected if a mailbox exceeds its + quota. + ''; + }; + + defaults = { + perUser = mkOption { + type = with types; nullOr str; + default = null; + example = "10G"; + description = '' + Default quota applied to all users. + + The value must use a size format like `500M`, `2G`, `10G`. + + If set to `null`, no default per user quota is applied and only + explicit per user quotas apply, if set. + ''; + }; + }; + }; + accounts = mkOption { type = types.attrsOf ( types.submodule ( @@ -260,8 +289,11 @@ in 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`. + The quota limit for this user. + + The value is must use a size format like `500M`, `2G`, `10G`. + + If unset, will fall back to {option}`mailserver.quota.defaults.perUser` if set. ''; }; diff --git a/mail-server/dovecot.nix b/mail-server/dovecot.nix index cc4f564..59d9664 100644 --- a/mail-server/dovecot.nix +++ b/mail-server/dovecot.nix @@ -33,6 +33,9 @@ with (import ./common.nix { let inherit (lib) + attrNames + concatMapStringsSep + filterAttrs mapAttrs' mkForce mkIf @@ -170,6 +173,22 @@ in 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)"; } + { + assertion = + let + usersWithQuota = attrNames ( + filterAttrs (_: account: account.quota != null) config.mailserver.loginAccounts + ); + in + !cfg.quota.enable -> usersWithQuota == { }; + message = '' + Without quota support enabled, per-user quotas cannot be applied to the following accounts: + + ${concatMapStringsSep "\n" (account: "- ${account}") quotaUsers} + + Either remove per user quota settings or re-enable `mailserver.quota.enable`. + ''; + } ]; warnings = @@ -278,7 +297,7 @@ in }; vsz_limit = "${toString cfg.lmtpMemoryLimit} MB"; }; - "service quota-status" = { + "service quota-status" = mkIf cfg.quota.enable { executable = toString [ "${config.services.dovecot2.package}/libexec/dovecot/quota-status" "-p" @@ -336,8 +355,10 @@ in mail_max_userip_connections = cfg.maxConnectionsPerUser; mail_plugins = [ "$mail_plugins" - "imap_quota" "imap_sieve" + ] + ++ lib.optionals cfg.quota.enable [ + "imap_quota" ]; }; "protocol pop3" = { @@ -347,6 +368,8 @@ in # globally enabled plugins mail_plugins = [ "$mail_plugins" + ] + ++ lib.optionals cfg.quota.enable [ "quota" ] ++ lib.optionals cfg.fullTextSearch.enable [ @@ -437,14 +460,6 @@ in lmtp_save_to_detail_mailbox = cfg.lmtpSaveToDetailMailbox; plugin = { - quota_rule = "*:storage=100G"; # TODO: quota option - quota = "count:User quota"; # per virtual mail user quota - quota_status_success = "DUNNO"; - quota_status_nouser = "DUNNO"; - quota_status_overquota = "552 5.2.2 Mailbox is full"; - quota_grace = "10%%"; - quota_vsizes = true; - sieve = "file:${cfg.sieveDirectory}/%{user}/scripts;active=${cfg.sieveDirectory}/%{user}/active.sieve"; sieve_default = "file:${cfg.sieveDirectory}/%{user}/default.sieve"; sieve_default_name = "default"; @@ -465,7 +480,16 @@ in fts_enforced = cfg.fullTextSearch.enforced; } // (listToMultiAttrs "fts_autoindex_exclude" cfg.fullTextSearch.autoIndexExclude) - ); + ) + // lib.optionalAttrs cfg.quota.enable { + quota_rule = mkIf (cfg.quota.defaults.perUser != null) "*:storage=${cfg.quota.defaults.perUser}"; + quota = "count:User quota"; # per virtual mail user quota + quota_status_success = "DUNNO"; + quota_status_nouser = "DUNNO"; + quota_status_overquota = "552 5.2.2 Mailbox is full"; + quota_grace = "10%%"; + quota_vsizes = true; + }; } // lib.optionalAttrs cfg.debug.dovecot { mail_debug = true; diff --git a/mail-server/postfix.nix b/mail-server/postfix.nix index 52dc45a..0408138 100644 --- a/mail-server/postfix.nix +++ b/mail-server/postfix.nix @@ -373,6 +373,8 @@ in # reject selected recipients "check_recipient_access ${mappedFile "denied_recipients"}" "check_recipient_access ${mappedFile "reject_recipients"}" + ] + ++ lib.optionals cfg.quota.enable [ # quota checking "check_policy_service unix:/run/dovecot2/quota-status" ]; diff --git a/scripts/generate-options.py b/scripts/generate-options.py index dfba41d..eb13425 100644 --- a/scripts/generate-options.py +++ b/scripts/generate-options.py @@ -33,6 +33,7 @@ groups = [ "mailserver.dmarcReporting", "mailserver.tlsrpt", "mailserver.fullTextSearch", + "mailserver.quota", "mailserver.redis", "mailserver.ldap", "mailserver.monitoring",