summaryrefslogtreecommitdiffstats
path: root/internal/config/config.go
diff options
context:
space:
mode:
authorGab Virebent <gabriel1@virebent.art>2026-08-24 17:34:45 +0200
committerGab Virebent <gabriel1@virebent.art>2026-08-24 17:34:45 +0200
commite9fbbe3373eb66a345f5e3829e2563b94dc92051 (patch)
tree950d75fda88574afb46b4aeab36e96f3c089a3bb /internal/config/config.go
parentfb83c4d70616ec23d8a5397409a5d31c70b70d66 (diff)
downloadn2usenet-e9fbbe3373eb66a345f5e3829e2563b94dc92051.tar.gz
n2usenet-e9fbbe3373eb66a345f5e3829e2563b94dc92051.tar.xz
n2usenet-e9fbbe3373eb66a345f5e3829e2563b94dc92051.zip
Harden transport and preserve profile identitiesHEADmain
Diffstat (limited to 'internal/config/config.go')
-rw-r--r--internal/config/config.go64
1 files changed, 64 insertions, 0 deletions
diff --git a/internal/config/config.go b/internal/config/config.go
index 5ee60ea..faf2a93 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -3,6 +3,7 @@ package config
import (
"fmt"
"net"
+ "net/mail"
"os"
"strconv"
"strings"
@@ -23,11 +24,16 @@ type SMTPConfig struct {
Port int
Recipient string
EnvelopeFrom string
+ Sender string
+ Username string
+ Password string
+ PasswordFile string
HELO string
TLSServerName string
RequireTLS bool
ImplicitTLS bool
Timeout time.Duration
+ ProbeInterval time.Duration
DryRun bool
}
@@ -55,6 +61,8 @@ type SecurityConfig struct {
MessageIDDomain string
IdenticonsCLI string
RequireFace bool
+ CSRFKeyFile string
+ CSRFKey []byte
}
func Load() (Config, error) {
@@ -66,11 +74,15 @@ func Load() (Config, error) {
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"),
+ Sender: envAny("n2usenet@virebent.art", "N2U_SMTP_SENDER", "M2U_SMTP_SENDER", "N2U_SMTP_HEADER_FROM", "M2U_SMTP_HEADER_FROM"),
+ Username: env("N2U_SMTP_USERNAME", "M2U_SMTP_USERNAME", ""),
+ PasswordFile: env("N2U_SMTP_PASSWORD_FILE", "M2U_SMTP_PASSWORD_FILE", ""),
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),
+ ProbeInterval: envDuration("N2U_TRANSPORT_PROBE_INTERVAL", "M2U_TRANSPORT_PROBE_INTERVAL", 15*time.Minute),
DryRun: envBool("N2U_DRY_RUN", "M2U_DRY_RUN", false),
},
Nym: NymConfig{
@@ -96,8 +108,23 @@ func Load() (Config, error) {
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),
+ CSRFKeyFile: env("N2U_CSRF_KEY_FILE", "M2U_CSRF_KEY_FILE", ""),
},
}
+ if cfg.SMTP.PasswordFile != "" {
+ password, err := readSecretFile(cfg.SMTP.PasswordFile)
+ if err != nil {
+ return Config{}, fmt.Errorf("read SMTP password file: %w", err)
+ }
+ cfg.SMTP.Password = password
+ }
+ if cfg.Security.CSRFKeyFile != "" {
+ key, err := readSecretFile(cfg.Security.CSRFKeyFile)
+ if err != nil {
+ return Config{}, fmt.Errorf("read CSRF key file: %w", err)
+ }
+ cfg.Security.CSRFKey = []byte(key)
+ }
if err := cfg.Validate(); err != nil {
return Config{}, err
}
@@ -120,9 +147,22 @@ func (c Config) Validate() error {
if c.SMTP.EnvelopeFrom == "" {
return fmt.Errorf("N2U_SMTP_ENVELOPE_FROM is required")
}
+ sender, err := mail.ParseAddress(c.SMTP.Sender)
+ if err != nil || sender.Address == "" {
+ return fmt.Errorf("N2U_SMTP_SENDER must be a valid email address")
+ }
+ if (c.SMTP.Username == "") != (c.SMTP.Password == "") {
+ return fmt.Errorf("N2U_SMTP_USERNAME and N2U_SMTP_PASSWORD_FILE must be configured together")
+ }
+ if c.SMTP.Username != "" && !c.SMTP.RequireTLS && !c.SMTP.ImplicitTLS {
+ return fmt.Errorf("SMTP authentication requires TLS")
+ }
if c.SMTP.RequireTLS && c.SMTP.TLSServerName == "" {
return fmt.Errorf("N2U_SMTP_TLS_SERVER_NAME is required when TLS is required")
}
+ if c.SMTP.ProbeInterval < time.Minute {
+ return fmt.Errorf("N2U_TRANSPORT_PROBE_INTERVAL must be at least 1m")
+ }
if c.Nym.Enabled {
if _, _, err := net.SplitHostPort(c.Nym.SocksAddr); err != nil {
return fmt.Errorf("invalid N2U_NYM_SOCKS: %w", err)
@@ -146,6 +186,9 @@ func (c Config) Validate() error {
if c.Security.RequireFace && c.Security.IdenticonsCLI == "" {
return fmt.Errorf("N2U_IDENTICONS_CLI is required when N2U_REQUIRE_FACE=true")
}
+ if c.Security.CSRFKeyFile != "" && len(c.Security.CSRFKey) < 32 {
+ return fmt.Errorf("N2U_CSRF_KEY_FILE must contain at least 32 bytes")
+ }
return nil
}
@@ -157,6 +200,15 @@ func env(primary, legacy, fallback string) string {
return v
}
+func envAny(fallback string, keys ...string) string {
+ for _, key := range keys {
+ if value := strings.TrimSpace(os.Getenv(key)); value != "" {
+ return value
+ }
+ }
+ return fallback
+}
+
func envBool(primary, legacy string, fallback bool) bool {
v, ok := envValue(primary, legacy)
if !ok {
@@ -207,3 +259,15 @@ func envValue(primary, legacy string) (string, bool) {
}
return "", false
}
+
+func readSecretFile(path string) (string, error) {
+ raw, err := os.ReadFile(path)
+ if err != nil {
+ return "", err
+ }
+ value := strings.TrimSpace(string(raw))
+ if value == "" {
+ return "", fmt.Errorf("file is empty")
+ }
+ return value, nil
+}