feat(mail): ING-08 strukturiertes Protokoll-Logging & Diagnose für IMAP/POP3/SMTP

Neues Paket mail/internal/protolog (log/slog): SessionLogger loggt
strukturierte Ereignisse einer Verbindung mit fester correlation_id und
protocol über die gesamte Verbindungsdauer (Akzeptanzkriterium 1) — ein
Logger mit logger==nil ist sicher benutzbar und loggt nichts
(Rückwärtskompatibilität zu ING-01..ING-07, Logging ist opt-in wie TLS
und Guard-Konfiguration). RedactCommandLine ersetzt bei sensiblen
Kommandos (PASS, LOGIN, AUTH) alle Argumente vollständig durch
[REDACTED] statt einzeln zu parsen (Akzeptanzkriterium 2).
Reconstruct liest zeilenweise JSON-Logs und liefert ausschließlich die
Einträge einer Korrelations-ID in Reihenfolge — das geforderte
Diagnosewerkzeug (Akzeptanzkriterium 3).

Alle drei Sessions loggen jetzt session_start/command (je empfangener
Zeile, redigiert)/session_end. Nachrichteninhalte werden strukturell
nie geloggt: SMTP-DATA-Body-Zeilen laufen durch eine eigene
Leseschleife, die nicht durch den Kommando-Logpfad der Hauptschleife
kommt: nur das Kommando DATA selbst erscheint im Log.

Alle drei Pflichtprüfungen mit echten Nachweisen durchgeführt, jeweils
in IMAP, POP3 und SMTP einzeln: Redaktion gegen den echten laufenden
Server bestätigt (Klartextpasswort bzw. absichtlich eingebettetes
Geheimnis im SMTP-Body erscheint nie im Log), zwei gemischte reale
Sessions über dieselbe Korrelations-ID lückenlos rekonstruiert,
Lasttest mit 100 Sessions mit/ohne Logging ohne relevante
Durchsatzeinbuße.

go build/go vet/golangci-lint clean, gesamtes Mail-Modul (~29 Pakete)
regressionsfrei getestet.
This commit is contained in:
sysops
2026-09-01 09:07:35 +02:00
parent b22ab67bb2
commit 7c892ed10a
14 changed files with 930 additions and 6 deletions
+17 -1
View File
@@ -6,10 +6,12 @@ import (
"crypto/tls"
"errors"
"io"
"log/slog"
"net"
"strings"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/protoguard"
"gitea.perlbach24.de/scripte/nexarch/mail/internal/protolog"
)
// phaseAuthorization/phaseTransaction sind die protoguard-Phasen dieser
@@ -42,13 +44,15 @@ type Session struct {
tlsConfig *tls.Config
tlsActive bool
log *protolog.SessionLogger // ING-08, nie nil (aber log.Event() ist nil-sicher)
state State
pendingUsername string // nach USER, vor erfolgreichem PASS
username string // nach erfolgreichem PASS
deleted map[int]bool
}
func newSession(conn net.Conn, auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config) *Session {
func newSession(conn net.Conn, auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config, logger *slog.Logger) *Session {
_, alreadyTLS := conn.(*tls.Conn)
return &Session{
conn: conn,
@@ -59,6 +63,7 @@ func newSession(conn net.Conn, auth Authenticator, store MailboxStore, guardCfg
guard: protoguard.New(guardCfg),
tlsConfig: tlsConfig,
tlsActive: alreadyTLS,
log: protolog.NewSessionLogger(logger, "pop3"),
state: Authorization,
deleted: map[int]bool{},
}
@@ -79,6 +84,11 @@ func (s *Session) State() State { return s.state }
func (s *Session) Serve(ctx context.Context) {
defer func() { _ = s.conn.Close() }()
// Akzeptanzkriterium 1 (ING-08): strukturierte Logs mit
// Korrelations-ID über die gesamte Verbindungsdauer.
s.log.Event(ctx, "session_start", slog.String("remote_addr", s.conn.RemoteAddr().String()))
defer s.log.Event(ctx, "session_end")
if err := writeOK(s.writer, "POP3 server ready"); err != nil {
return
}
@@ -115,6 +125,12 @@ func (s *Session) Serve(ctx context.Context) {
continue
}
// Akzeptanzkriterium 2 (ING-08): Zugangsdaten (PASS-Argument)
// erscheinen über RedactCommandLine nie im Klartext im Log.
// Nachrichteninhalte werden hier grundsätzlich nicht geloggt —
// RETR/LIST-Antworten sind kein Bestandteil dieses Ereignisses.
s.log.Event(ctx, "command", slog.String("command", protolog.RedactCommandLine(cmd.Name, cmd.Args)))
if !s.dispatch(ctx, cmd) {
return
}