Files
nexarch/mail/internal/pop3/server.go
T
sysops af1709a2bb feat(mail): ING-09 Rate-Limiting auf Protokollebene für IMAP/POP3/SMTP
Neues Paket mail/internal/ratelimit: Token-Bucket je (Mandant,
Quelle)-Schlüssel. ConfigProvider/StaticConfig liefern Burst/Nachfüllrate
je Mandant mit Fallback auf eine Default-Konfiguration
(Akzeptanzkriterium 2/3: begrenzt, konfigurierbar, je Mandant getrennt).
Limiter.Allow liefert bei Ablehnung eine konkrete positive Wartezeit
statt nur false — Grundlage für die geforderte definierte Ablehnung MIT
Wartezeit-Hinweis statt Verbindungsabbruch (Akzeptanzkriterium 3).

IMAP LOGIN und POP3 PASS begrenzen Anmeldeversuche pro (Mandant,
Quell-IP). SMTP MAIL FROM begrenzt die Annahmerate pro (Mandant,
Absenderadresse+Quell-IP). Bei Überschreitung: POP3 -ERR, IMAP NO, SMTP
451 (temporärer Fehlercode) mit Wartezeit in Sekunden, Verbindung
bleibt in allen drei Fällen offen und weiter nutzbar. loginLimiter/
acceptLimiter sind optional (nil = deaktiviert, Rückwärtskompatibilität
zu ING-01..ING-08) — neue Konstruktoren
NewServerWithGuardTLSLoggerAndRateLimit (IMAP/POP3) und
NewServerWithMaxMessageBytesTLSLoggerAndRateLimit (SMTP). Jeder Server
bekommt eine tenantID, konsistent mit dem in ING-10 etablierten Muster
"ein Server je Mandant".

Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt, jeweils
in IMAP, POP3 und SMTP einzeln: 20 reale Versuche bei Burst=5 -> exakt
5 akzeptiert, 15 mit Wartezeit-Hinweis abgelehnt, keine Verbindung
getrennt; legitime Nutzung unterhalb der Schwelle unbeeinträchtigt;
zwei Mandanten mit unterschiedlichem Burst über denselben Limiter
unabhängig und exakt wirksam (2 vs. 8 akzeptierte Versuche).

go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~31 Pakete)
regressionsfrei getestet — insbesondere QA-07-Lasttests bleiben grün,
da Rate-Limiting standardmäßig deaktiviert ist.
2026-09-01 12:22:21 +02:00

94 lines
3.6 KiB
Go

package pop3
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/protoguard"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/ratelimit"
)
// Server nimmt POP3-Verbindungen an und bedient jede in einer eigenen
// Goroutine (Akzeptanzkriterium 1) — gleiches Muster wie
// mail/internal/imap.Server. tlsConfig ist optional: nil bedeutet, STLS
// ist nicht verfügbar und USER/PASS sind wie vor ING-06 uneingeschränkt
// erlaubt (Klartextbetrieb, z. B. hinter einem separaten TLS-Terminator
// oder in Tests); ist tlsConfig gesetzt, verlangt PASS zwingend STLS
// oder eine bereits per implizitem TLS-Listener verschlüsselte
// Verbindung (Akzeptanzkriterium 1).
type Server struct {
auth Authenticator
store MailboxStore
guardCfg protoguard.Config
tlsConfig *tls.Config
logger *slog.Logger
// tenantID identifiziert diesen Server für das Rate-Limiting
// (ING-09, Akzeptanzkriterium 3: je Mandant getrennt konfigurierbar)
// — leer, wenn loginLimiter nil ist.
tenantID string
loginLimiter *ratelimit.Limiter
}
func NewServer(auth Authenticator, store MailboxStore) *Server {
return NewServerWithGuardConfig(auth, store, protoguard.DefaultConfig())
}
// NewServerWithGuardConfig erlaubt abweichende Phase-Timeouts und
// Backoff-Parameter (ING-07), z. B. für Tests oder gehärtete
// Betriebsumgebungen.
func NewServerWithGuardConfig(auth Authenticator, store MailboxStore, guardCfg protoguard.Config) *Server {
return &Server{auth: auth, store: store, guardCfg: guardCfg}
}
// NewServerWithGuardAndTLSConfig erlaubt zusätzlich, STLS/TLS zu
// aktivieren (ING-06). tlsConfig sollte über tlscert.Store.Config()
// erzeugt werden.
func NewServerWithGuardAndTLSConfig(auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config) *Server {
return &Server{auth: auth, store: store, guardCfg: guardCfg, tlsConfig: tlsConfig}
}
// NewServerWithGuardTLSAndLogger erlaubt zusätzlich strukturiertes
// Protokoll-Logging (ING-08). logger darf nil sein (Logging dann
// deaktiviert, Rückwärtskompatibilität zu ING-01..ING-07).
func NewServerWithGuardTLSAndLogger(auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config, logger *slog.Logger) *Server {
return &Server{auth: auth, store: store, guardCfg: guardCfg, tlsConfig: tlsConfig, logger: logger}
}
// NewServerWithGuardTLSLoggerAndRateLimit erlaubt zusätzlich
// Rate-Limiting für PASS-Versuche (ING-09). loginLimiter darf nil sein
// (Rate-Limiting dann deaktiviert, Rückwärtskompatibilität zu
// ING-01..ING-08). tenantID identifiziert diesen Server gegenüber dem
// Limiter (Akzeptanzkriterium 3).
func NewServerWithGuardTLSLoggerAndRateLimit(auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config, logger *slog.Logger, tenantID string, loginLimiter *ratelimit.Limiter) *Server {
return &Server{auth: auth, store: store, guardCfg: guardCfg, tlsConfig: tlsConfig, logger: logger, tenantID: tenantID, loginLimiter: loginLimiter}
}
// Serve nimmt Verbindungen auf listener an, bis ctx beendet wird.
func (srv *Server) Serve(ctx context.Context, listener net.Listener) error {
go func() {
<-ctx.Done()
_ = listener.Close()
}()
for {
conn, err := listener.Accept()
if err != nil {
if ctx.Err() != nil {
return nil
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
continue
}
return fmt.Errorf("pop3: verbindung annehmen: %w", err)
}
session := newSession(conn, srv.auth, srv.store, srv.guardCfg, srv.tlsConfig, srv.logger, srv.tenantID, srv.loginLimiter)
go session.Serve(ctx)
}
}