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:
@@ -7,6 +7,16 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"gitea.perlbach24.de/scripte/nexarch/mail/internal/protoguard"
|
||||
)
|
||||
|
||||
// phaseAuthorization/phaseTransaction sind die protoguard-Phasen dieser
|
||||
// Sitzung (ING-07 Akzeptanzkriterium 2: Timeouts pro Protokollphase
|
||||
// konfigurierbar).
|
||||
const (
|
||||
phaseAuthorization protoguard.Phase = "authorization"
|
||||
phaseTransaction protoguard.Phase = "transaction"
|
||||
)
|
||||
|
||||
// maxCommandLineBytes begrenzt eine einzelne Kommandozeile (defensive
|
||||
@@ -22,6 +32,7 @@ type Session struct {
|
||||
writer *bufio.Writer
|
||||
auth Authenticator
|
||||
store MailboxStore
|
||||
guard *protoguard.Guard
|
||||
|
||||
state State
|
||||
pendingUsername string // nach USER, vor erfolgreichem PASS
|
||||
@@ -29,18 +40,27 @@ type Session struct {
|
||||
deleted map[int]bool
|
||||
}
|
||||
|
||||
func newSession(conn net.Conn, auth Authenticator, store MailboxStore) *Session {
|
||||
func newSession(conn net.Conn, auth Authenticator, store MailboxStore, guardCfg protoguard.Config) *Session {
|
||||
return &Session{
|
||||
conn: conn,
|
||||
reader: bufio.NewReaderSize(conn, maxCommandLineBytes),
|
||||
writer: bufio.NewWriter(conn),
|
||||
auth: auth,
|
||||
store: store,
|
||||
guard: protoguard.New(guardCfg),
|
||||
state: Authorization,
|
||||
deleted: map[int]bool{},
|
||||
}
|
||||
}
|
||||
|
||||
// currentPhase liefert die protoguard-Phase des aktuellen Sitzungszustands.
|
||||
func (s *Session) currentPhase() protoguard.Phase {
|
||||
if s.state == Authorization {
|
||||
return phaseAuthorization
|
||||
}
|
||||
return phaseTransaction
|
||||
}
|
||||
|
||||
// State liefert den aktuellen Sitzungszustand (für Tests).
|
||||
func (s *Session) State() State { return s.state }
|
||||
|
||||
@@ -53,13 +73,23 @@ func (s *Session) Serve(ctx context.Context) {
|
||||
}
|
||||
|
||||
for {
|
||||
// Akzeptanzkriterium 2 (ING-07): Idle-Timeout pro Protokollphase,
|
||||
// vor jedem Lesevorgang neu gesetzt, da ein Zustandswechsel die
|
||||
// Phase (und damit den geltenden Timeout) ändern kann.
|
||||
if err := s.guard.ApplyReadDeadline(s.conn, s.currentPhase()); err != nil {
|
||||
return
|
||||
}
|
||||
line, err := s.readLine()
|
||||
if err != nil {
|
||||
// Verbindung endet OHNE QUIT — Akzeptanzkriterium/
|
||||
// Pflichtprüfung 3: als Deleted markierte Nachrichten dürfen
|
||||
// dadurch NICHT gelöscht werden. Da store.Delete nur im
|
||||
// regulären handleQuit aufgerufen wird, ist das hier bereits
|
||||
// strukturell garantiert (kein Aufruf, keine Löschung).
|
||||
// Verbindung endet OHNE QUIT (Timeout, Netzwerkabbruch oder
|
||||
// harter Verbindungsabbruch) — Akzeptanzkriterium 1: die
|
||||
// Session-Ressourcen (Verbindung, Reader/Writer) werden über
|
||||
// das defer conn.Close() oben zuverlässig freigegeben.
|
||||
// Zusätzlich Pflichtprüfung 3: als Deleted markierte
|
||||
// Nachrichten dürfen dadurch NICHT gelöscht werden. Da
|
||||
// store.Delete nur im regulären handleQuit aufgerufen wird,
|
||||
// ist das hier bereits strukturell garantiert (kein Aufruf,
|
||||
// keine Löschung).
|
||||
return
|
||||
}
|
||||
if line == "" {
|
||||
|
||||
Reference in New Issue
Block a user