feat(mail): ING-07 einheitliche Fehlerbehandlung & Wiederverbindung IMAP/POP3

Neues Paket mail/internal/protoguard kapselt die für IMAP- und
POP3-Sessions gemeinsam benötigte Timeout- und Backoff-Logik einer
einzelnen Verbindung:

- Pro Protokollphase konfigurierbarer Idle-Read-Timeout (POP3:
  Authorization/Transaction, IMAP: NotAuthenticated/Selected), vor
  jedem Lesevorgang neu gesetzt.
- Sich verdoppelnder Backoff bei wiederholten Anmeldefehlversuchen
  einer Verbindung (BackoffBase bis BackoffMax), Verbindungstrennung
  nach konfigurierbarer Höchstzahl statt Dauerschleife.

Server.NewServer bleibt unverändert (Standardkonfiguration);
NewServerWithGuardConfig erlaubt abweichende Werte. Ressourcenaufräumung
bei Verbindungsabbruch war bereits durch defer conn.Close() strukturell
gegeben — der Timeout sorgt dafür, dass dieser Pfad auch bei hängenden
oder böswilligen Gegenstellen zuverlässig erreicht wird.

Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt:
Chaos-Test mit 30 hart gekappten Verbindungen während aktiver
Übertragung (kein Goroutine-Leck), Timeout-Auslösung in jeder
Protokollphase beider Server, steigender Backoff mit definierter
Verbindungstrennung nach Höchstzahl an Fehlversuchen.

go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~24 Pakete)
regressionsfrei getestet.
This commit is contained in:
sysops
2026-09-01 00:51:52 +02:00
parent bd1f52648c
commit 16c4ad0075
10 changed files with 688 additions and 28 deletions
+11 -3
View File
@@ -44,11 +44,19 @@ func (s *Session) handlePass(ctx context.Context, cmd command) bool {
ok, err := s.auth.Authenticate(ctx, s.pendingUsername, cmd.Args[0])
if err != nil || !ok {
// Immer derselbe generische Text, egal ob unbekannter Nutzer,
// falsches Passwort oder interner Fehler (Akzeptanzkriterium 3).
return writeErr(s.writer, genericAuthFailure) == nil
// Backoff statt Dauerschleife bei wiederholten Fehlversuchen
// (Akzeptanzkriterium 3, ING-07). Immer derselbe generische Text,
// egal ob unbekannter Nutzer, falsches Passwort oder interner
// Fehler.
backoff, disconnect := s.guard.RecordAuthFailure()
s.guard.Wait(ctx, backoff)
if err := writeErr(s.writer, genericAuthFailure); err != nil {
return false
}
return !disconnect
}
s.guard.ResetAuthFailures()
s.username = s.pendingUsername
s.state = Transaction
return writeOK(s.writer, "maildrop locked and ready") == nil