From 3714b3ae67b2a7a2ab5ea12f534fae9803f2504e Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 3 Apr 2026 16:17:57 +0200 Subject: [PATCH] fix(mana-notify): support insecure TLS for internal SMTP (Stalwart) Add SMTP_INSECURE_TLS env var to skip certificate verification for internal Docker-network SMTP connections. Stalwart's self-signed cert uses 'localhost' as CN which doesn't match the 'stalwart' hostname. Co-Authored-By: Claude Opus 4.6 (1M context) --- docker-compose.macmini.yml | 1 + .../mana-notify/internal/channel/email.go | 60 +++++++++++++++---- .../mana-notify/internal/config/config.go | 16 ++--- 3 files changed, 58 insertions(+), 19 deletions(-) diff --git a/docker-compose.macmini.yml b/docker-compose.macmini.yml index 1f60bec29..ba1bcf901 100644 --- a/docker-compose.macmini.yml +++ b/docker-compose.macmini.yml @@ -542,6 +542,7 @@ services: SMTP_USER: ${SMTP_USER:-noreply@mana.how} SMTP_PASSWORD: ${SMTP_PASSWORD:-ManaNoReply2026!} SMTP_FROM: "ManaCore " + SMTP_INSECURE_TLS: "true" EXPO_ACCESS_TOKEN: ${EXPO_ACCESS_TOKEN:-} MATRIX_HOMESERVER_URL: http://mana-matrix-synapse:8008 MATRIX_ACCESS_TOKEN: ${MATRIX_NOTIFY_BOT_TOKEN:-} diff --git a/services/mana-notify/internal/channel/email.go b/services/mana-notify/internal/channel/email.go index 941c83e20..f93b82582 100644 --- a/services/mana-notify/internal/channel/email.go +++ b/services/mana-notify/internal/channel/email.go @@ -12,20 +12,22 @@ import ( ) type EmailService struct { - host string - port int - user string - password string - from string + host string + port int + user string + password string + from string + insecureTLS bool } func NewEmailService(cfg *config.Config) *EmailService { return &EmailService{ - host: cfg.SMTPHost, - port: cfg.SMTPPort, - user: cfg.SMTPUser, - password: cfg.SMTPPassword, - from: cfg.SMTPFrom, + host: cfg.SMTPHost, + port: cfg.SMTPPort, + user: cfg.SMTPUser, + password: cfg.SMTPPassword, + from: cfg.SMTPFrom, + insecureTLS: cfg.SMTPInsecureTLS, } } @@ -85,10 +87,44 @@ func (s *EmailService) Send(msg *EmailMessage) EmailResult { auth := smtp.PlainAuth("", s.user, s.password, s.host) - tlsConfig := &tls.Config{ServerName: s.host} + tlsConfig := &tls.Config{ServerName: s.host, InsecureSkipVerify: s.insecureTLS} conn, err := tls.Dial("tcp", addr, tlsConfig) if err != nil { - // Try STARTTLS fallback + // Try STARTTLS fallback — use custom dialer to support insecure TLS + if s.insecureTLS { + c, dialErr := smtp.Dial(addr) + if dialErr != nil { + slog.Error("email send failed", "to", msg.To, "error", dialErr, "duration", time.Since(start)) + return EmailResult{Success: false, Error: dialErr.Error()} + } + defer c.Close() + if err := c.StartTLS(tlsConfig); err != nil { + // Continue without TLS for internal connections + slog.Warn("STARTTLS failed, continuing without TLS", "error", err) + } + if err := c.Auth(auth); err != nil { + return EmailResult{Success: false, Error: err.Error()} + } + if err := c.Mail(fromAddr); err != nil { + return EmailResult{Success: false, Error: err.Error()} + } + if err := c.Rcpt(msg.To); err != nil { + return EmailResult{Success: false, Error: err.Error()} + } + w, err := c.Data() + if err != nil { + return EmailResult{Success: false, Error: err.Error()} + } + if _, err := w.Write([]byte(builder.String())); err != nil { + return EmailResult{Success: false, Error: err.Error()} + } + if err := w.Close(); err != nil { + return EmailResult{Success: false, Error: err.Error()} + } + c.Quit() + slog.Info("email sent via STARTTLS (insecure)", "to", msg.To, "duration", time.Since(start)) + return EmailResult{Success: true} + } err = smtp.SendMail(addr, auth, fromAddr, []string{msg.To}, []byte(builder.String())) if err != nil { slog.Error("email send failed", "to", msg.To, "error", err, "duration", time.Since(start)) diff --git a/services/mana-notify/internal/config/config.go b/services/mana-notify/internal/config/config.go index 9117abf66..e83f47c55 100644 --- a/services/mana-notify/internal/config/config.go +++ b/services/mana-notify/internal/config/config.go @@ -19,12 +19,13 @@ type Config struct { ServiceKey string ManaCoreAuthURL string - // SMTP (Brevo) - SMTPHost string - SMTPPort int - SMTPUser string - SMTPPassword string - SMTPFrom string + // SMTP + SMTPHost string + SMTPPort int + SMTPUser string + SMTPPassword string + SMTPFrom string + SMTPInsecureTLS bool // Expo Push ExpoAccessToken string @@ -58,7 +59,8 @@ func Load() *Config { SMTPPort: envutil.GetInt("SMTP_PORT", 587), SMTPUser: envutil.Get("SMTP_USER", ""), SMTPPassword: envutil.Get("SMTP_PASSWORD", ""), - SMTPFrom: envutil.Get("SMTP_FROM", "ManaCore "), + SMTPFrom: envutil.Get("SMTP_FROM", "ManaCore "), + SMTPInsecureTLS: envutil.GetBool("SMTP_INSECURE_TLS", false), ExpoAccessToken: envutil.Get("EXPO_ACCESS_TOKEN", ""),