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.
70 lines
2.4 KiB
Go
70 lines
2.4 KiB
Go
// Package quota implementiert ARC-09: konfigurierbares Speicherlimit
|
|
// je einzelnem Postfach — eine ANDERE Ebene als die tenant-weite Quota
|
|
// aus Core LIC-05 (bekannter Fehler zu vermeiden: beide Ebenen müssen
|
|
// unabhängig durchgesetzt werden, ein Tenant kann insgesamt genug
|
|
// Kontingent haben, während ein einzelnes Postfach überläuft).
|
|
package quota
|
|
|
|
import "context"
|
|
|
|
// LimitProvider liefert das konfigurierte Speicherlimit eines
|
|
// Postfachs (Akzeptanzkriterium 1). configured=false bedeutet: kein
|
|
// Limit gesetzt, also unbegrenzt — erfüllt von
|
|
// mail/internal/mailboxconfig.Store.LimitBytes.
|
|
type LimitProvider interface {
|
|
LimitBytes(ctx context.Context, tenantSlug, mailbox string) (limitBytes int64, configured bool, err error)
|
|
}
|
|
|
|
// UsageProvider liefert den TATSÄCHLICHEN, real gemessenen
|
|
// Speicherverbrauch eines Postfachs — erfüllt von
|
|
// mail/internal/storage.UsageCounter.
|
|
type UsageProvider interface {
|
|
UsageBytes(ctx context.Context, tenantSlug, mailbox string) (usedBytes int64, err error)
|
|
}
|
|
|
|
// Result ist das Ergebnis einer Quota-Prüfung.
|
|
type Result struct {
|
|
Allowed bool
|
|
UsedBytes int64
|
|
LimitBytes int64
|
|
}
|
|
|
|
// Checker verknüpft konfiguriertes Limit und realen Verbrauch.
|
|
type Checker struct {
|
|
limits LimitProvider
|
|
usage UsageProvider
|
|
}
|
|
|
|
func NewChecker(limits LimitProvider, usage UsageProvider) *Checker {
|
|
return &Checker{limits: limits, usage: usage}
|
|
}
|
|
|
|
// Check prüft, ob mailbox innerhalb seines konfigurierten Limits ist.
|
|
// Kein konfiguriertes Limit bedeutet immer "erlaubt" — die
|
|
// Tenant-weite Quota (Core LIC-05) läuft unabhängig davon weiter.
|
|
func (c *Checker) Check(ctx context.Context, tenantSlug, mailbox string) (Result, error) {
|
|
limit, configured, err := c.limits.LimitBytes(ctx, tenantSlug, mailbox)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
if !configured {
|
|
return Result{Allowed: true}, nil
|
|
}
|
|
used, err := c.usage.UsageBytes(ctx, tenantSlug, mailbox)
|
|
if err != nil {
|
|
return Result{}, err
|
|
}
|
|
return Result{Allowed: used < limit, UsedBytes: used, LimitBytes: limit}, nil
|
|
}
|
|
|
|
// Allowed ist die schmale Form von Check für Aufrufer, die nur die
|
|
// Ja/Nein-Entscheidung brauchen (z. B. mail/internal/smtp.QuotaChecker,
|
|
// Akzeptanzkriterium 2).
|
|
func (c *Checker) Allowed(ctx context.Context, tenantSlug, mailbox string) (bool, error) {
|
|
result, err := c.Check(ctx, tenantSlug, mailbox)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return result.Allowed, nil
|
|
}
|