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) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-03 16:17:57 +02:00
parent d5b76bd646
commit 3714b3ae67
3 changed files with 58 additions and 19 deletions

View file

@ -542,6 +542,7 @@ services:
SMTP_USER: ${SMTP_USER:-noreply@mana.how}
SMTP_PASSWORD: ${SMTP_PASSWORD:-ManaNoReply2026!}
SMTP_FROM: "ManaCore <noreply@mana.how>"
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:-}

View file

@ -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))

View file

@ -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 <noreply@mana.how>"),
SMTPFrom: envutil.Get("SMTP_FROM", "ManaCore <noreply@mana.how>"),
SMTPInsecureTLS: envutil.GetBool("SMTP_INSECURE_TLS", false),
ExpoAccessToken: envutil.Get("EXPO_ACCESS_TOKEN", ""),