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.
156 lines
4.7 KiB
Go
156 lines
4.7 KiB
Go
package smtp
|
|
|
|
import (
|
|
"context"
|
|
"net"
|
|
"strings"
|
|
"testing"
|
|
|
|
"gitea.perlbach24.de/scripte/nexarch/mail/internal/protoguard"
|
|
)
|
|
|
|
// fakeQuotaChecker ist ein In-Memory-Fake für QuotaChecker — reine
|
|
// Protokolltests brauchen keine echte Postgres-/S3-Instanz.
|
|
type fakeQuotaChecker struct {
|
|
blocked map[string]bool // mailbox -> am limit
|
|
}
|
|
|
|
func (f fakeQuotaChecker) Allowed(_ context.Context, _, mailbox string) (bool, error) {
|
|
return !f.blocked[mailbox], nil
|
|
}
|
|
|
|
func startQuotaTestServer(t *testing.T, sink MessageSink, checker QuotaChecker) (addr string, stop func()) {
|
|
t.Helper()
|
|
srv := NewServerWithMaxMessageBytesTLSLoggerRateLimitGuardAndQuota(
|
|
sink, defaultMaxMessageBytes, nil, nil, "mandant-arc09", nil, protoguard.DefaultConfig(), checker,
|
|
)
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("listener: %v", err)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := make(chan struct{})
|
|
go func() {
|
|
_ = srv.Serve(ctx, listener)
|
|
close(done)
|
|
}()
|
|
return listener.Addr().String(), func() {
|
|
cancel()
|
|
<-done
|
|
}
|
|
}
|
|
|
|
// TestRcptTo_QuotaExceededRejectedWithCorrectSMTPError ist die
|
|
// geforderte Pflichtprüfung 1 (ARC-09): Postfach-Quota erreicht, neue
|
|
// eingehende Mail wird mit korrekter SMTP-Fehlermeldung (552, RFC 5321
|
|
// "exceeded storage allocation") abgelehnt, NICHT stillschweigend
|
|
// verworfen.
|
|
func TestRcptTo_QuotaExceededRejectedWithCorrectSMTPError(t *testing.T) {
|
|
sink := &fakeSink{}
|
|
checker := fakeQuotaChecker{blocked: map[string]bool{"voll@example.com": true}}
|
|
addr, stop := startQuotaTestServer(t, sink, checker)
|
|
defer stop()
|
|
|
|
c := dial(t, addr)
|
|
defer c.close()
|
|
c.send(t, "EHLO client.example.com")
|
|
for {
|
|
line := c.readLine(t)
|
|
if strings.HasPrefix(line, "250 ") {
|
|
break
|
|
}
|
|
}
|
|
c.send(t, "MAIL FROM:<a@example.com>")
|
|
|
|
resp := c.send(t, "RCPT TO:<voll@example.com>")
|
|
if code(resp) != "552" {
|
|
t.Fatalf("erwartete 552 (quota überschritten), habe: %q", resp)
|
|
}
|
|
if !strings.Contains(resp, "quota") {
|
|
t.Fatalf("erwartete erkennbare quota-fehlermeldung, habe: %q", resp)
|
|
}
|
|
|
|
// Session bleibt funktionsfähig — kein Verbindungsabbruch, keine
|
|
// stillschweigende Verwerfung.
|
|
if resp := c.send(t, "NOOP"); code(resp) != "250" {
|
|
t.Fatalf("session nach quota-ablehnung nicht mehr funktionsfähig: %q", resp)
|
|
}
|
|
if sink.count() != 0 {
|
|
t.Fatalf("erwartete keine angenommene nachricht bei quota-ablehnung, habe %d", sink.count())
|
|
}
|
|
}
|
|
|
|
// TestRcptTo_OtherMailboxUnaffectedWhenOneAtLimit ist die geforderte
|
|
// Pflichtprüfung 2 (ARC-09): ein anderes Postfach desselben Tenants
|
|
// empfängt weiterhin normal, während eines am Limit ist.
|
|
func TestRcptTo_OtherMailboxUnaffectedWhenOneAtLimit(t *testing.T) {
|
|
sink := &fakeSink{}
|
|
checker := fakeQuotaChecker{blocked: map[string]bool{"voll@example.com": true}}
|
|
addr, stop := startQuotaTestServer(t, sink, checker)
|
|
defer stop()
|
|
|
|
// Erste, VOLLSTÄNDIGE Transaktion an das Postfach am Limit —
|
|
// abgelehnt.
|
|
c1 := dial(t, addr)
|
|
c1.send(t, "EHLO client.example.com")
|
|
for {
|
|
line := c1.readLine(t)
|
|
if strings.HasPrefix(line, "250 ") {
|
|
break
|
|
}
|
|
}
|
|
c1.send(t, "MAIL FROM:<a@example.com>")
|
|
if resp := c1.send(t, "RCPT TO:<voll@example.com>"); code(resp) != "552" {
|
|
t.Fatalf("erwartete 552 für volles postfach, habe: %q", resp)
|
|
}
|
|
c1.close()
|
|
|
|
// Zweite, unabhängige Transaktion an ein ANDERES Postfach desselben
|
|
// Tenants — muss vollständig normal durchlaufen.
|
|
c2 := dial(t, addr)
|
|
defer c2.close()
|
|
c2.send(t, "EHLO client.example.com")
|
|
for {
|
|
line := c2.readLine(t)
|
|
if strings.HasPrefix(line, "250 ") {
|
|
break
|
|
}
|
|
}
|
|
c2.send(t, "MAIL FROM:<a@example.com>")
|
|
if resp := c2.send(t, "RCPT TO:<frei@example.com>"); code(resp) != "250" {
|
|
t.Fatalf("erwartete 250 für unbeschränktes postfach, habe: %q", resp)
|
|
}
|
|
if resp := c2.send(t, "DATA"); code(resp) != "354" {
|
|
t.Fatalf("erwartete 354, habe: %q", resp)
|
|
}
|
|
if resp := c2.send(t, "Subject: test\r\n\r\nHallo\r\n."); code(resp) != "250" {
|
|
t.Fatalf("erwartete 250 nach abgeschlossener DATA, habe: %q", resp)
|
|
}
|
|
if sink.count() != 1 {
|
|
t.Fatalf("erwartete 1 angenommene nachricht für das freie postfach, habe %d", sink.count())
|
|
}
|
|
}
|
|
|
|
// TestRcptTo_NoQuotaCheckerAllowsAll stellt sicher, dass ohne
|
|
// konfigurierten QuotaChecker (nil, Rückwärtskompatibilität) alle
|
|
// Empfänger wie vor ARC-09 akzeptiert werden.
|
|
func TestRcptTo_NoQuotaCheckerAllowsAll(t *testing.T) {
|
|
sink := &fakeSink{}
|
|
addr, stop := startTestServer(t, sink, defaultMaxMessageBytes)
|
|
defer stop()
|
|
|
|
c := dial(t, addr)
|
|
defer c.close()
|
|
c.send(t, "EHLO client.example.com")
|
|
for {
|
|
line := c.readLine(t)
|
|
if strings.HasPrefix(line, "250 ") {
|
|
break
|
|
}
|
|
}
|
|
c.send(t, "MAIL FROM:<a@example.com>")
|
|
if resp := c.send(t, "RCPT TO:<beliebig@example.com>"); code(resp) != "250" {
|
|
t.Fatalf("erwartete 250 ohne quotachecker, habe: %q", resp)
|
|
}
|
|
}
|