Compare commits

..

No commits in common. "539d81cf465483fb557ec738b51d998843fb34ee" and "62676eaaa306e28a14b0468e9e62ec234cce9c02" have entirely different histories.

6 changed files with 163 additions and 209 deletions

View file

@ -43,38 +43,17 @@ Add the module to your `flake.nix`:
Add this to your `configuration.nix` file Add this to your `configuration.nix` file
```nix ```nix
environment.etc."mail-quota-warning-secrets.yml".text = '' environment.etc."eintopf-radar-sync-secrets.yml".text = ''
accounts: EINTOPF_AUTHORIZATION_TOKEN=foobar23
- 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 = { services.mail-quota-warning = {
enable = true; enable = true;
settings = { settings = {
CHECK_INTERVAL_DAYS = 7; EINTOPF_URL = "https://karlsunruh.eintopf.info";
QUOTA_WARNING_THRESHOLD_PERCENT = 80; RADAR_GROUP_ID = "436012";
}; };
secretFile = "/etc/mail-quota-warning-secrets.yml"; secrets = [ /etc/mail-quota-warning-secrets.yml ];
}; };
``` ```
@ -85,7 +64,8 @@ Replace setting variables according to your setup.
``` ```
cd mail-quota-warning cd mail-quota-warning
nix develop nix develop
export CHECK_INTERVAL_DAYS=7 export EINTOPF_URL = "https://karlsunruh.eintopf.info"
export QUOTA_WARNING_THRESHOLD_PERCENT=80 export EINTOPF_AUTHORIZATION_TOKEN = "secret key"
export RADAR_GROUP_ID = "436012"
nix run nix run
``` ```

View file

@ -1,5 +1,6 @@
check_interval_days: 7 # Minimum days between warnings for same account check_interval_days: 7 # Minimum days between warnings for same account
quota_warning_threshold_percent: 80 quota_warning_threshold_percent: 80
working_dir: /var/lib/mail-quota-warning
accounts: accounts:
- name: Sales - name: Sales

View file

@ -181,7 +181,7 @@ def should_send_warning(state, account_name, interval_days):
last_sent = datetime.fromisoformat(last_sent_str) last_sent = datetime.fromisoformat(last_sent_str)
return datetime.now() - last_sent >= timedelta(days=interval_days) return datetime.now() - last_sent >= timedelta(days=interval_days)
def send_warning(config, triggered_accounts, all_quotas, threshold): def send_warning(config, triggered_accounts, all_quotas):
mail_cfg = config["mail"] mail_cfg = config["mail"]
# Create subject based on number of accounts # Create subject based on number of accounts
@ -199,7 +199,7 @@ def send_warning(config, triggered_accounts, all_quotas, threshold):
body_lines.append(f"The mailbox for account '{account_name}' has reached {quota_info['percent_used']:.1f}% of its quota.") 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'])}") body_lines.append(f"Usage: {format_bytes(quota_info['used_kb'])} of {format_bytes(quota_info['limit_kb'])}")
else: else:
body_lines.append(f"The following mailboxes have exceeded the quota threshold ({threshold}%):") body_lines.append("The following mailboxes have exceeded the quota threshold:")
body_lines.append("") body_lines.append("")
for account_name, quota_info in triggered_accounts.items(): 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'])})") body_lines.append(f"{account_name}: {quota_info['percent_used']:.1f}% ({format_bytes(quota_info['used_kb'])} of {format_bytes(quota_info['limit_kb'])})")
@ -263,8 +263,8 @@ def main():
args = parse_args() args = parse_args()
config = load_config(args.config) config = load_config(args.config)
state = load_state() state = load_state()
interval_days = get_config_value(config, "CHECK_INTERVAL_DAYS", "check_interval_days", 7, int) interval_days = config.get("check_interval_days", 7)
threshold = get_config_value(config, "QUOTA_WARNING_THRESHOLD_PERCENT", "quota_warning_threshold_percent", 80, int) threshold = config.get("quota_warning_threshold_percent", 80)
# For thread-safe state updates # For thread-safe state updates
state_lock = threading.Lock() state_lock = threading.Lock()
@ -287,7 +287,7 @@ def main():
# Send consolidated warning email if any accounts triggered # Send consolidated warning email if any accounts triggered
if triggered_accounts: if triggered_accounts:
send_warning(config, triggered_accounts, all_quotas, threshold) send_warning(config, triggered_accounts, all_quotas)
save_state(state) save_state(state)

View file

@ -1,157 +1,139 @@
{ {config, lib, pkgs, ...}:
config,
lib,
pkgs,
...
}:
let let
cfg = config.services.mail-quota-warning; cfg = config.services.mail-quota-warning;
in in
{ {
options = { options = {
services.mail-quota-warning = { services.mail-quota-warning = {
enable = lib.mkOption { enable = lib.mkOption {
type = lib.types.bool; type = lib.types.bool;
default = false; default = false;
description = '' description = ''
Enable mail-quota-warning daemon. 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.
'';
};
};
}; };
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 { settings = lib.mkOption {
type = lib.types.nullOr (lib.types.pathWith { type = lib.types.submodule {
inStore = false; freeformType = with lib.types; attrsOf types.str;
absolute = true; options = {
}); CHECK_INTERVAL_DAYS = lib.mkOption {
default = null; default = "";
example = "/run/keys/mail-quota-warning-secrets"; type = lib.types.str;
description = '' description = ''
A YAML file containing secrets, see example config file Base URL of the target Eintopf host.
in the repository. '';
''; };
}; QUOTA_WARNING_THRESHOLD_PERCENT = lib.mkOption {
default = "";
type = lib.types.str;
description = ''
Radar group ID which events to sync.
'';
};
};
};
default = {};
description = ''
Extra options which should be used by the Radar sync script.
'';
example = lib.literalExpression ''
{
EINTOPF_URL = "eintopf.info";
RADAR_GROUP_ID = "436012";
}
'';
};
interval = lib.mkOption { secretFile = lib.mkOption {
type = lib.types.str; type = with lib.types; listOf path;
default = "*:00,30:00"; description = ''
description = '' A list of files containing the various secrets. Should be in the
How often we run the sync. Default is half an hour. format expected by systemd's `EnvironmentFile` directory.
'';
default = [ ];
};
The format is described in interval = lib.mkOption {
{manpage}`systemd.time(7)`. 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";
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";
# 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";
}; };
}; };
systemd.timers.mail-quota-warning = { config = lib.mkIf cfg.enable {
timerConfig = {
OnCalendar = [ systemd.services."mail-quota-warning" = {
"" description = "mail-quota-warning script";
cfg.interval after = [ "network.target" ];
]; wants = [ "network-online.target" ];
environment = {
PYTHONUNBUFFERED = "1";
} // 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;
};
}; };
wantedBy = [ "timers.target" ];
systemd.timers.mail-quota-warning = {
timerConfig = {
OnCalendar = [
""
cfg.interval
];
};
wantedBy = [ "timers.target" ];
};
}; };
}; meta = {
maintainers = with lib.maintainers; [ onny ];
};
meta = { }
maintainers = with lib.maintainers; [ onny ];
};
}

29
vm-eintopf.nix Normal file
View file

@ -0,0 +1,29 @@
{ 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";
};
};
}

View file

@ -1,38 +0,0 @@
{ 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";
};
}