package pop3 import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "net" "gitea.perlbach24.de/scripte/nexarch/mail/internal/protoguard" "gitea.perlbach24.de/scripte/nexarch/mail/internal/ratelimit" ) // Server nimmt POP3-Verbindungen an und bedient jede in einer eigenen // Goroutine (Akzeptanzkriterium 1) — gleiches Muster wie // mail/internal/imap.Server. tlsConfig ist optional: nil bedeutet, STLS // ist nicht verfügbar und USER/PASS sind wie vor ING-06 uneingeschränkt // erlaubt (Klartextbetrieb, z. B. hinter einem separaten TLS-Terminator // oder in Tests); ist tlsConfig gesetzt, verlangt PASS zwingend STLS // oder eine bereits per implizitem TLS-Listener verschlüsselte // Verbindung (Akzeptanzkriterium 1). type Server struct { auth Authenticator store MailboxStore guardCfg protoguard.Config tlsConfig *tls.Config logger *slog.Logger // tenantID identifiziert diesen Server für das Rate-Limiting // (ING-09, Akzeptanzkriterium 3: je Mandant getrennt konfigurierbar) // — leer, wenn loginLimiter nil ist. tenantID string loginLimiter *ratelimit.Limiter } func NewServer(auth Authenticator, store MailboxStore) *Server { return NewServerWithGuardConfig(auth, store, protoguard.DefaultConfig()) } // NewServerWithGuardConfig erlaubt abweichende Phase-Timeouts und // Backoff-Parameter (ING-07), z. B. für Tests oder gehärtete // Betriebsumgebungen. func NewServerWithGuardConfig(auth Authenticator, store MailboxStore, guardCfg protoguard.Config) *Server { return &Server{auth: auth, store: store, guardCfg: guardCfg} } // NewServerWithGuardAndTLSConfig erlaubt zusätzlich, STLS/TLS zu // aktivieren (ING-06). tlsConfig sollte über tlscert.Store.Config() // erzeugt werden. func NewServerWithGuardAndTLSConfig(auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config) *Server { return &Server{auth: auth, store: store, guardCfg: guardCfg, tlsConfig: tlsConfig} } // NewServerWithGuardTLSAndLogger erlaubt zusätzlich strukturiertes // Protokoll-Logging (ING-08). logger darf nil sein (Logging dann // deaktiviert, Rückwärtskompatibilität zu ING-01..ING-07). func NewServerWithGuardTLSAndLogger(auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config, logger *slog.Logger) *Server { return &Server{auth: auth, store: store, guardCfg: guardCfg, tlsConfig: tlsConfig, logger: logger} } // NewServerWithGuardTLSLoggerAndRateLimit erlaubt zusätzlich // Rate-Limiting für PASS-Versuche (ING-09). loginLimiter darf nil sein // (Rate-Limiting dann deaktiviert, Rückwärtskompatibilität zu // ING-01..ING-08). tenantID identifiziert diesen Server gegenüber dem // Limiter (Akzeptanzkriterium 3). func NewServerWithGuardTLSLoggerAndRateLimit(auth Authenticator, store MailboxStore, guardCfg protoguard.Config, tlsConfig *tls.Config, logger *slog.Logger, tenantID string, loginLimiter *ratelimit.Limiter) *Server { return &Server{auth: auth, store: store, guardCfg: guardCfg, tlsConfig: tlsConfig, logger: logger, tenantID: tenantID, loginLimiter: loginLimiter} } // 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("pop3: verbindung annehmen: %w", err) } session := newSession(conn, srv.auth, srv.store, srv.guardCfg, srv.tlsConfig, srv.logger, srv.tenantID, srv.loginLimiter) go session.Serve(ctx) } }