403 lines
13 KiB
Go
403 lines
13 KiB
Go
// Package config loads the Mail Service process configuration from environment
|
|
// variables.
|
|
package config
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"galaxy/mail/internal/telemetry"
|
|
)
|
|
|
|
const (
|
|
shutdownTimeoutEnvVar = "MAIL_SHUTDOWN_TIMEOUT"
|
|
logLevelEnvVar = "MAIL_LOG_LEVEL"
|
|
|
|
internalHTTPAddrEnvVar = "MAIL_INTERNAL_HTTP_ADDR"
|
|
internalHTTPReadHeaderTimeoutEnvVar = "MAIL_INTERNAL_HTTP_READ_HEADER_TIMEOUT"
|
|
internalHTTPReadTimeoutEnvVar = "MAIL_INTERNAL_HTTP_READ_TIMEOUT"
|
|
internalHTTPIdleTimeoutEnvVar = "MAIL_INTERNAL_HTTP_IDLE_TIMEOUT"
|
|
|
|
redisAddrEnvVar = "MAIL_REDIS_ADDR"
|
|
redisUsernameEnvVar = "MAIL_REDIS_USERNAME"
|
|
redisPasswordEnvVar = "MAIL_REDIS_PASSWORD"
|
|
redisDBEnvVar = "MAIL_REDIS_DB"
|
|
redisTLSEnabledEnvVar = "MAIL_REDIS_TLS_ENABLED"
|
|
redisOperationTimeoutEnvVar = "MAIL_REDIS_OPERATION_TIMEOUT"
|
|
redisCommandStreamEnvVar = "MAIL_REDIS_COMMAND_STREAM"
|
|
redisAttemptScheduleEnvVar = "MAIL_REDIS_ATTEMPT_SCHEDULE_KEY"
|
|
redisDeadLetterPrefixEnvVar = "MAIL_REDIS_DEAD_LETTER_PREFIX"
|
|
|
|
smtpModeEnvVar = "MAIL_SMTP_MODE"
|
|
smtpAddrEnvVar = "MAIL_SMTP_ADDR"
|
|
smtpUsernameEnvVar = "MAIL_SMTP_USERNAME"
|
|
smtpPasswordEnvVar = "MAIL_SMTP_PASSWORD"
|
|
smtpFromEmailEnvVar = "MAIL_SMTP_FROM_EMAIL"
|
|
smtpFromNameEnvVar = "MAIL_SMTP_FROM_NAME"
|
|
smtpTimeoutEnvVar = "MAIL_SMTP_TIMEOUT"
|
|
smtpInsecureSkipVerifyEnvVar = "MAIL_SMTP_INSECURE_SKIP_VERIFY"
|
|
|
|
templateDirEnvVar = "MAIL_TEMPLATE_DIR"
|
|
|
|
attemptWorkerConcurrencyEnvVar = "MAIL_ATTEMPT_WORKER_CONCURRENCY"
|
|
streamBlockTimeoutEnvVar = "MAIL_STREAM_BLOCK_TIMEOUT"
|
|
operatorRequestTimeoutEnvVar = "MAIL_OPERATOR_REQUEST_TIMEOUT"
|
|
idempotencyTTLEnvVar = "MAIL_IDEMPOTENCY_TTL"
|
|
deliveryTTLEnvVar = "MAIL_DELIVERY_TTL"
|
|
attemptTTLEnvVar = "MAIL_ATTEMPT_TTL"
|
|
|
|
otelServiceNameEnvVar = "OTEL_SERVICE_NAME"
|
|
otelTracesExporterEnvVar = "OTEL_TRACES_EXPORTER"
|
|
otelMetricsExporterEnvVar = "OTEL_METRICS_EXPORTER"
|
|
otelExporterOTLPProtocolEnvVar = "OTEL_EXPORTER_OTLP_PROTOCOL"
|
|
otelExporterOTLPTracesProtocolEnvVar = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
|
|
otelExporterOTLPMetricsProtocolEnvVar = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"
|
|
otelStdoutTracesEnabledEnvVar = "MAIL_OTEL_STDOUT_TRACES_ENABLED"
|
|
otelStdoutMetricsEnabledEnvVar = "MAIL_OTEL_STDOUT_METRICS_ENABLED"
|
|
|
|
defaultShutdownTimeout = 5 * time.Second
|
|
defaultLogLevel = "info"
|
|
defaultInternalHTTPAddr = ":8080"
|
|
defaultReadHeaderTimeout = 2 * time.Second
|
|
defaultReadTimeout = 10 * time.Second
|
|
defaultIdleTimeout = time.Minute
|
|
defaultRedisDB = 0
|
|
defaultRedisOperationTimeout = 250 * time.Millisecond
|
|
defaultRedisCommandStream = "mail:delivery_commands"
|
|
defaultRedisAttemptScheduleKey = "mail:attempt_schedule"
|
|
defaultRedisDeadLetterPrefix = "mail:dead_letters:"
|
|
defaultSMTPMode = SMTPModeStub
|
|
defaultSMTPTimeout = 15 * time.Second
|
|
defaultTemplateDir = "templates"
|
|
defaultAttemptWorkerCount = 4
|
|
defaultStreamBlockTimeout = 2 * time.Second
|
|
defaultOperatorRequestTimeout = 5 * time.Second
|
|
defaultIdempotencyTTL = 7 * 24 * time.Hour
|
|
defaultDeliveryTTL = 30 * 24 * time.Hour
|
|
defaultAttemptTTL = 90 * 24 * time.Hour
|
|
defaultOTelServiceName = "galaxy-mail"
|
|
)
|
|
|
|
const (
|
|
// SMTPModeStub configures the deterministic in-process stub provider.
|
|
SMTPModeStub = "stub"
|
|
|
|
// SMTPModeSMTP configures the real SMTP-backed provider adapter.
|
|
SMTPModeSMTP = "smtp"
|
|
)
|
|
|
|
// Config stores the full Mail Service process configuration.
|
|
type Config struct {
|
|
// ShutdownTimeout bounds graceful shutdown of every long-lived component.
|
|
ShutdownTimeout time.Duration
|
|
|
|
// Logging configures the process-wide structured logger.
|
|
Logging LoggingConfig
|
|
|
|
// InternalHTTP configures the trusted internal HTTP listener.
|
|
InternalHTTP InternalHTTPConfig
|
|
|
|
// Redis configures the shared Redis client and Redis-owned keys used by the
|
|
// runnable service skeleton.
|
|
Redis RedisConfig
|
|
|
|
// SMTP configures the runtime mail provider mode and provider-specific
|
|
// connection details.
|
|
SMTP SMTPConfig
|
|
|
|
// Templates configures the filesystem-backed template catalog root.
|
|
Templates TemplateConfig
|
|
|
|
// AttemptWorkerConcurrency stores how many idle attempt workers the process
|
|
// starts.
|
|
AttemptWorkerConcurrency int
|
|
|
|
// StreamBlockTimeout stores the maximum Redis Streams blocking read window
|
|
// used by the future command consumer.
|
|
StreamBlockTimeout time.Duration
|
|
|
|
// OperatorRequestTimeout stores the future application-layer request budget
|
|
// for trusted operator handlers.
|
|
OperatorRequestTimeout time.Duration
|
|
|
|
// IdempotencyTTL stores the configured retention for idempotency records.
|
|
IdempotencyTTL time.Duration
|
|
|
|
// DeliveryTTL stores the configured retention for delivery records.
|
|
DeliveryTTL time.Duration
|
|
|
|
// AttemptTTL stores the configured retention for attempt and dead-letter
|
|
// records.
|
|
AttemptTTL time.Duration
|
|
|
|
// Telemetry configures the process-wide OpenTelemetry runtime.
|
|
Telemetry TelemetryConfig
|
|
}
|
|
|
|
// LoggingConfig configures the process-wide structured logger.
|
|
type LoggingConfig struct {
|
|
// Level stores the process log level accepted by log/slog.
|
|
Level string
|
|
}
|
|
|
|
// InternalHTTPConfig configures the trusted internal HTTP listener.
|
|
type InternalHTTPConfig struct {
|
|
// Addr stores the TCP listen address.
|
|
Addr string
|
|
|
|
// ReadHeaderTimeout bounds request-header reading.
|
|
ReadHeaderTimeout time.Duration
|
|
|
|
// ReadTimeout bounds reading one request.
|
|
ReadTimeout time.Duration
|
|
|
|
// IdleTimeout bounds how long keep-alive connections stay open.
|
|
IdleTimeout time.Duration
|
|
}
|
|
|
|
// Validate reports whether cfg stores a usable internal HTTP listener
|
|
// configuration.
|
|
func (cfg InternalHTTPConfig) Validate() error {
|
|
switch {
|
|
case strings.TrimSpace(cfg.Addr) == "":
|
|
return fmt.Errorf("internal HTTP addr must not be empty")
|
|
case !isTCPAddr(cfg.Addr):
|
|
return fmt.Errorf("internal HTTP addr %q must use host:port form", cfg.Addr)
|
|
case cfg.ReadHeaderTimeout <= 0:
|
|
return fmt.Errorf("internal HTTP read header timeout must be positive")
|
|
case cfg.ReadTimeout <= 0:
|
|
return fmt.Errorf("internal HTTP read timeout must be positive")
|
|
case cfg.IdleTimeout <= 0:
|
|
return fmt.Errorf("internal HTTP idle timeout must be positive")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// RedisConfig configures the shared Redis client used by the runnable process.
|
|
type RedisConfig struct {
|
|
// Addr stores the Redis network address.
|
|
Addr string
|
|
|
|
// Username stores the optional Redis ACL username.
|
|
Username string
|
|
|
|
// Password stores the optional Redis ACL password.
|
|
Password string
|
|
|
|
// DB stores the Redis logical database index.
|
|
DB int
|
|
|
|
// TLSEnabled reports whether TLS must be used for Redis connections.
|
|
TLSEnabled bool
|
|
|
|
// OperationTimeout bounds one Redis round trip including the startup PING.
|
|
OperationTimeout time.Duration
|
|
|
|
// CommandStream stores the configured Redis Streams key for async command
|
|
// intake.
|
|
CommandStream string
|
|
|
|
// AttemptScheduleKey stores the configured sorted-set key of scheduled
|
|
// attempts.
|
|
AttemptScheduleKey string
|
|
|
|
// DeadLetterPrefix stores the configured Redis key prefix of dead-letter
|
|
// entries.
|
|
DeadLetterPrefix string
|
|
}
|
|
|
|
// TLSConfig returns the conservative TLS configuration used by the Redis
|
|
// client when TLSEnabled is true.
|
|
func (cfg RedisConfig) TLSConfig() *tls.Config {
|
|
if !cfg.TLSEnabled {
|
|
return nil
|
|
}
|
|
|
|
return &tls.Config{MinVersion: tls.VersionTLS12}
|
|
}
|
|
|
|
// Validate reports whether cfg stores a usable Redis configuration.
|
|
func (cfg RedisConfig) Validate() error {
|
|
switch {
|
|
case strings.TrimSpace(cfg.Addr) == "":
|
|
return fmt.Errorf("redis addr must not be empty")
|
|
case !isTCPAddr(cfg.Addr):
|
|
return fmt.Errorf("redis addr %q must use host:port form", cfg.Addr)
|
|
case cfg.DB < 0:
|
|
return fmt.Errorf("redis db must not be negative")
|
|
case cfg.OperationTimeout <= 0:
|
|
return fmt.Errorf("redis operation timeout must be positive")
|
|
case strings.TrimSpace(cfg.CommandStream) == "":
|
|
return fmt.Errorf("redis command stream must not be empty")
|
|
case strings.TrimSpace(cfg.AttemptScheduleKey) == "":
|
|
return fmt.Errorf("redis attempt schedule key must not be empty")
|
|
case strings.TrimSpace(cfg.DeadLetterPrefix) == "":
|
|
return fmt.Errorf("redis dead-letter prefix must not be empty")
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// SMTPConfig configures the selected provider adapter.
|
|
type SMTPConfig struct {
|
|
// Mode selects the runtime provider implementation. Supported values are
|
|
// `stub` and `smtp`.
|
|
Mode string
|
|
|
|
// Addr stores the SMTP server address when Mode is `smtp`.
|
|
Addr string
|
|
|
|
// Username stores the optional SMTP authentication username.
|
|
Username string
|
|
|
|
// Password stores the optional SMTP authentication password.
|
|
Password string
|
|
|
|
// FromEmail stores the RFC 5322 single mailbox used as the envelope sender
|
|
// when Mode is `smtp`.
|
|
FromEmail string
|
|
|
|
// FromName stores the optional display name attached to FromEmail.
|
|
FromName string
|
|
|
|
// Timeout stores the maximum SMTP dial-and-send window.
|
|
Timeout time.Duration
|
|
|
|
// InsecureSkipVerify disables SMTP certificate verification. This is meant
|
|
// only for local development and black-box tests with self-signed capture
|
|
// servers.
|
|
InsecureSkipVerify bool
|
|
}
|
|
|
|
// Validate reports whether cfg stores a usable provider configuration.
|
|
func (cfg SMTPConfig) Validate() error {
|
|
switch cfg.Mode {
|
|
case SMTPModeStub:
|
|
return nil
|
|
case SMTPModeSMTP:
|
|
switch {
|
|
case strings.TrimSpace(cfg.Addr) == "":
|
|
return fmt.Errorf("smtp addr must not be empty")
|
|
case !isTCPAddr(cfg.Addr):
|
|
return fmt.Errorf("smtp addr %q must use host:port form", cfg.Addr)
|
|
case cfg.Timeout <= 0:
|
|
return fmt.Errorf("smtp timeout must be positive")
|
|
case strings.TrimSpace(cfg.Username) == "" && strings.TrimSpace(cfg.Password) != "":
|
|
return fmt.Errorf("smtp username and password must be configured together")
|
|
case strings.TrimSpace(cfg.Username) != "" && strings.TrimSpace(cfg.Password) == "":
|
|
return fmt.Errorf("smtp username and password must be configured together")
|
|
default:
|
|
return validateMailbox("smtp from email", cfg.FromEmail)
|
|
}
|
|
default:
|
|
return fmt.Errorf("smtp mode %q is unsupported", cfg.Mode)
|
|
}
|
|
}
|
|
|
|
// TemplateConfig configures the filesystem-backed template catalog.
|
|
type TemplateConfig struct {
|
|
// Dir stores the root directory of the template catalog.
|
|
Dir string
|
|
}
|
|
|
|
// TelemetryConfig configures the Mail Service OpenTelemetry runtime.
|
|
type TelemetryConfig struct {
|
|
// ServiceName overrides the default OpenTelemetry service name.
|
|
ServiceName string
|
|
|
|
// TracesExporter selects the external traces exporter. Supported values are
|
|
// `none` and `otlp`.
|
|
TracesExporter string
|
|
|
|
// MetricsExporter selects the external metrics exporter. Supported values
|
|
// are `none` and `otlp`.
|
|
MetricsExporter string
|
|
|
|
// TracesProtocol selects the OTLP traces protocol when TracesExporter is
|
|
// `otlp`.
|
|
TracesProtocol string
|
|
|
|
// MetricsProtocol selects the OTLP metrics protocol when MetricsExporter is
|
|
// `otlp`.
|
|
MetricsProtocol string
|
|
|
|
// StdoutTracesEnabled enables the additional stdout trace exporter used for
|
|
// local development and debugging.
|
|
StdoutTracesEnabled bool
|
|
|
|
// StdoutMetricsEnabled enables the additional stdout metric exporter used
|
|
// for local development and debugging.
|
|
StdoutMetricsEnabled bool
|
|
}
|
|
|
|
// Validate reports whether cfg stores a usable template catalog root.
|
|
func (cfg TemplateConfig) Validate() error {
|
|
if strings.TrimSpace(cfg.Dir) == "" {
|
|
return fmt.Errorf("template dir must not be empty")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DefaultConfig returns the default Mail Service process configuration.
|
|
func DefaultConfig() Config {
|
|
return Config{
|
|
ShutdownTimeout: defaultShutdownTimeout,
|
|
Logging: LoggingConfig{
|
|
Level: defaultLogLevel,
|
|
},
|
|
InternalHTTP: InternalHTTPConfig{
|
|
Addr: defaultInternalHTTPAddr,
|
|
ReadHeaderTimeout: defaultReadHeaderTimeout,
|
|
ReadTimeout: defaultReadTimeout,
|
|
IdleTimeout: defaultIdleTimeout,
|
|
},
|
|
Redis: RedisConfig{
|
|
DB: defaultRedisDB,
|
|
OperationTimeout: defaultRedisOperationTimeout,
|
|
CommandStream: defaultRedisCommandStream,
|
|
AttemptScheduleKey: defaultRedisAttemptScheduleKey,
|
|
DeadLetterPrefix: defaultRedisDeadLetterPrefix,
|
|
},
|
|
SMTP: SMTPConfig{
|
|
Mode: defaultSMTPMode,
|
|
Timeout: defaultSMTPTimeout,
|
|
},
|
|
Templates: TemplateConfig{
|
|
Dir: defaultTemplateDir,
|
|
},
|
|
AttemptWorkerConcurrency: defaultAttemptWorkerCount,
|
|
StreamBlockTimeout: defaultStreamBlockTimeout,
|
|
OperatorRequestTimeout: defaultOperatorRequestTimeout,
|
|
IdempotencyTTL: defaultIdempotencyTTL,
|
|
DeliveryTTL: defaultDeliveryTTL,
|
|
AttemptTTL: defaultAttemptTTL,
|
|
Telemetry: TelemetryConfig{
|
|
ServiceName: defaultOTelServiceName,
|
|
TracesExporter: "none",
|
|
MetricsExporter: "none",
|
|
TracesProtocol: "",
|
|
MetricsProtocol: "",
|
|
StdoutTracesEnabled: false,
|
|
StdoutMetricsEnabled: false,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Validate reports whether cfg contains a supported OpenTelemetry
|
|
// configuration.
|
|
func (cfg TelemetryConfig) Validate() error {
|
|
return telemetry.ProcessConfig{
|
|
ServiceName: cfg.ServiceName,
|
|
TracesExporter: cfg.TracesExporter,
|
|
MetricsExporter: cfg.MetricsExporter,
|
|
TracesProtocol: cfg.TracesProtocol,
|
|
MetricsProtocol: cfg.MetricsProtocol,
|
|
StdoutTracesEnabled: cfg.StdoutTracesEnabled,
|
|
StdoutMetricsEnabled: cfg.StdoutMetricsEnabled,
|
|
}.Validate()
|
|
}
|