From 331f073c93f0fd9cf3632dcc2b6d750a81ebad25 Mon Sep 17 00:00:00 2001 From: ilhom Date: Tue, 14 Apr 2026 09:52:53 +0700 Subject: [PATCH 1/2] fix(email): add SMTP client timeouts to prevent indefinite hanging --- .../internal/infrastructure/email/service.go | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/services/iam/internal/infrastructure/email/service.go b/services/iam/internal/infrastructure/email/service.go index c494bd0..92c15ee 100644 --- a/services/iam/internal/infrastructure/email/service.go +++ b/services/iam/internal/infrastructure/email/service.go @@ -5,14 +5,22 @@ import ( "context" "crypto/tls" "fmt" + "net" "net/smtp" "strings" + "time" "github.com/rs/zerolog/log" "github.com/mutugading/goapps-backend/services/iam/internal/infrastructure/config" ) +// SMTP client timeouts. Kept tight so failures surface fast and never block the request path. +const ( + smtpDialTimeout = 10 * time.Second + smtpOverallTimeout = 30 * time.Second +) + // Service implements the auth.EmailService interface via SMTP. type Service struct { cfg *config.EmailConfig @@ -70,7 +78,7 @@ func (s *Service) send(ctx context.Context, to, subject, htmlBody string) error var msg strings.Builder for k, v := range headers { - fmt.Fprintf(&msg, "%s: %s\r\n", k, v) + _, _ = fmt.Fprintf(&msg, "%s: %s\r\n", k, v) } msg.WriteString("\r\n") msg.WriteString(htmlBody) @@ -96,6 +104,10 @@ func (s *Service) send(ctx context.Context, to, subject, htmlBody string) error } func (s *Service) sendTLS(ctx context.Context, addr string, auth smtp.Auth, to, msg string) error { + // Enforce a bounded overall deadline so no SMTP step can hang indefinitely. + ctx, cancel := context.WithTimeout(ctx, smtpOverallTimeout) + defer cancel() + tlsConfig := &tls.Config{ ServerName: s.cfg.SMTPHost, MinVersion: tls.VersionTLS12, @@ -104,12 +116,23 @@ func (s *Service) sendTLS(ctx context.Context, addr string, auth smtp.Auth, to, tlsConfig.InsecureSkipVerify = true //nolint:gosec // Configurable for dev/self-hosted environments. } - dialer := &tls.Dialer{Config: tlsConfig} + dialer := &tls.Dialer{ + NetDialer: &net.Dialer{Timeout: smtpDialTimeout}, + Config: tlsConfig, + } conn, err := dialer.DialContext(ctx, "tcp", addr) if err != nil { return fmt.Errorf("failed to connect to SMTP server: %w", err) } + // After dial, TCP-level deadlines prevent a stalled server from hanging reads/writes. + if deadline, ok := ctx.Deadline(); ok { + if err := conn.SetDeadline(deadline); err != nil { + _ = conn.Close() + return fmt.Errorf("failed to set SMTP connection deadline: %w", err) + } + } + client, err := smtp.NewClient(conn, s.cfg.SMTPHost) if err != nil { return fmt.Errorf("failed to create SMTP client: %w", err) From 9e1b2c3b74ddeeafd904eb76fb14d59dc9f24325 Mon Sep 17 00:00:00 2001 From: ilhom Date: Tue, 14 Apr 2026 10:06:39 +0700 Subject: [PATCH 2/2] fix(email): handle errors when closing SMTP connection after SetDeadline failure --- services/iam/internal/infrastructure/email/service.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/services/iam/internal/infrastructure/email/service.go b/services/iam/internal/infrastructure/email/service.go index 92c15ee..84b927b 100644 --- a/services/iam/internal/infrastructure/email/service.go +++ b/services/iam/internal/infrastructure/email/service.go @@ -128,7 +128,9 @@ func (s *Service) sendTLS(ctx context.Context, addr string, auth smtp.Auth, to, // After dial, TCP-level deadlines prevent a stalled server from hanging reads/writes. if deadline, ok := ctx.Deadline(); ok { if err := conn.SetDeadline(deadline); err != nil { - _ = conn.Close() + if closeErr := conn.Close(); closeErr != nil { + log.Warn().Err(closeErr).Msg("failed to close SMTP connection after SetDeadline error") + } return fmt.Errorf("failed to set SMTP connection deadline: %w", err) } }