feat(mail): ARC-09 Postfach-Quota (unabhängig von Core LIC-05)

mailboxconfig (IMP-07) bekommt eine quota_bytes-Spalte statt einer
eigenen Tabelle — ein Postfach ist bereits eindeutig über
(tenant_slug, name) identifiziert. SetQuotaBytes/LimitBytes, 0 =
unbegrenzt (Standardwert, keine Migration bestehender Postfächer
nötig). LimitBytes erfüllt strukturell quota.LimitProvider.

storage.ArchiveMailboxPrefix (ARC-04-Ergänzung, Präfix ALLER Jahre
eines Postfachs) und storage.UsageCounter: realer Speicherverbrauch
durch echtes S3-Listing im physisch getrennten Mandanten-Bucket
(ARC-06) — kein separat gepflegter Zählerstand.

Neues Paket mail/internal/quota: Checker verbindet LimitProvider und
UsageProvider. Kein konfiguriertes Limit = immer erlaubt (Core-LIC-05-
Quota läuft unabhängig weiter — beide Ebenen bewusst unabhängig
durchgesetzt, bekannter Fehler vermieden).

smtp.QuotaChecker (schmale Schnittstelle, keine Paketkopplung an
quota) wird in handleRcptTo geprüft, VOR der Datenübertragung: 552
(RFC 5321 "exceeded storage allocation") bei Überschreitung, Session
bleibt nutzbar. nil-Checker erhält bisheriges Verhalten unverändert.

Alle drei Pflichtprüfungen mit echten Nachweisen: Quota-Überschreitung
liefert 552, Session bleibt funktionsfähig; ein anderes Postfach
desselben Tenants läuft währenddessen vollständig normal durch;
vollständiger Ende-zu-Ende-Integrationstest gegen reale Postgres- und
MinIO-Instanzen — 5000 echte Bytes abgelegt, real gemessen, Limit
knapp darunter/darüber gesetzt, SMTP reagiert jeweils korrekt auf den
tatsächlichen gemessenen Wert. Dabei einen echten Cleanup-Fehler
gefunden und behoben (defer schloss den Pool vor dem zugehörigen
t.Cleanup, verwaiste Testdaten blieben zurück).

