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:
@@ -15,6 +15,15 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/ratelimit"
|
||||
)
|
||||
|
||||
// QuotaChecker prüft, ob ein Postfach noch innerhalb seines
|
||||
// konfigurierten Speicherlimits ist (ARC-09). Erfüllt von
|
||||
// *quota.Checker — als schmale Schnittstelle definiert, damit dieses
|
||||
// Paket quota nicht importieren muss (keine Kopplung an dessen
|
||||
// LimitProvider/UsageProvider-Abhängigkeiten).
|
||||
type QuotaChecker interface {
|
||||
Allowed(ctx context.Context, tenantSlug, mailbox string) (bool, error)
|
||||
}
|
||||
|
||||
// maxCommandLineBytes begrenzt eine einzelne Kommando-/DATA-Zeile
|
||||
// (defensive Fehlerbehandlung bei nicht-konformen Gegenstellen statt
|
||||
// optimistischem Parsing, gleiche Konvention wie mail/internal/imap und
|
||||
@@ -46,6 +55,7 @@ type Session struct {
|
||||
|
||||
tenantID string
|
||||
acceptLimiter *ratelimit.Limiter // ING-09, nil = kein Rate-Limiting
|
||||
quotaChecker QuotaChecker // ARC-09, nil = keine Quota-Prüfung
|
||||
|
||||
guard *protoguard.Guard // QA-04-Nachtrag zu ING-07
|
||||
|
||||
@@ -54,7 +64,7 @@ type Session struct {
|
||||
to []string
|
||||
}
|
||||
|
||||
func newSession(conn net.Conn, sink MessageSink, maxMessageBytes int64, tlsConfig *tls.Config, logger *slog.Logger, tenantID string, acceptLimiter *ratelimit.Limiter, guardCfg protoguard.Config) *Session {
|
||||
func newSession(conn net.Conn, sink MessageSink, maxMessageBytes int64, tlsConfig *tls.Config, logger *slog.Logger, tenantID string, acceptLimiter *ratelimit.Limiter, guardCfg protoguard.Config, quotaChecker QuotaChecker) *Session {
|
||||
_, alreadyTLS := conn.(*tls.Conn)
|
||||
return &Session{
|
||||
conn: conn,
|
||||
@@ -67,6 +77,7 @@ func newSession(conn net.Conn, sink MessageSink, maxMessageBytes int64, tlsConfi
|
||||
log: protolog.NewSessionLogger(logger, "smtp"),
|
||||
tenantID: tenantID,
|
||||
acceptLimiter: acceptLimiter,
|
||||
quotaChecker: quotaChecker,
|
||||
guard: protoguard.New(guardCfg),
|
||||
state: Greeting,
|
||||
}
|
||||
@@ -153,7 +164,7 @@ func (s *Session) dispatch(ctx context.Context, verb, arg string) bool {
|
||||
case "MAIL":
|
||||
return s.handleMailFrom(arg)
|
||||
case "RCPT":
|
||||
return s.handleRcptTo(arg)
|
||||
return s.handleRcptTo(ctx, arg)
|
||||
case "DATA":
|
||||
return s.handleData(ctx)
|
||||
case "RSET":
|
||||
|
||||
Reference in New Issue
Block a user