From ccbfeb097bce86a5c2e3d279c687baa86539a7f1 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 18 Aug 2025 15:40:13 +0200 Subject: [PATCH 01/10] fix module option defaults --- module.nix | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/module.nix b/module.nix index 1e6efe1..67061ed 100644 --- a/module.nix +++ b/module.nix @@ -22,15 +22,15 @@ in freeformType = with lib.types; attrsOf types.str; options = { CHECK_INTERVAL_DAYS = lib.mkOption { - default = ""; - type = lib.types.str; + default = 7; + type = lib.types.int; description = '' Base URL of the target Eintopf host. ''; }; QUOTA_WARNING_THRESHOLD_PERCENT = lib.mkOption { - default = ""; - type = lib.types.str; + default = 80; + type = lib.types.int; description = '' Radar group ID which events to sync. ''; @@ -44,7 +44,7 @@ in example = lib.literalExpression '' { EINTOPF_URL = "eintopf.info"; - RADAR_GROUP_ID = "436012"; + QUOTA_WARNING_RADAR_GROUP_ID = "436012"; } ''; }; From ee7b81d07aa0255ab5bf0ebc2514e2e30d3580df Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 18 Aug 2025 15:44:15 +0200 Subject: [PATCH 02/10] fix module option defaults --- README.md | 9 ++++----- module.nix | 14 ++++++++------ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2379e42..17abc5c 100644 --- a/README.md +++ b/README.md @@ -50,8 +50,8 @@ EINTOPF_AUTHORIZATION_TOKEN=foobar23 services.mail-quota-warning = { enable = true; settings = { - EINTOPF_URL = "https://karlsunruh.eintopf.info"; - RADAR_GROUP_ID = "436012"; + CHECK_INTERVAL_DAYS = 7; + QUOTA_WARNING_THRESHOLD_PERCENT = 80; }; secrets = [ /etc/mail-quota-warning-secrets.yml ]; }; @@ -64,8 +64,7 @@ Replace setting variables according to your setup. ``` cd mail-quota-warning nix develop -export EINTOPF_URL = "https://karlsunruh.eintopf.info" -export EINTOPF_AUTHORIZATION_TOKEN = "secret key" -export RADAR_GROUP_ID = "436012" +export CHECK_INTERVAL_DAYS=7 +export QUOTA_WARNING_THRESHOLD_PERCENT=80 nix run ``` diff --git a/module.nix b/module.nix index 67061ed..9b21493 100644 --- a/module.nix +++ b/module.nix @@ -19,32 +19,34 @@ in settings = lib.mkOption { type = lib.types.submodule { - freeformType = with lib.types; attrsOf types.str; + freeformType = with lib.types; attrsOf types.str types.int; options = { CHECK_INTERVAL_DAYS = lib.mkOption { default = 7; type = lib.types.int; description = '' - Base URL of the target Eintopf host. + Interval of days in which a warning message will be + delivered. ''; }; QUOTA_WARNING_THRESHOLD_PERCENT = lib.mkOption { default = 80; type = lib.types.int; description = '' - Radar group ID which events to sync. + Threshold of used mailbox space in percent after which + a warning message will be delivered. ''; }; }; }; default = {}; description = '' - Extra options which should be used by the Radar sync script. + Extra options which should be used by the mailbox quota warning script. ''; example = lib.literalExpression '' { - EINTOPF_URL = "eintopf.info"; - QUOTA_WARNING_RADAR_GROUP_ID = "436012"; + CHECK_INTERVAL_DAYS = 7; + QUOTA_WARNING_THRESHOLD_PERCENT = 80; } ''; }; From 80bb80632d1bb99d1191e074b30a9115fe5b8ec1 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 18 Aug 2025 15:46:38 +0200 Subject: [PATCH 03/10] fix module option freeform --- module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module.nix b/module.nix index 9b21493..11f3433 100644 --- a/module.nix +++ b/module.nix @@ -19,7 +19,7 @@ in settings = lib.mkOption { type = lib.types.submodule { - freeformType = with lib.types; attrsOf types.str types.int; + freeformType = with lib.types; attrsOf anything; options = { CHECK_INTERVAL_DAYS = lib.mkOption { default = 7; From 9a764301815ab82e13ba64b5cd559af6fe52090d Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 18 Aug 2025 15:49:13 +0200 Subject: [PATCH 04/10] fix module option freeform --- module.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module.nix b/module.nix index 11f3433..63cd1dd 100644 --- a/module.nix +++ b/module.nix @@ -82,7 +82,7 @@ in wants = [ "network-online.target" ]; environment = { PYTHONUNBUFFERED = "1"; - } // cfg.settings; + } // lib.mapAttrs (_: v: toString v) cfg.settings; serviceConfig = { Type = "simple"; ExecStart = lib.getExe pkgs.mail-quota-warning; From e658def79869032e48fff6945958c4d198262df1 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 18 Aug 2025 16:00:51 +0200 Subject: [PATCH 05/10] reformat, fix load env vars, update README --- README.md | 23 +++- mail-quota-warning.py | 4 +- module.nix | 258 ++++++++++++++++++++++-------------------- 3 files changed, 159 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index 17abc5c..fb4e9a3 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,28 @@ Add this to your `configuration.nix` file ```nix environment.etc."eintopf-radar-sync-secrets.yml".text = '' -EINTOPF_AUTHORIZATION_TOKEN=foobar23 +accounts: + - name: Sales + imap_server: mail.example.com + imap_port: 993 + username: sales@example.com + password: secret + + - name: Support + imap_server: mail.example.com + imap_port: 993 + username: support@example.com + password: secret + +mail: + smtp_server: mail.example.com + smtp_port: 587 + smtp_username: monitoring@example.com + smtp_password: secret + from_address: monitoring@example.com + recipients: + - admin1@example.com + - admin2@example.com ''; services.mail-quota-warning = { diff --git a/mail-quota-warning.py b/mail-quota-warning.py index 5b2eea3..08a4d2b 100644 --- a/mail-quota-warning.py +++ b/mail-quota-warning.py @@ -263,8 +263,8 @@ def main(): args = parse_args() config = load_config(args.config) state = load_state() - interval_days = config.get("check_interval_days", 7) - threshold = config.get("quota_warning_threshold_percent", 80) + interval_days = get_config_value(config, "CHECK_INTERVAL_DAYS", "check_interval_days", 7, int) + threshold = get_config_value(config, "QUOTA_WARNING_THRESHOLD_PERCENT", "quota_warning_threshold_percent", 80, int) # For thread-safe state updates state_lock = threading.Lock() diff --git a/module.nix b/module.nix index 63cd1dd..18cdb4f 100644 --- a/module.nix +++ b/module.nix @@ -1,141 +1,153 @@ -{config, lib, pkgs, ...}: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.services.mail-quota-warning; -in - { +in +{ - options = { - services.mail-quota-warning = { + options = { + services.mail-quota-warning = { - enable = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Enable mail-quota-warning daemon. - ''; - }; + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Enable mail-quota-warning daemon. + ''; + }; - settings = lib.mkOption { - type = lib.types.submodule { - freeformType = with lib.types; attrsOf anything; - options = { - CHECK_INTERVAL_DAYS = lib.mkOption { - default = 7; - type = lib.types.int; - description = '' - Interval of days in which a warning message will be - delivered. - ''; - }; - QUOTA_WARNING_THRESHOLD_PERCENT = lib.mkOption { - default = 80; - type = lib.types.int; - description = '' - Threshold of used mailbox space in percent after which - a warning message will be delivered. - ''; - }; - }; + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = with lib.types; attrsOf anything; + options = { + CHECK_INTERVAL_DAYS = lib.mkOption { + default = 7; + type = lib.types.int; + description = '' + Interval of days in which a warning message will be + delivered. + ''; + }; + QUOTA_WARNING_THRESHOLD_PERCENT = lib.mkOption { + default = 80; + type = lib.types.int; + description = '' + Threshold of used mailbox space in percent after which + a warning message will be delivered. + ''; + }; }; - default = {}; - description = '' - Extra options which should be used by the mailbox quota warning script. - ''; - example = lib.literalExpression '' - { - CHECK_INTERVAL_DAYS = 7; - QUOTA_WARNING_THRESHOLD_PERCENT = 80; - } - ''; }; - - secretFile = lib.mkOption { - type = with lib.types; listOf path; - description = '' - A list of files containing the various secrets. Should be in the - format expected by systemd's `EnvironmentFile` directory. - ''; - default = [ ]; - }; - - interval = lib.mkOption { - type = lib.types.str; - default = "*:00,30:00"; - description = '' - How often we run the sync. Default is half an hour. - - The format is described in - {manpage}`systemd.time(7)`. - ''; - }; - - }; - }; - - config = lib.mkIf cfg.enable { - - systemd.services."mail-quota-warning" = { - description = "mail-quota-warning script"; - after = [ "network.target" ]; - wants = [ "network-online.target" ]; - environment = { - PYTHONUNBUFFERED = "1"; - } // lib.mapAttrs (_: v: toString v) cfg.settings; - serviceConfig = { - Type = "simple"; - ExecStart = lib.getExe pkgs.mail-quota-warning; - - # hardening - AmbientCapabilities = ""; - CapabilityBoundingSet = "" ; - DevicePolicy = "closed"; - DynamicUser = true; - LockPersonality = true; - MemoryDenyWriteExecute = true; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateTmp = true; - PrivateUsers = true; - ProcSubset = "pid"; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - ProtectProc = "invisible"; - ProtectSystem = "strict"; - RemoveIPC = true; - RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - SystemCallFilter = [ "@system-service" "~@privileged" ]; - UMask = "0077"; - } // lib.optionalAttrs (cfg.secretFile != [ ]) { - EnvironmentFile = cfg.secretFile; - }; + default = { }; + description = '' + Extra options which should be used by the mailbox quota warning script. + ''; + example = lib.literalExpression '' + { + CHECK_INTERVAL_DAYS = 7; + QUOTA_WARNING_THRESHOLD_PERCENT = 80; + } + ''; }; - systemd.timers.mail-quota-warning = { - timerConfig = { - OnCalendar = [ - "" - cfg.interval - ]; - }; - wantedBy = [ "timers.target" ]; + secretFile = lib.mkOption { + type = with lib.types; listOf path; + description = '' + A list of files containing the various secrets. Should be in the + format expected by systemd's `EnvironmentFile` directory. + ''; + default = [ ]; + }; + + interval = lib.mkOption { + type = lib.types.str; + default = "*:00,30:00"; + description = '' + How often we run the sync. Default is half an hour. + + The format is described in + {manpage}`systemd.time(7)`. + ''; }; }; + }; - meta = { - maintainers = with lib.maintainers; [ onny ]; + config = lib.mkIf cfg.enable { + + systemd.services."mail-quota-warning" = { + description = "mail-quota-warning script"; + after = [ "network.target" ]; + wants = [ "network-online.target" ]; + environment = { + PYTHONUNBUFFERED = "1"; + } + // lib.mapAttrs (_: v: toString v) cfg.settings; + serviceConfig = { + Type = "simple"; + ExecStart = lib.getExe pkgs.mail-quota-warning; + + # hardening + AmbientCapabilities = ""; + CapabilityBoundingSet = ""; + DevicePolicy = "closed"; + DynamicUser = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + } + // lib.optionalAttrs (cfg.secretFile != [ ]) { + EnvironmentFile = cfg.secretFile; + }; }; - } + systemd.timers.mail-quota-warning = { + timerConfig = { + OnCalendar = [ + "" + cfg.interval + ]; + }; + wantedBy = [ "timers.target" ]; + }; + }; + + meta = { + maintainers = with lib.maintainers; [ onny ]; + }; + +} From 805e5be3b1701df9775ccc4201130fc98f349a75 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 18 Aug 2025 16:08:21 +0200 Subject: [PATCH 06/10] fix passing secret file as config --- module.nix | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/module.nix b/module.nix index 18cdb4f..5e729a9 100644 --- a/module.nix +++ b/module.nix @@ -57,12 +57,16 @@ in }; secretFile = lib.mkOption { - type = with lib.types; listOf path; + type = lib.types.nullOr (lib.types.pathWith { + inStore = false; + absolute = true; + }); + default = null; + example = "/run/keys/mail-quota-warning-secrets"; description = '' - A list of files containing the various secrets. Should be in the - format expected by systemd's `EnvironmentFile` directory. + A YAML file containing secrets, see example config file + in the repository. ''; - default = [ ]; }; interval = lib.mkOption { @@ -91,7 +95,7 @@ in // lib.mapAttrs (_: v: toString v) cfg.settings; serviceConfig = { Type = "simple"; - ExecStart = lib.getExe pkgs.mail-quota-warning; + ExecStart = "${lib.getExe pkgs.mail-quota-warning}${lib.optionalString (cfg.secretFile != null) " --config ${cfg.secretFile}"}"; # hardening AmbientCapabilities = ""; @@ -128,9 +132,6 @@ in "~@privileged" ]; UMask = "0077"; - } - // lib.optionalAttrs (cfg.secretFile != [ ]) { - EnvironmentFile = cfg.secretFile; }; }; From 8bd5cace11e0d803fd8ee0851ea945780aafa2ca Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 18 Aug 2025 16:17:35 +0200 Subject: [PATCH 07/10] update vm file, fix README, add working dir in module --- README.md | 2 +- config.yml.example | 1 - module.nix | 2 ++ vm-eintopf.nix | 29 ----------------------------- vm-mail-quota-warning.py | 38 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 41 insertions(+), 31 deletions(-) delete mode 100644 vm-eintopf.nix create mode 100644 vm-mail-quota-warning.py diff --git a/README.md b/README.md index fb4e9a3..d5a6110 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Add the module to your `flake.nix`: Add this to your `configuration.nix` file ```nix -environment.etc."eintopf-radar-sync-secrets.yml".text = '' +environment.etc."mail-quota-warning-secrets.yml".text = '' accounts: - name: Sales imap_server: mail.example.com diff --git a/config.yml.example b/config.yml.example index 9b3f4ff..dfc6526 100644 --- a/config.yml.example +++ b/config.yml.example @@ -1,6 +1,5 @@ check_interval_days: 7 # Minimum days between warnings for same account quota_warning_threshold_percent: 80 -working_dir: /var/lib/mail-quota-warning accounts: - name: Sales diff --git a/module.nix b/module.nix index 5e729a9..e9f322b 100644 --- a/module.nix +++ b/module.nix @@ -96,6 +96,8 @@ in serviceConfig = { Type = "simple"; ExecStart = "${lib.getExe pkgs.mail-quota-warning}${lib.optionalString (cfg.secretFile != null) " --config ${cfg.secretFile}"}"; + WorkingDirectory = "%S/mail-quota-warning"; + StateDirectory = "mail-quota-warning"; # hardening AmbientCapabilities = ""; diff --git a/vm-eintopf.nix b/vm-eintopf.nix deleted file mode 100644 index f4933d6..0000000 --- a/vm-eintopf.nix +++ /dev/null @@ -1,29 +0,0 @@ -{ pkgs, ... }: -let - - template-karlsunruh = pkgs.stdenv.mkDerivation { - name = "karlsunruh"; - src = pkgs.fetchgit { - url = "https://git.project-insanity.org/onny/eintopf-karlsunruh.git"; - rev = "0c2a36574260da70da80b379d7475af7b29849c9"; - hash = "sha256-GPKlqpztl4INqVyz/4y/vVrkDPHA3rIxtUZB9LNZ96c="; - }; - dontBuild = true; - installPhase = '' - cp -r . $out/ - ''; - }; - -in -{ - - services.eintopf = { - enable = true; - settings = { - EINTOPF_THEMES = "eintopf,${template-karlsunruh}"; - EINTOPF_ADMIN_PASSWORD = "foobar23"; - EINTOPF_ADMIN_EMAIL = "onny@project-insanity.org"; - }; - }; - -} diff --git a/vm-mail-quota-warning.py b/vm-mail-quota-warning.py new file mode 100644 index 0000000..7cfe699 --- /dev/null +++ b/vm-mail-quota-warning.py @@ -0,0 +1,38 @@ +{ pkgs, ... }: +{ + + environment.etc."mail-quota-warning-secrets.yml".text = '' + accounts: + - name: Sales + imap_server: mail.example.com + imap_port: 993 + username: sales@example.com + password: secret + + - name: Support + imap_server: mail.example.com + imap_port: 993 + username: support@example.com + password: secret + + mail: + smtp_server: mail.example.com + smtp_port: 587 + smtp_username: monitoring@example.com + smtp_password: secret + from_address: monitoring@example.com + recipients: + - admin1@example.com + - admin2@example.com + ''; + + services.mail-quota-warning = { + enable = true; + settings = { + CHECK_INTERVAL_DAYS = 7; + QUOTA_WARNING_THRESHOLD_PERCENT = 80; + }; + secretFile = /etc/mail-quota-warning-secrets.yml; + }; + +} From 74502b0c62b56df4b4b0e85f452a30a7ce9d45f1 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 18 Aug 2025 16:22:47 +0200 Subject: [PATCH 08/10] add current threshold to warning mail --- mail-quota-warning.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mail-quota-warning.py b/mail-quota-warning.py index 08a4d2b..26fb045 100644 --- a/mail-quota-warning.py +++ b/mail-quota-warning.py @@ -181,7 +181,7 @@ def should_send_warning(state, account_name, interval_days): last_sent = datetime.fromisoformat(last_sent_str) return datetime.now() - last_sent >= timedelta(days=interval_days) -def send_warning(config, triggered_accounts, all_quotas): +def send_warning(config, triggered_accounts, all_quotas, threshold): mail_cfg = config["mail"] # Create subject based on number of accounts @@ -199,7 +199,7 @@ def send_warning(config, triggered_accounts, all_quotas): body_lines.append(f"The mailbox for account '{account_name}' has reached {quota_info['percent_used']:.1f}% of its quota.") body_lines.append(f"Usage: {format_bytes(quota_info['used_kb'])} of {format_bytes(quota_info['limit_kb'])}") else: - body_lines.append("The following mailboxes have exceeded the quota threshold:") + body_lines.append(f"The following mailboxes have exceeded the quota threshold ({threshold}%):") body_lines.append("") for account_name, quota_info in triggered_accounts.items(): body_lines.append(f"• {account_name}: {quota_info['percent_used']:.1f}% ({format_bytes(quota_info['used_kb'])} of {format_bytes(quota_info['limit_kb'])})") @@ -287,7 +287,7 @@ def main(): # Send consolidated warning email if any accounts triggered if triggered_accounts: - send_warning(config, triggered_accounts, all_quotas) + send_warning(config, triggered_accounts, all_quotas, threshold) save_state(state) From 3b4176fa0a643be9f0f2b6c70a3d7eec5c392795 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Mon, 18 Aug 2025 16:37:04 +0200 Subject: [PATCH 09/10] fix secretFile option format --- README.md | 2 +- vm-mail-quota-warning.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d5a6110..ddae5f2 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ services.mail-quota-warning = { CHECK_INTERVAL_DAYS = 7; QUOTA_WARNING_THRESHOLD_PERCENT = 80; }; - secrets = [ /etc/mail-quota-warning-secrets.yml ]; + secretFile = "/etc/mail-quota-warning-secrets.yml"; }; ``` diff --git a/vm-mail-quota-warning.py b/vm-mail-quota-warning.py index 7cfe699..93bc22e 100644 --- a/vm-mail-quota-warning.py +++ b/vm-mail-quota-warning.py @@ -32,7 +32,7 @@ CHECK_INTERVAL_DAYS = 7; QUOTA_WARNING_THRESHOLD_PERCENT = 80; }; - secretFile = /etc/mail-quota-warning-secrets.yml; + secretFile = "/etc/mail-quota-warning-secrets.yml"; }; } From 539d81cf465483fb557ec738b51d998843fb34ee Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Fri, 2 Jan 2026 13:20:46 +0100 Subject: [PATCH 10/10] switch to systemd LoadCredential --- module.nix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/module.nix b/module.nix index e9f322b..3de0c6c 100644 --- a/module.nix +++ b/module.nix @@ -95,7 +95,8 @@ in // lib.mapAttrs (_: v: toString v) cfg.settings; serviceConfig = { Type = "simple"; - ExecStart = "${lib.getExe pkgs.mail-quota-warning}${lib.optionalString (cfg.secretFile != null) " --config ${cfg.secretFile}"}"; + LoadCredential = lib.optionalString (cfg.secretFile != null) "secrets.yaml:${cfg.secretFile}"; + ExecStart = "${lib.getExe pkgs.mail-quota-warning}${lib.optionalString (cfg.secretFile != null) " --config \${CREDENTIALS_DIRECTORY}/secrets.yaml"; WorkingDirectory = "%S/mail-quota-warning"; StateDirectory = "mail-quota-warning";