Files
nexarch/mail/internal/smtp/server.go
T
sysops 7c892ed10a 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.
2026-09-01 09:07:35 +02:00

75 lines
2.5 KiB
Go

package smtp
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log/slog"
"net"
)
// defaultMaxMessageBytes ist die Standard-Höchstgröße einer
// angenommenen Nachricht (Akzeptanzkriterium 1).
const defaultMaxMessageBytes = 25 * 1024 * 1024 // 25 MiB
// Server nimmt SMTP-Verbindungen an und bedient jede in einer eigenen
// Goroutine — gleiches Muster wie mail/internal/imap.Server und
// mail/internal/pop3.Server. tlsConfig ist optional (ING-06): nil
// bedeutet, STARTTLS ist nicht verfügbar. Rate-Limiting ist Sache von
// ING-09, Protokoll-Logging Sache von ING-08 — keine dieser Kacheln.
type Server struct {
sink MessageSink
maxMessageBytes int64
tlsConfig *tls.Config
logger *slog.Logger
}
func NewServer(sink MessageSink) *Server {
return NewServerWithMaxMessageBytes(sink, defaultMaxMessageBytes)
}
// NewServerWithMaxMessageBytes erlaubt eine abweichende
// Nachrichten-Höchstgröße, z. B. für Tests.
func NewServerWithMaxMessageBytes(sink MessageSink, maxMessageBytes int64) *Server {
return &Server{sink: sink, maxMessageBytes: maxMessageBytes}
}
// NewServerWithMaxMessageBytesAndTLSConfig erlaubt zusätzlich, STARTTLS
// zu aktivieren (ING-06). tlsConfig sollte über tlscert.Store.Config()
// erzeugt werden.
func NewServerWithMaxMessageBytesAndTLSConfig(sink MessageSink, maxMessageBytes int64, tlsConfig *tls.Config) *Server {
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig}
}
// NewServerWithMaxMessageBytesTLSAndLogger erlaubt zusätzlich
// strukturiertes Protokoll-Logging (ING-08). logger darf nil sein
// (Logging dann deaktiviert, Rückwärtskompatibilität zu ING-01..ING-06).
func NewServerWithMaxMessageBytesTLSAndLogger(sink MessageSink, maxMessageBytes int64, tlsConfig *tls.Config, logger *slog.Logger) *Server {
return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig, logger: logger}
}
// 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("smtp: verbindung annehmen: %w", err)
}
session := newSession(conn, srv.sink, srv.maxMessageBytes, srv.tlsConfig, srv.logger)
go session.Serve(ctx)
}
}