package config import ( "fmt" "net" "os" "strconv" "strings" "time" ) type Config struct { Listen string PublicBaseURL string SMTP SMTPConfig Nym NymConfig Security SecurityConfig } type SMTPConfig struct { Host string Port int Recipient string EnvelopeFrom string HELO string TLSServerName string RequireTLS bool ImplicitTLS bool Timeout time.Duration DryRun bool } type NymConfig struct { Enabled bool Managed bool Binary string Provider string HomeDir string ClientID string SocksAddr string AnonymousReplies bool StartupTimeout time.Duration } type SecurityConfig struct { MinHashcashBits int MinMessageBytes int MaxMessageBytes int MaxNewsgroups int RateLimitCount int RateLimitWindow time.Duration TrustProxy bool SecureCookies bool MessageIDDomain string IdenticonsCLI string RequireFace bool } func Load() (Config, error) { cfg := Config{ Listen: env("N2U_LISTEN", "M2U_LISTEN", "127.0.0.1:8095"), PublicBaseURL: env("N2U_PUBLIC_BASE_URL", "M2U_PUBLIC_BASE_URL", "https://n2usenet.virebent.art"), SMTP: SMTPConfig{ Host: env("N2U_SMTP_HOST", "M2U_SMTP_HOST", "mail2news.tcpreset.net"), Port: envInt("N2U_SMTP_PORT", "M2U_SMTP_PORT", 25), Recipient: env("N2U_SMTP_RECIPIENT", "M2U_SMTP_RECIPIENT", "mail2news@mail2news.tcpreset.net"), EnvelopeFrom: env("N2U_SMTP_ENVELOPE_FROM", "M2U_SMTP_ENVELOPE_FROM", "n2usenet@virebent.art"), HELO: env("N2U_SMTP_HELO", "M2U_SMTP_HELO", "n2usenet.virebent.art"), TLSServerName: env("N2U_SMTP_TLS_SERVER_NAME", "M2U_SMTP_TLS_SERVER_NAME", "mail.tcpreset.net"), RequireTLS: envBool("N2U_SMTP_REQUIRE_TLS", "M2U_SMTP_REQUIRE_TLS", true), ImplicitTLS: envBool("N2U_SMTP_IMPLICIT_TLS", "M2U_SMTP_IMPLICIT_TLS", false), Timeout: envDuration("N2U_SMTP_TIMEOUT", "M2U_SMTP_TIMEOUT", 90*time.Second), DryRun: envBool("N2U_DRY_RUN", "M2U_DRY_RUN", false), }, Nym: NymConfig{ Enabled: envBool("N2U_NYM_ENABLED", "M2U_NYM_ENABLED", true), Managed: envBool("N2U_NYM_MANAGED", "M2U_NYM_MANAGED", false), Binary: env("N2U_NYM_BINARY", "M2U_NYM_BINARY", "nym-socks5-client"), Provider: env("N2U_NYM_PROVIDER", "M2U_NYM_PROVIDER", ""), HomeDir: env("N2U_NYM_HOME", "M2U_NYM_HOME", "./data/nym"), ClientID: env("N2U_NYM_CLIENT_ID", "M2U_NYM_CLIENT_ID", "n2usenet"), SocksAddr: env("N2U_NYM_SOCKS", "M2U_NYM_SOCKS", "127.0.0.1:11080"), AnonymousReplies: envBool("N2U_NYM_ANONYMOUS_REPLIES", "M2U_NYM_ANONYMOUS_REPLIES", true), StartupTimeout: envDuration("N2U_NYM_STARTUP_TIMEOUT", "M2U_NYM_STARTUP_TIMEOUT", 120*time.Second), }, Security: SecurityConfig{ MinHashcashBits: envInt("N2U_HASHCASH_MIN_BITS", "M2U_HASHCASH_MIN_BITS", 20), MinMessageBytes: envInt("N2U_MIN_MESSAGE_BYTES", "M2U_MIN_MESSAGE_BYTES", 10), MaxMessageBytes: envInt("N2U_MAX_MESSAGE_BYTES", "M2U_MAX_MESSAGE_BYTES", 65536), MaxNewsgroups: envInt("N2U_MAX_NEWSGROUPS", "M2U_MAX_NEWSGROUPS", 3), RateLimitCount: envInt("N2U_RATE_LIMIT_COUNT", "M2U_RATE_LIMIT_COUNT", 10), RateLimitWindow: envDuration("N2U_RATE_LIMIT_WINDOW", "M2U_RATE_LIMIT_WINDOW", time.Hour), TrustProxy: envBool("N2U_TRUST_PROXY", "M2U_TRUST_PROXY", true), SecureCookies: envBool("N2U_SECURE_COOKIES", "M2U_SECURE_COOKIES", true), MessageIDDomain: env("N2U_MESSAGE_ID_DOMAIN", "M2U_MESSAGE_ID_DOMAIN", "n2usenet.virebent.art"), IdenticonsCLI: env("N2U_IDENTICONS_CLI", "M2U_IDENTICONS_CLI", "/usr/local/bin/identicons-cli"), RequireFace: envBool("N2U_REQUIRE_FACE", "M2U_REQUIRE_FACE", true), }, } if err := cfg.Validate(); err != nil { return Config{}, err } return cfg, nil } func (c Config) Validate() error { if _, _, err := net.SplitHostPort(c.Listen); err != nil { return fmt.Errorf("invalid N2U_LISTEN: %w", err) } if c.SMTP.Host == "" { return fmt.Errorf("N2U_SMTP_HOST is required") } if c.SMTP.Port < 1 || c.SMTP.Port > 65535 { return fmt.Errorf("invalid N2U_SMTP_PORT") } if c.SMTP.Recipient == "" { return fmt.Errorf("N2U_SMTP_RECIPIENT is required") } if c.SMTP.EnvelopeFrom == "" { return fmt.Errorf("N2U_SMTP_ENVELOPE_FROM is required") } if c.SMTP.RequireTLS && c.SMTP.TLSServerName == "" { return fmt.Errorf("N2U_SMTP_TLS_SERVER_NAME is required when TLS is required") } if c.Nym.Enabled { if _, _, err := net.SplitHostPort(c.Nym.SocksAddr); err != nil { return fmt.Errorf("invalid N2U_NYM_SOCKS: %w", err) } if c.Nym.Managed && c.Nym.Provider == "" { return fmt.Errorf("N2U_NYM_PROVIDER is required when N2U_NYM_MANAGED=true") } } if c.Security.MinHashcashBits < 1 || c.Security.MinHashcashBits > 32 { return fmt.Errorf("invalid N2U_HASHCASH_MIN_BITS") } if c.Security.MinMessageBytes < 0 || c.Security.MaxMessageBytes <= c.Security.MinMessageBytes { return fmt.Errorf("invalid message size limits") } if c.Security.MaxNewsgroups < 1 || c.Security.MaxNewsgroups > 10 { return fmt.Errorf("invalid N2U_MAX_NEWSGROUPS") } if c.Security.RateLimitCount < 1 { return fmt.Errorf("invalid N2U_RATE_LIMIT_COUNT") } if c.Security.RequireFace && c.Security.IdenticonsCLI == "" { return fmt.Errorf("N2U_IDENTICONS_CLI is required when N2U_REQUIRE_FACE=true") } return nil } func env(primary, legacy, fallback string) string { v, ok := envValue(primary, legacy) if !ok { return fallback } return v } func envBool(primary, legacy string, fallback bool) bool { v, ok := envValue(primary, legacy) if !ok { return fallback } switch strings.ToLower(v) { case "1", "true", "yes", "on": return true case "0", "false", "no", "off": return false default: return fallback } } func envInt(primary, legacy string, fallback int) int { v, ok := envValue(primary, legacy) if !ok { return fallback } n, err := strconv.Atoi(v) if err != nil { return fallback } return n } func envDuration(primary, legacy string, fallback time.Duration) time.Duration { v, ok := envValue(primary, legacy) if !ok { return fallback } if d, err := time.ParseDuration(v); err == nil { return d } if n, err := strconv.Atoi(v); err == nil { return time.Duration(n) * time.Second } return fallback } func envValue(primary, legacy string) (string, bool) { for _, key := range []string{primary, legacy} { v := strings.TrimSpace(os.Getenv(key)) if v != "" { return v, true } } return "", false }