fix(server): SMTP dispatch dial timeout + implicit/STARTTLS handling
Server Deploy / deploy (push) Successful in 2m19s

This commit is contained in:
2026-07-21 14:46:15 +01:00
parent f28ab1a741
commit 45178d455e
+70 -9
View File
@@ -1,15 +1,25 @@
package notify
import (
"crypto/tls"
"fmt"
"net"
"net/smtp"
"strings"
"time"
"github.com/mrhid6/vantage/server/internal/models"
)
const smtpTimeout = 15 * time.Second
// dispatchSMTP sends the alert as a plain-text email. Config keys: host, port,
// username, password, from, to. Auth is skipped when username is empty.
// username, password, from, to. Auth is skipped when username is empty. Port 465
// uses implicit TLS; other ports use STARTTLS when the server advertises it.
//
// It dials with a timeout and sets a connection deadline so an unreachable or
// misconfigured SMTP host fails fast instead of hanging the request until the OS
// TCP timeout (which resets the upstream proxy connection).
func dispatchSMTP(ch models.NotificationChannel, ev Event) error {
host := ch.Config["host"]
port := ch.Config["port"]
@@ -19,6 +29,56 @@ func dispatchSMTP(ch models.NotificationChannel, ev Event) error {
return fmt.Errorf("smtp: missing host/port/from/to")
}
addr := net.JoinHostPort(host, port)
conn, err := net.DialTimeout("tcp", addr, smtpTimeout)
if err != nil {
return fmt.Errorf("smtp: dial %s: %w", addr, err)
}
_ = conn.SetDeadline(time.Now().Add(smtpTimeout))
// Implicit TLS on 465; otherwise start plain and upgrade via STARTTLS.
if port == "465" {
conn = tls.Client(conn, &tls.Config{ServerName: host})
}
c, err := smtp.NewClient(conn, host)
if err != nil {
conn.Close()
return fmt.Errorf("smtp: client: %w", err)
}
defer c.Close()
if port != "465" {
if ok, _ := c.Extension("STARTTLS"); ok {
if err := c.StartTLS(&tls.Config{ServerName: host}); err != nil {
return fmt.Errorf("smtp: starttls: %w", err)
}
}
}
if user := ch.Config["username"]; user != "" {
if err := c.Auth(smtp.PlainAuth("", user, ch.Config["password"], host)); err != nil {
return fmt.Errorf("smtp: auth: %w", err)
}
}
recipients := strings.Split(to, ",")
for i := range recipients {
recipients[i] = strings.TrimSpace(recipients[i])
}
if err := c.Mail(from); err != nil {
return fmt.Errorf("smtp: mail from: %w", err)
}
for _, rcpt := range recipients {
if rcpt == "" {
continue
}
if err := c.Rcpt(rcpt); err != nil {
return fmt.Errorf("smtp: rcpt %s: %w", rcpt, err)
}
}
title := ev.title()
msg := strings.Join([]string{
"From: " + from,
@@ -32,14 +92,15 @@ func dispatchSMTP(ch models.NotificationChannel, ev Event) error {
"Time: " + ev.Time.String(),
}, "\r\n")
var auth smtp.Auth
if user := ch.Config["username"]; user != "" {
auth = smtp.PlainAuth("", user, ch.Config["password"], host)
w, err := c.Data()
if err != nil {
return fmt.Errorf("smtp: data: %w", err)
}
addr := host + ":" + port
recipients := strings.Split(to, ",")
for i := range recipients {
recipients[i] = strings.TrimSpace(recipients[i])
if _, err := w.Write([]byte(msg)); err != nil {
return fmt.Errorf("smtp: write: %w", err)
}
return smtp.SendMail(addr, auth, from, recipients, []byte(msg))
if err := w.Close(); err != nil {
return fmt.Errorf("smtp: close data: %w", err)
}
return c.Quit()
}