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:
@@ -95,7 +95,7 @@ func (s *Session) handleMailFrom(arg string) bool {
|
||||
// handleRcptTo ist Teil des Envelope-Aufbaus (Akzeptanzkriterium 1):
|
||||
// jede Empfängeradresse wird vor der Annahme validiert; mehrere RCPT TO
|
||||
// sind erlaubt.
|
||||
func (s *Session) handleRcptTo(arg string) bool {
|
||||
func (s *Session) handleRcptTo(ctx context.Context, arg string) bool {
|
||||
if s.state != MailFromSet && s.state != RcptToSet {
|
||||
return s.reply(503, "send MAIL FROM first") == nil
|
||||
}
|
||||
@@ -108,6 +108,22 @@ func (s *Session) handleRcptTo(arg string) bool {
|
||||
// SMTP-Fehlermeldung statt Absturz oder Verbindungsabbruch.
|
||||
return s.reply(553, "invalid recipient address") == nil
|
||||
}
|
||||
if s.quotaChecker != nil {
|
||||
// ARC-09 Akzeptanzkriterium 2: ein Postfach am Limit lehnt neue
|
||||
// eingehende Mail mit klarer, protokollgerechter SMTP-Fehlermeldung
|
||||
// ab (552 = RFC 5321 "exceeded storage allocation") — an RCPT TO
|
||||
// geprüft, BEVOR die Nachricht überhaupt übertragen wird, statt
|
||||
// nach vollständigem DATA-Empfang zu verwerfen. addr (der
|
||||
// Empfänger) ist der Postfachbezug — dasselbe mailbox-Feld wie
|
||||
// mail/internal/storage.ArchiveKey/mail/internal/mailboxconfig.
|
||||
allowed, err := s.quotaChecker.Allowed(ctx, s.tenantID, addr)
|
||||
if err != nil {
|
||||
return s.reply(451, "unable to verify mailbox quota, try again later") == nil
|
||||
}
|
||||
if !allowed {
|
||||
return s.reply(552, "mailbox quota exceeded for "+addr) == nil
|
||||
}
|
||||
}
|
||||
s.to = append(s.to, addr)
|
||||
s.state = RcptToSet
|
||||
return s.reply(250, "OK") == nil
|
||||
|
||||
Reference in New Issue
Block a user