// Package protoguard bündelt die Fehlerbehandlungs- und // Wiederverbindungslogik, die IMAP- und POP3-Sessions gemeinsam // brauchen (ING-07): pro Protokollphase konfigurierbare Idle-Timeouts // und Backoff statt Dauerschleife bei wiederholten Anmeldefehlern. // Ressourcenaufräumung selbst passiert bereits strukturell durch // defer conn.Close() in den Sessions — Guard sorgt dafür, dass dieser // Pfad auch bei hängenden oder böswilligen Gegenstellen zuverlässig // erreicht wird. package protoguard import ( "context" "net" "time" ) // Phase identifiziert eine Protokollphase, für die ein eigener // Idle-Timeout gilt. type Phase string // Config steuert Timeout- und Backoff-Verhalten einer Verbindung. type Config struct { // PhaseTimeout liefert den Idle-Timeout je Phase. Fehlt ein Eintrag, // gilt DefaultTimeout. PhaseTimeout map[Phase]time.Duration // DefaultTimeout gilt, wenn für die aktuelle Phase kein eigener Wert // gesetzt ist. 0 bedeutet: kein Timeout. DefaultTimeout time.Duration // MaxAuthFailures ist die Anzahl fehlgeschlagener Anmeldeversuche, // nach der eine Verbindung getrennt wird. 0 bedeutet: unbegrenzt // (kein Trennen, nur Backoff). MaxAuthFailures int // BackoffBase ist die Wartezeit vor der Antwort nach dem ersten // Fehlversuch, verdoppelt sich je weiterem Fehlversuch bis // BackoffMax. BackoffBase time.Duration BackoffMax time.Duration } // DefaultConfig liefert praxistaugliche Werte für Produktionsbetrieb. func DefaultConfig() Config { return Config{ DefaultTimeout: 5 * time.Minute, MaxAuthFailures: 5, BackoffBase: 200 * time.Millisecond, BackoffMax: 5 * time.Second, } } // Guard kapselt den Fehlerbehandlungszustand EINER Verbindung: aktuell // angewandte Phase-Timeouts und Zahl der Anmeldefehlversuche. type Guard struct { cfg Config authFailures int } // New erstellt einen Guard für eine einzelne Session. func New(cfg Config) *Guard { return &Guard{cfg: cfg} } // ApplyReadDeadline setzt die Lese-Deadline von conn passend zur // angegebenen Protokollphase (Akzeptanzkriterium 2). func (g *Guard) ApplyReadDeadline(conn net.Conn, phase Phase) error { d := g.cfg.DefaultTimeout if pd, ok := g.cfg.PhaseTimeout[phase]; ok { d = pd } if d <= 0 { return conn.SetReadDeadline(time.Time{}) } return conn.SetReadDeadline(time.Now().Add(d)) } // RecordAuthFailure zählt einen fehlgeschlagenen Anmeldeversuch dieser // Verbindung und liefert die Backoff-Wartezeit vor der Fehlerantwort // sowie ob die Verbindung danach getrennt werden muss (Akzeptanzkriterium // 3: klar definierter Backoff statt Dauerschleife). func (g *Guard) RecordAuthFailure() (backoff time.Duration, disconnect bool) { g.authFailures++ backoff = g.backoffFor(g.authFailures) disconnect = g.cfg.MaxAuthFailures > 0 && g.authFailures >= g.cfg.MaxAuthFailures return backoff, disconnect } // ResetAuthFailures setzt den Fehlversuchszähler nach erfolgreicher // Anmeldung zurück. func (g *Guard) ResetAuthFailures() { g.authFailures = 0 } func (g *Guard) backoffFor(failures int) time.Duration { if g.cfg.BackoffBase <= 0 { return 0 } d := g.cfg.BackoffBase for i := 1; i < failures; i++ { d *= 2 if g.cfg.BackoffMax > 0 && d >= g.cfg.BackoffMax { return g.cfg.BackoffMax } } if g.cfg.BackoffMax > 0 && d > g.cfg.BackoffMax { return g.cfg.BackoffMax } return d } // Wait wartet d, bricht aber bei ctx-Abbruch sofort ab, damit ein // Server-Shutdown nicht auf eine laufende Backoff-Pause warten muss. func (g *Guard) Wait(ctx context.Context, d time.Duration) { if d <= 0 { return } timer := time.NewTimer(d) defer timer.Stop() select { case <-timer.C: case <-ctx.Done(): } }