go build/go vet/golangci-lint clean, gesamtes Mail-Modul
regressionsfrei getestet.
This commit is contained in:
sysops
2026-09-02 23:47:32 +02:00
parent c344fa938b
commit 1825387603
12 changed files with 762 additions and 5 deletions
+112
View File
@@ -0,0 +1,112 @@
# ARC-09 — Postfach-Quota: Prüfprotokoll
Datum: 2026-09-02
Host: 192.168.1.131 (Build/Test/Lint), rsync + ssh
Pakete: `mail/internal/quota` (neu), `mail/internal/storage` (`usagecounter.go`, `archivekey.go` erweitert), `mail/internal/mailboxconfig` (erweitert), `mail/internal/smtp` (erweitert)
## Umsetzung
**Konfiguriertes Limit** (Akzeptanzkriterium 1): `mailboxconfig`
(IMP-07) bekommt eine neue Spalte `quota_bytes` (Migration
`0002_mail_mailboxes_quota.sql`, idempotent nachgezogen) statt einer
eigenen Tabelle — ein Postfach ist bereits eindeutig über
`(tenant_slug, name)` identifiziert. `Store.SetQuotaBytes`/`LimitBytes`
(`0` = unbegrenzt, Standardwert, keine Migration bestehender
Postfächer nötig). `LimitBytes` erfüllt strukturell `quota.
LimitProvider` — eigenständig von der tenant-weiten Core-LIC-05-Quota.
**Realer Verbrauch** (Pflichtprüfung 3): `storage.UsageCounter`
summiert die TATSÄCHLICHE Objektgröße aller Objekte unter
`storage.ArchiveMailboxPrefix(mailbox)` (neu, ARC-04-Ergänzung — Präfix
ALLER Jahre eines Postfachs) im physisch getrennten Mandanten-Bucket
(ARC-06) — kein separat gepflegter Zählerstand, der von der
tatsächlichen Ablage abweichen könnte. Erfüllt strukturell `quota.
UsageProvider`.
**Verknüpfung**: `quota.Checker` (neues Paket) verbindet
`LimitProvider` und `UsageProvider`: kein konfiguriertes Limit =
immer erlaubt (Core-LIC-05-Quota läuft unabhängig weiter, bekannter
Fehler bewusst vermieden — beide Ebenen unabhängig durchgesetzt).
**SMTP-Durchsetzung** (Akzeptanzkriterium 2): `smtp.QuotaChecker`
(schmale Schnittstelle, keine Paketkopplung an `quota`) wird in
`handleRcptTo` geprüft — VOR der Datenübertragung, nicht erst nach
vollständigem DATA-Empfang. Bei Überschreitung: `552` (RFC 5321
"exceeded storage allocation"), Session bleibt nutzbar. Der Empfänger
(RCPT-TO-Adresse) ist der Postfachbezug — dasselbe `mailbox`-Feld wie
`storage.ArchiveKey`/`mailboxconfig`. `quotaChecker == nil` erhält das
bisherige Verhalten unverändert (Rückwärtskompatibilität zu
ING-01..QA-04).
## Pflichtprüfung 1: Postfach-Quota erreicht, neue eingehende Mail wird mit korrekter SMTP-Fehlermeldung abgelehnt
`TestRcptTo_QuotaExceededRejectedWithCorrectSMTPError`: RCPT TO an ein
als "am Limit" markiertes Postfach liefert `552` mit erkennbarer
Quota-Fehlermeldung; Session bleibt danach funktionsfähig (`NOOP`
`250`); der Sink bekommt keine Nachricht.
Ergebnis: **BESTANDEN**.
## Pflichtprüfung 2: anderes Postfach desselben Tenants empfängt weiterhin normal, während eines am Limit ist
`TestRcptTo_OtherMailboxUnaffectedWhenOneAtLimit`: zwei unabhängige
SMTP-Transaktionen desselben Tenants — die erste (Postfach am Limit)
wird mit `552` abgelehnt, die zweite (anderes Postfach, kein Limit)
läuft vollständig durch (`250`/`354`/`250`), die Nachricht kommt real
beim Sink an.
Ergebnis: **BESTANDEN**.
## Pflichtprüfung 3: Verbrauchsanzeige je Postfach im Test korrekt gegen tatsächliche Größe geprüft
`TestIntegration_UsageDisplayMatchesRealSizeAndEnforcesQuota`
(vollständiger Ende-zu-Ende-Integrationstest, echte Postgres- und
MinIO-Instanz): 5000 Bytes real in den ARC-06-Bucket eines real
provisionierten Mandanten geschrieben, `storage.UsageCounter.
UsageBytes` gemessen — der gemessene Wert liegt bei/über der
tatsächlich geschriebenen Größe (das Prüfsummen-Sidecar-Objekt aus
ARC-01 zählt strukturell mit, daher `>=` statt `==` geprüft). Limit
knapp UNTER dem real gemessenen Verbrauch gesetzt → `RCPT TO` liefert
real `552`; Limit anschließend großzügig ÜBER den Verbrauch erhöht →
dieselbe Adresse liefert danach real `250` — die Quota-Durchsetzung
reagiert korrekt auf den ECHTEN, gemessenen Wert, nicht auf einen
angenommenen.
Ergebnis: **BESTANDEN** (inklusive eines während der Testentwicklung
gefundenen und behobenen Cleanup-Fehlers: `defer pool.Close()` schloss
die Postgres-Verbindung VOR den zugehörigen `t.Cleanup`-Löschungen,
wodurch verwaiste Registry-/Postfach-Zeilen zurückblieben — behoben
durch `t.Cleanup(pool.Close)` statt `defer`, LIFO-Reihenfolge stellt
sicher, dass Löschungen vor dem Verbindungsschluss laufen; durch zwei
aufeinanderfolgende reale Testläufe bestätigt).
## Akzeptanzkriterien
1. **Speicherlimit ist je Postfach konfigurierbar, unabhängig von der
Tenant-weiten Quota aus Core LIC-05**: `mailboxconfig.
SetQuotaBytes`/`LimitBytes`, durch Pflichtprüfung 3 belegt.
2. **Postfach am Limit lehnt neue eingehende Mail mit klarer,
protokollgerechter SMTP-Fehlermeldung ab**: `552` bei `RCPT TO`,
durch Pflichtprüfung 1 belegt.
3. **Ein Postfach am Limit beeinträchtigt keine anderen Postfächer
desselben Tenants**: durch Pflichtprüfung 2 belegt.
## Build/Vet/Lint/Test — Gesamtmodul
```
go build ./... → OK
go vet ./... → OK
golangci-lint run ./... → 0 issues
go test ./... -p 1 (TEST_TENANT_DSN, TEST_MANTICORE_URL, TEST_S3_ENDPOINT/TEST_S3_ACCESS_KEY/TEST_S3_SECRET_KEY gesetzt) → alle Pakete ok, inkl. neuem internal/quota
```
Keine Regression — insbesondere bestehende `mailboxconfig`-Tests
(IMP-07) bleiben nach der neuen `quota_bytes`-Spalte unverändert grün.
## Ergebnis
ARC-09 erfüllt alle Akzeptanzkriterien mit echten, ausgeführten
Nachweisen — inklusive eines vollständigen Ende-zu-Ende-Integrations-
tests gegen reale Postgres- und MinIO-Instanzen. Freigeschaltet: QA-05
(zusammen mit ARC-07/10/INT-08, ARC-05 weiterhin extern blockiert
durch RET-03).