-
Notifications
You must be signed in to change notification settings - Fork 2
fix(email): add SMTP client timeouts to prevent indefinite hanging #51
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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() | ||||||||
|
|
||||||||
|
Comment on lines
106
to
+110
|
||||||||
| tlsConfig := &tls.Config{ | ||||||||
| ServerName: s.cfg.SMTPHost, | ||||||||
| MinVersion: tls.VersionTLS12, | ||||||||
|
|
@@ -104,12 +116,25 @@ 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 { | ||||||||
| 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) | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| client, err := smtp.NewClient(conn, s.cfg.SMTPHost) | ||||||||
| if err != nil { | ||||||||
|
||||||||
| if err != nil { | |
| if err != nil { | |
| _ = conn.Close() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment claims the SMTP timeouts "never block the request path", but the code can still block the caller for up to smtpOverallTimeout (and up to smtpDialTimeout during connect). Consider rewording to reflect that the goal is to bound/limit blocking time rather than eliminate it entirely.