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:") resp := c.send(t, "RCPT TO:") 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:") if resp := c1.send(t, "RCPT TO:"); 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:") if resp := c2.send(t, "RCPT TO:"); 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:") if resp := c.send(t, "RCPT TO:"); code(resp) != "250" { t.Fatalf("erwartete 250 ohne quotachecker, habe: %q", resp) } }