Files
bongo/contrib/testing/queue-quota-partial-check.py
Mario Fetka b145f52dc9
Debian Trixie package bundle / packages (push) Failing after 14m42s
Distinguish collected mail quota retries
2026-07-23 18:37:51 +02:00

325 lines
10 KiB
Python
Executable File

#!/usr/bin/env python3
# This program is free software, licensed under the terms of the GNU GPL.
# See the Bongo COPYING file for full details.
# Copyright (c) 2026 Bongo Project contributors
"""Verify partial Queue delivery when a mailbox has limited free quota."""
from __future__ import annotations
import hashlib
import os
import re
import subprocess
import sys
import tempfile
from pathlib import Path
from bongo.store.StoreClient import StoreClient
USER = os.environ.get("BONGO_TEST_USER", "stqquota")
PASSWORD = os.environ.get("BONGO_TEST_PASSWORD", "")
ADMIN = os.environ.get("BONGO_TEST_ADMIN", "/usr/bin/bongo-admin")
QUEUE_TOOL = os.environ.get(
"BONGO_TEST_QUEUE_TOOL", "/usr/bin/bongo-queuetool"
)
HOST = os.environ.get("BONGO_TEST_STORE_HOST", "127.0.0.1")
PORT = int(os.environ.get("BONGO_TEST_STORE_PORT", "689"))
MESSAGE_SIZE = int(os.environ.get("BONGO_TEST_QUOTA_MESSAGE_SIZE", "32768"))
ALLOW_LIVE = os.environ.get("BONGO_ALLOW_LIVE_USER_TEST") == "1"
class QuotaQueueTestError(RuntimeError):
"""Raised when the live Queue/Store quota contract is not met."""
def run(
arguments: list[str],
*,
check: bool = True,
binary: bool = False,
) -> subprocess.CompletedProcess:
completed = subprocess.run(
arguments,
check=False,
capture_output=True,
text=not binary,
)
if check and completed.returncode:
stderr = completed.stderr
stdout = completed.stdout
if binary:
stderr = stderr.decode("utf-8", "replace")
stdout = stdout.decode("utf-8", "replace")
detail = stderr.strip() or stdout.strip()
raise QuotaQueueTestError(
f"{' '.join(arguments)} failed: {detail}"
)
return completed
def run_admin(*arguments: str) -> str:
return run(["sudo", "-n", ADMIN, *arguments]).stdout
def run_queue(*arguments: str, check: bool = True, binary: bool = False):
return run(
["sudo", "-n", "-u", "bongo", QUEUE_TOOL, *arguments],
check=check,
binary=binary,
)
def read_admin_quota() -> tuple[int, int]:
output = run_admin("user", "quota", USER)
used_match = re.search(r"^Used: ([0-9]+) bytes$", output, re.MULTILINE)
limit_match = re.search(
r"^Quota: (?:(unlimited)|([0-9]+) bytes)$", output, re.MULTILINE
)
if not used_match or not limit_match:
raise QuotaQueueTestError(
f"cannot parse bongo-admin quota output: {output!r}"
)
limit = 0 if limit_match.group(1) else int(limit_match.group(2))
return int(used_match.group(1)), limit
def set_admin_quota(limit: int) -> None:
run_admin(
"user",
"quota",
USER,
"unlimited" if limit == 0 else str(limit),
)
def create_message(marker: str) -> bytes:
headers = (
f"From: STQ-10 <stq10@bongo.test>\r\n"
f"To: {USER}@bongo.test\r\n"
f"Subject: STQ-10 partial quota delivery {marker}\r\n"
f"Date: Thu, 23 Jul 2026 12:34:56 +0200\r\n"
f"Message-ID: <stq10-{marker}@bongo.test>\r\n"
"MIME-Version: 1.0\r\n"
"Content-Type: text/plain; charset=UTF-8\r\n"
"Content-Transfer-Encoding: 8bit\r\n"
f"X-STQ-10-Marker: {marker}\r\n"
"\r\n"
).encode()
if len(headers) >= MESSAGE_SIZE:
raise QuotaQueueTestError(
"BONGO_TEST_QUOTA_MESSAGE_SIZE is too small"
)
return headers + marker.encode() + b":" + b"Q" * (
MESSAGE_SIZE - len(headers) - len(marker) - 1
)
def open_store() -> StoreClient:
return StoreClient(
USER,
USER,
authPassword=PASSWORD,
host=HOST,
port=PORT,
)
def inbox_documents(store: StoreClient) -> dict[str, int]:
return {
document.uid: int(document.bodylen)
for document in store.List("/mail/INBOX")
}
def safe_queue_delete(queue_id: str | None) -> None:
if queue_id:
run_queue("delete", queue_id, check=False)
def safe_store_delete(store: StoreClient | None, document: str) -> None:
if store is None:
return
try:
store.Delete(document)
except Exception as error:
print(
f"STQ-10 cleanup warning for Store document {document}: {error}",
file=sys.stderr,
)
def require_safe_environment() -> None:
if not ALLOW_LIVE:
raise QuotaQueueTestError(
"set BONGO_ALLOW_LIVE_USER_TEST=1 for the disposable live account"
)
if not PASSWORD:
raise QuotaQueueTestError("BONGO_TEST_PASSWORD must be set")
if MESSAGE_SIZE < 4096:
raise QuotaQueueTestError(
"BONGO_TEST_QUOTA_MESSAGE_SIZE must be at least 4096"
)
def main() -> int:
require_safe_environment()
baseline, original_limit = read_admin_quota()
queue_ids: list[str] = []
created_documents: list[str] = []
store: StoreClient | None = None
try:
store = open_store()
before = inbox_documents(store)
queued_messages: list[bytes] = []
with tempfile.TemporaryDirectory(prefix="bongo-stq10-") as work:
work_path = Path(work)
os.chmod(work, 0o755)
for index in range(2):
message_path = work_path / f"message-{index}.eml"
message_path.write_bytes(
create_message(f"{os.getpid()}-{index}")
)
os.chmod(message_path, 0o644)
queue_id = run_queue(
"hold-local", str(message_path)
).stdout.strip()
if not queue_id.startswith("999-"):
raise QuotaQueueTestError(
f"unexpected held Queue ID: {queue_id!r}"
)
queue_ids.append(queue_id)
queued_messages.append(
run_queue(
"message", queue_id, binary=True
).stdout
)
first_size = len(queued_messages[0])
set_admin_quota(baseline + first_size)
run_queue(
"deliver-local",
queue_ids[0],
"stq10@bongo.test",
USER,
"INBOX",
)
after_first = inbox_documents(store)
first_new = set(after_first) - set(before)
if len(first_new) != 1:
raise QuotaQueueTestError(
"first delivery did not create exactly one Store document"
)
first_document = first_new.pop()
created_documents.append(first_document)
if store.Read(first_document) != queued_messages[0]:
raise QuotaQueueTestError(
"first Store document differs from its Queue message"
)
rejected = run_queue(
"deliver-local",
queue_ids[1],
"stq10@bongo.test",
USER,
"INBOX",
check=False,
)
rejected_output = rejected.stdout + rejected.stderr
if rejected.returncode == 0:
raise QuotaQueueTestError(
"second delivery unexpectedly crossed the hard quota"
)
if "5220" not in rejected_output and "quota" not in rejected_output.lower():
raise QuotaQueueTestError(
"second delivery failed without a quota result: "
+ rejected_output.strip()
)
retained = run_queue(
"message", queue_ids[1], binary=True
).stdout
if retained != queued_messages[1]:
raise QuotaQueueTestError(
"quota rejection changed or removed the second Queue message"
)
retained_hash = hashlib.sha256(retained).hexdigest()
store.Delete(first_document)
created_documents.remove(first_document)
used_after_cleanup, limit_after_cleanup = read_admin_quota()
if (used_after_cleanup, limit_after_cleanup) != (
baseline,
baseline + first_size,
):
raise QuotaQueueTestError(
"deleting the first message did not release its quota"
)
run_queue(
"deliver-local",
queue_ids[1],
"stq10@bongo.test",
USER,
"INBOX",
)
after_retry = inbox_documents(store)
retry_new = set(after_retry) - set(before)
if len(retry_new) != 1:
raise QuotaQueueTestError(
"retry did not create exactly one Store document"
)
retry_document = retry_new.pop()
created_documents.append(retry_document)
if store.Read(retry_document) != queued_messages[1]:
raise QuotaQueueTestError(
"retried Store document differs from retained Queue message"
)
store.Delete(retry_document)
created_documents.remove(retry_document)
restored_used, temporary_limit = read_admin_quota()
if restored_used != baseline or temporary_limit != baseline + first_size:
raise QuotaQueueTestError(
"test Store cleanup did not restore baseline usage"
)
finally:
for document in reversed(created_documents):
safe_store_delete(store, document)
if store is not None:
try:
store.Quit()
except Exception:
pass
for queue_id in queue_ids:
safe_queue_delete(queue_id)
set_admin_quota(original_limit)
restored_used, restored_limit = read_admin_quota()
if (restored_used, restored_limit) != (baseline, original_limit):
raise QuotaQueueTestError(
"final cleanup did not restore the original quota state"
)
print(
"STQ-10 PARTIAL PASS "
f"user={USER} first-bytes={first_size} "
f"second-bytes={len(queued_messages[1])} "
f"retained-sha256={retained_hash} "
"first-delivered=yes second-retained=yes retry-exact=yes "
"quota-restored=yes"
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (OSError, QuotaQueueTestError) as error:
print(f"STQ-10 PARTIAL FAIL: {error}", file=sys.stderr)
raise SystemExit(1)