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.
76 lines
2.7 KiB
Go
76 lines
2.7 KiB
Go
// ARC-04: Aufbewahrungsstruktur nach Mandant/Postfach/Jahr. Mandant ist
|
|
// bereits durch den physisch getrennten Bucket (ARC-06) abgebildet —
|
|
// ArchiveKey deckt Postfach und Jahr INNERHALB des Buckets ab, als
|
|
// eigener, additiver Schlüssel-Konstruktor neben dem bereits
|
|
// bestehenden, flachen ObjectKey (ARC-01, weiterhin unverändert für
|
|
// bestehende Aufrufer wie mail/internal/mailapi — kein Umbau
|
|
// angrenzender Bereiche).
|
|
package storage
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Fallback-Segmente (Akzeptanzkriterium 3): fehlende/abweichende
|
|
// Metadaten führen zu einem DOKUMENTIERTEN Fallback-Pfad statt einem
|
|
// Ablagefehler.
|
|
const (
|
|
FallbackMailboxSegment = "postfach-unbekannt"
|
|
FallbackYearSegment = "jahr-unbekannt"
|
|
)
|
|
|
|
// ArchiveKey liefert den kanonischen Objektschlüssel für einen
|
|
// archivierten Mail-Anhang/-Teil nach dem Schema Postfach/Jahr
|
|
// (Akzeptanzkriterium 1) — innerhalb des bereits mandantenspezifischen
|
|
// Buckets. sentAt darf der Nullwert sein und mailbox leer
|
|
// (Akzeptanzkriterium 3): beides führt zum jeweiligen Fallback-Segment,
|
|
// nie zu einem Fehler.
|
|
func ArchiveKey(mailbox string, sentAt time.Time, messageID string, partIndex int) string {
|
|
return ArchiveYearPrefix(mailbox, yearOf(sentAt)) + ObjectKey(messageID, partIndex)
|
|
}
|
|
|
|
// ArchiveYearPrefix liefert den Verzeichnispräfix EINES Postfach-Jahrs
|
|
// (Akzeptanzkriterium 2: Grundlage für spätere Retention-Regeln OHNE
|
|
// Migration — ein Retention-Job kann alle Objekte eines Postfachs/
|
|
// Jahrs direkt über diesen Präfix auflisten, ohne die bereits
|
|
// abgelegten Schlüssel umzubenennen oder neu zu strukturieren).
|
|
func ArchiveYearPrefix(mailbox string, year int) string {
|
|
mailboxSegment := sanitizeSegment(mailbox)
|
|
if mailboxSegment == "" {
|
|
mailboxSegment = FallbackMailboxSegment
|
|
}
|
|
yearSegment := FallbackYearSegment
|
|
if year > 0 {
|
|
yearSegment = strconv.Itoa(year)
|
|
}
|
|
return "postfach/" + mailboxSegment + "/" + yearSegment + "/"
|
|
}
|
|
|
|
// ArchiveMailboxPrefix liefert den Präfix ALLER Jahre eines Postfachs
|
|
// (ohne Jahressegment) — Grundlage für eine reale
|
|
// Postfach-Verbrauchsmessung über alle Jahre hinweg (ARC-09).
|
|
func ArchiveMailboxPrefix(mailbox string) string {
|
|
mailboxSegment := sanitizeSegment(mailbox)
|
|
if mailboxSegment == "" {
|
|
mailboxSegment = FallbackMailboxSegment
|
|
}
|
|
return "postfach/" + mailboxSegment + "/"
|
|
}
|
|
|
|
func yearOf(t time.Time) int {
|
|
if t.IsZero() {
|
|
return 0
|
|
}
|
|
return t.UTC().Year()
|
|
}
|
|
|
|
// sanitizeSegment entfernt Pfadtrenner aus einem Postfachnamen, damit
|
|
// er nie versehentlich zusätzliche Verzeichnisebenen erzeugt (z. B. ein
|
|
// Postfachname, der ein "/" enthält).
|
|
func sanitizeSegment(raw string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
return strings.ReplaceAll(raw, "/", "_")
|
|
}
|