From 20f7aafcff1c268b346494da44b595292fb85cbe Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 23 Apr 2026 22:56:53 +0200 Subject: [PATCH] for last 7 days of messages remove large messages first, then by age --- chatmaild/src/chatmaild/expire.py | 20 +++++++++++++- chatmaild/src/chatmaild/tests/test_expire.py | 28 ++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/chatmaild/src/chatmaild/expire.py b/chatmaild/src/chatmaild/expire.py index 7cb710b9..853defa5 100644 --- a/chatmaild/src/chatmaild/expire.py +++ b/chatmaild/src/chatmaild/expire.py @@ -99,11 +99,29 @@ def scan_mailbox_messages(mbox): return messages +# Within this window, large messages are deleted before small ones. +DELETE_LARGE_FIRST_DAYS = 7 + + def expire_to_target(mbox, target_bytes): + cutoff = time.time() - DELETE_LARGE_FIRST_DAYS * 86400 messages = scan_mailbox_messages(mbox) + + def sort_key(msg): + # prio 0: Older than cutoff -> remove oldest first + if msg.mtime < cutoff: + return (0, msg.mtime) + + # prio 1: more recent than cutoff, large -> remove largest first + if msg.quota_size > 200000: + return (1, -msg.quota_size, msg.mtime) + + # prio 2: more recent than cutoff, small -> remove oldest first + return (2, msg.mtime) + total_size = sum(m.quota_size for m in messages) removed = 0 - for entry in sorted(messages): + for entry in sorted(messages, key=sort_key): if total_size <= target_bytes: break (mbox / entry.path).unlink(missing_ok=True) diff --git a/chatmaild/src/chatmaild/tests/test_expire.py b/chatmaild/src/chatmaild/tests/test_expire.py index a58eae53..1f052c82 100644 --- a/chatmaild/src/chatmaild/tests/test_expire.py +++ b/chatmaild/src/chatmaild/tests/test_expire.py @@ -1,6 +1,7 @@ import itertools import os import random +import shutil import time from datetime import datetime from fnmatch import fnmatch @@ -240,6 +241,33 @@ def test_expire_to_target(tmp_path): assert len(scan_mailbox_messages(tmp_path)) == 1 +def test_expire_to_target_prioritization(tmp_path): + def create_messages(): + for sub in ("cur", "new"): + if (tmp_path / sub).exists(): + shutil.rmtree(tmp_path / sub) + # prio 0: older than 7 days + _create_message(tmp_path, "cur", 5 * MB, days_old=10) + # prio 1: last 7 days, large (>200KB) + _create_message(tmp_path, "cur", 5 * MB, days_old=1) + # prio 2: last 7 days, small + _create_message(tmp_path, "cur", 1000, days_old=2) + + # Shrink to 6MB: only the old message (prio 0) is removed. + create_messages() + assert expire_to_target(tmp_path, 6 * MB) == 1 + msgs = scan_mailbox_messages(tmp_path) + assert len(msgs) == 2 + assert all(m.mtime > time.time() - 7 * 86400 for m in msgs) + + # Shrink to 1KB: old and recent-large removed, small survives. + create_messages() + assert expire_to_target(tmp_path, 1024) == 2 + msgs = scan_mailbox_messages(tmp_path) + assert len(msgs) == 1 + assert msgs[0].quota_size == 1000 + + def test_quota_expire_main(tmp_path, capsys): mbox = tmp_path / "user@example.org" _create_message(mbox, "cur", 2 * MB, days_old=5)