package smtp import ( "context" "crypto/tls" "errors" "fmt" "log/slog" "net" "gitea.perlbach24.de/scripte/nexarch/mail/internal/ratelimit" ) // 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 tenantID string acceptLimiter *ratelimit.Limiter } 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} } // NewServerWithMaxMessageBytesTLSLoggerAndRateLimit erlaubt zusätzlich // Rate-Limiting der SMTP-Annahmerate pro Absender (ING-09, MAIL FROM). // acceptLimiter darf nil sein (Rate-Limiting dann deaktiviert). // tenantID identifiziert diesen Server gegenüber dem Limiter // (Akzeptanzkriterium 3). func NewServerWithMaxMessageBytesTLSLoggerAndRateLimit(sink MessageSink, maxMessageBytes int64, tlsConfig *tls.Config, logger *slog.Logger, tenantID string, acceptLimiter *ratelimit.Limiter) *Server { return &Server{sink: sink, maxMessageBytes: maxMessageBytes, tlsConfig: tlsConfig, logger: logger, tenantID: tenantID, acceptLimiter: acceptLimiter} } // 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, srv.tenantID, srv.acceptLimiter) go session.Serve(ctx) } }