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.
94 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|