Skip to content

Commit 7007236

Browse files
FIX (email): Recrate client in case of auth error
1 parent db55cad commit 7007236

File tree

2 files changed

+154
-147
lines changed

2 files changed

+154
-147
lines changed

backend/internal/features/notifiers/models/email_notifier/dto.go renamed to backend/internal/features/notifiers/models/email_notifier/auth.go

File renamed without changes.

backend/internal/features/notifiers/models/email_notifier/model.go

Lines changed: 154 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,10 @@ func (e *EmailNotifier) Validate(encryptor encryption.FieldEncryptor) error {
5858

5959
func (e *EmailNotifier) Send(
6060
encryptor encryption.FieldEncryptor,
61-
logger *slog.Logger,
61+
_ *slog.Logger,
6262
heading string,
6363
message string,
6464
) error {
65-
// Decrypt SMTP password if provided
6665
var smtpPassword string
6766
if e.SMTPPassword != "" {
6867
decrypted, err := encryptor.Decrypt(e.NotifierID, e.SMTPPassword)
@@ -72,7 +71,6 @@ func (e *EmailNotifier) Send(
7271
smtpPassword = decrypted
7372
}
7473

75-
// Compose email
7674
from := e.From
7775
if from == "" {
7876
from = e.SMTPUser
@@ -81,191 +79,200 @@ func (e *EmailNotifier) Send(
8179
}
8280
}
8381

84-
to := []string{e.TargetEmail}
82+
emailContent := e.buildEmailContent(heading, message, from)
83+
isAuthRequired := e.SMTPUser != "" && smtpPassword != ""
84+
85+
if e.SMTPPort == ImplicitTLSPort {
86+
return e.sendImplicitTLS(emailContent, from, smtpPassword, isAuthRequired)
87+
}
88+
return e.sendStartTLS(emailContent, from, smtpPassword, isAuthRequired)
89+
}
90+
91+
func (e *EmailNotifier) HideSensitiveData() {
92+
e.SMTPPassword = ""
93+
}
94+
95+
func (e *EmailNotifier) Update(incoming *EmailNotifier) {
96+
e.TargetEmail = incoming.TargetEmail
97+
e.SMTPHost = incoming.SMTPHost
98+
e.SMTPPort = incoming.SMTPPort
99+
e.SMTPUser = incoming.SMTPUser
100+
e.From = incoming.From
85101

86-
// Format the email content
102+
if incoming.SMTPPassword != "" {
103+
e.SMTPPassword = incoming.SMTPPassword
104+
}
105+
}
106+
107+
func (e *EmailNotifier) EncryptSensitiveData(encryptor encryption.FieldEncryptor) error {
108+
if e.SMTPPassword != "" {
109+
encrypted, err := encryptor.Encrypt(e.NotifierID, e.SMTPPassword)
110+
if err != nil {
111+
return fmt.Errorf("failed to encrypt SMTP password: %w", err)
112+
}
113+
e.SMTPPassword = encrypted
114+
}
115+
return nil
116+
}
117+
118+
func (e *EmailNotifier) buildEmailContent(heading, message, from string) []byte {
87119
subject := fmt.Sprintf("Subject: %s\r\n", heading)
88120
mime := fmt.Sprintf(
89121
"MIME-version: 1.0;\nContent-Type: %s; charset=\"%s\";\n\n",
90122
MIMETypeHTML,
91123
MIMECharsetUTF8,
92124
)
93-
body := message
94125
fromHeader := fmt.Sprintf("From: %s\r\n", from)
95126
toHeader := fmt.Sprintf("To: %s\r\n", e.TargetEmail)
127+
return []byte(fromHeader + toHeader + subject + mime + message)
128+
}
96129

97-
// Combine all parts of the email
98-
emailContent := []byte(fromHeader + toHeader + subject + mime + body)
130+
func (e *EmailNotifier) sendImplicitTLS(
131+
emailContent []byte,
132+
from string,
133+
password string,
134+
isAuthRequired bool,
135+
) error {
136+
createClient := func() (*smtp.Client, func(), error) {
137+
return e.createImplicitTLSClient()
138+
}
99139

100-
addr := net.JoinHostPort(e.SMTPHost, fmt.Sprintf("%d", e.SMTPPort))
101-
timeout := DefaultTimeout
140+
client, cleanup, err := e.authenticateWithRetry(createClient, password, isAuthRequired)
141+
if err != nil {
142+
return err
143+
}
144+
defer cleanup()
102145

103-
// Determine if authentication is required
104-
isAuthRequired := e.SMTPUser != "" && smtpPassword != ""
146+
return e.sendEmail(client, from, emailContent)
147+
}
105148

106-
// Handle different port scenarios
107-
if e.SMTPPort == ImplicitTLSPort {
108-
// Implicit TLS (port 465)
109-
// Set up TLS config
110-
tlsConfig := &tls.Config{
111-
ServerName: e.SMTPHost,
112-
}
149+
func (e *EmailNotifier) sendStartTLS(
150+
emailContent []byte,
151+
from string,
152+
password string,
153+
isAuthRequired bool,
154+
) error {
155+
createClient := func() (*smtp.Client, func(), error) {
156+
return e.createStartTLSClient()
157+
}
113158

114-
// Dial with timeout
115-
dialer := &net.Dialer{Timeout: timeout}
116-
conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
117-
if err != nil {
118-
return fmt.Errorf("failed to connect to SMTP server: %w", err)
119-
}
120-
defer func() {
121-
_ = conn.Close()
122-
}()
159+
client, cleanup, err := e.authenticateWithRetry(createClient, password, isAuthRequired)
160+
if err != nil {
161+
return err
162+
}
163+
defer cleanup()
123164

124-
// Create SMTP client
125-
client, err := smtp.NewClient(conn, e.SMTPHost)
126-
if err != nil {
127-
return fmt.Errorf("failed to create SMTP client: %w", err)
128-
}
129-
defer func() {
130-
_ = client.Quit()
131-
}()
165+
return e.sendEmail(client, from, emailContent)
166+
}
132167

133-
// Set up authentication only if credentials are provided
134-
if isAuthRequired {
135-
if err := e.authenticate(client, smtpPassword); err != nil {
136-
return err
137-
}
138-
}
168+
func (e *EmailNotifier) createImplicitTLSClient() (*smtp.Client, func(), error) {
169+
addr := net.JoinHostPort(e.SMTPHost, fmt.Sprintf("%d", e.SMTPPort))
170+
tlsConfig := &tls.Config{ServerName: e.SMTPHost}
171+
dialer := &net.Dialer{Timeout: DefaultTimeout}
139172

140-
// Set sender and recipients
141-
if err := client.Mail(from); err != nil {
142-
return fmt.Errorf("failed to set sender: %w", err)
143-
}
144-
for _, recipient := range to {
145-
if err := client.Rcpt(recipient); err != nil {
146-
return fmt.Errorf("failed to set recipient: %w", err)
147-
}
148-
}
173+
conn, err := tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
174+
if err != nil {
175+
return nil, nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
176+
}
149177

150-
// Send the email body
151-
writer, err := client.Data()
152-
if err != nil {
153-
return fmt.Errorf("failed to get data writer: %w", err)
154-
}
155-
_, err = writer.Write(emailContent)
156-
if err != nil {
157-
return fmt.Errorf("failed to write email content: %w", err)
158-
}
159-
err = writer.Close()
160-
if err != nil {
161-
return fmt.Errorf("failed to close data writer: %w", err)
162-
}
178+
client, err := smtp.NewClient(conn, e.SMTPHost)
179+
if err != nil {
180+
_ = conn.Close()
181+
return nil, nil, fmt.Errorf("failed to create SMTP client: %w", err)
182+
}
163183

164-
return nil
165-
} else {
166-
// STARTTLS (port 587) or other ports
167-
// Create a custom dialer with timeout
168-
dialer := &net.Dialer{Timeout: timeout}
169-
conn, err := dialer.Dial("tcp", addr)
170-
if err != nil {
171-
return fmt.Errorf("failed to connect to SMTP server: %w", err)
172-
}
184+
return client, func() { _ = client.Quit() }, nil
185+
}
173186

174-
// Create client from connection
175-
client, err := smtp.NewClient(conn, e.SMTPHost)
176-
if err != nil {
177-
return fmt.Errorf("failed to create SMTP client: %w", err)
178-
}
179-
defer func() {
180-
_ = client.Quit()
181-
}()
187+
func (e *EmailNotifier) createStartTLSClient() (*smtp.Client, func(), error) {
188+
addr := net.JoinHostPort(e.SMTPHost, fmt.Sprintf("%d", e.SMTPPort))
189+
dialer := &net.Dialer{Timeout: DefaultTimeout}
182190

183-
// Send email using the client
184-
if err := client.Hello(DefaultHelloName); err != nil {
185-
return fmt.Errorf("SMTP hello failed: %w", err)
186-
}
191+
conn, err := dialer.Dial("tcp", addr)
192+
if err != nil {
193+
return nil, nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
194+
}
187195

188-
// Start TLS if available
189-
if ok, _ := client.Extension("STARTTLS"); ok {
190-
if err := client.StartTLS(&tls.Config{ServerName: e.SMTPHost}); err != nil {
191-
return fmt.Errorf("STARTTLS failed: %w", err)
192-
}
193-
}
196+
client, err := smtp.NewClient(conn, e.SMTPHost)
197+
if err != nil {
198+
_ = conn.Close()
199+
return nil, nil, fmt.Errorf("failed to create SMTP client: %w", err)
200+
}
194201

195-
// Authenticate only if credentials are provided
196-
if isAuthRequired {
197-
if err := e.authenticate(client, smtpPassword); err != nil {
198-
return err
199-
}
200-
}
202+
if err := client.Hello(DefaultHelloName); err != nil {
203+
_ = client.Quit()
204+
_ = conn.Close()
205+
return nil, nil, fmt.Errorf("SMTP hello failed: %w", err)
206+
}
201207

202-
if err := client.Mail(from); err != nil {
203-
return fmt.Errorf("failed to set sender: %w", err)
208+
if ok, _ := client.Extension("STARTTLS"); ok {
209+
if err := client.StartTLS(&tls.Config{ServerName: e.SMTPHost}); err != nil {
210+
_ = client.Quit()
211+
_ = conn.Close()
212+
return nil, nil, fmt.Errorf("STARTTLS failed: %w", err)
204213
}
214+
}
205215

206-
for _, recipient := range to {
207-
if err := client.Rcpt(recipient); err != nil {
208-
return fmt.Errorf("failed to set recipient: %w", err)
209-
}
210-
}
216+
return client, func() { _ = client.Quit() }, nil
217+
}
211218

212-
writer, err := client.Data()
213-
if err != nil {
214-
return fmt.Errorf("failed to get data writer: %w", err)
215-
}
219+
func (e *EmailNotifier) authenticateWithRetry(
220+
createClient func() (*smtp.Client, func(), error),
221+
password string,
222+
isAuthRequired bool,
223+
) (*smtp.Client, func(), error) {
224+
client, cleanup, err := createClient()
225+
if err != nil {
226+
return nil, nil, err
227+
}
216228

217-
_, err = writer.Write(emailContent)
218-
if err != nil {
219-
return fmt.Errorf("failed to write email content: %w", err)
220-
}
229+
if !isAuthRequired {
230+
return client, cleanup, nil
231+
}
221232

222-
err = writer.Close()
223-
if err != nil {
224-
return fmt.Errorf("failed to close data writer: %w", err)
225-
}
233+
// Try PLAIN auth first
234+
plainAuth := smtp.PlainAuth("", e.SMTPUser, password, e.SMTPHost)
235+
if err := client.Auth(plainAuth); err == nil {
236+
return client, cleanup, nil
237+
}
238+
239+
// PLAIN auth failed, connection may be closed - recreate and try LOGIN auth
240+
cleanup()
226241

227-
return client.Quit()
242+
client, cleanup, err = createClient()
243+
if err != nil {
244+
return nil, nil, err
228245
}
229-
}
230246

231-
func (e *EmailNotifier) HideSensitiveData() {
232-
e.SMTPPassword = ""
247+
loginAuth := &loginAuth{username: e.SMTPUser, password: password}
248+
if err := client.Auth(loginAuth); err != nil {
249+
cleanup()
250+
return nil, nil, fmt.Errorf("SMTP authentication failed: %w", err)
251+
}
252+
253+
return client, cleanup, nil
233254
}
234255

235-
func (e *EmailNotifier) Update(incoming *EmailNotifier) {
236-
e.TargetEmail = incoming.TargetEmail
237-
e.SMTPHost = incoming.SMTPHost
238-
e.SMTPPort = incoming.SMTPPort
239-
e.SMTPUser = incoming.SMTPUser
240-
e.From = incoming.From
256+
func (e *EmailNotifier) sendEmail(client *smtp.Client, from string, content []byte) error {
257+
if err := client.Mail(from); err != nil {
258+
return fmt.Errorf("failed to set sender: %w", err)
259+
}
241260

242-
if incoming.SMTPPassword != "" {
243-
e.SMTPPassword = incoming.SMTPPassword
261+
if err := client.Rcpt(e.TargetEmail); err != nil {
262+
return fmt.Errorf("failed to set recipient: %w", err)
244263
}
245-
}
246264

247-
func (e *EmailNotifier) EncryptSensitiveData(encryptor encryption.FieldEncryptor) error {
248-
if e.SMTPPassword != "" {
249-
encrypted, err := encryptor.Encrypt(e.NotifierID, e.SMTPPassword)
250-
if err != nil {
251-
return fmt.Errorf("failed to encrypt SMTP password: %w", err)
252-
}
253-
e.SMTPPassword = encrypted
265+
writer, err := client.Data()
266+
if err != nil {
267+
return fmt.Errorf("failed to get data writer: %w", err)
254268
}
255-
return nil
256-
}
257269

258-
func (e *EmailNotifier) authenticate(client *smtp.Client, password string) error {
259-
// Try PLAIN auth first (most common)
260-
plainAuth := smtp.PlainAuth("", e.SMTPUser, password, e.SMTPHost)
261-
if err := client.Auth(plainAuth); err == nil {
262-
return nil
270+
if _, err = writer.Write(content); err != nil {
271+
return fmt.Errorf("failed to write email content: %w", err)
263272
}
264273

265-
// If PLAIN fails, try LOGIN auth (required by Office 365 and some providers)
266-
loginAuth := &loginAuth{e.SMTPUser, password}
267-
if err := client.Auth(loginAuth); err != nil {
268-
return fmt.Errorf("SMTP authentication failed: %w", err)
274+
if err = writer.Close(); err != nil {
275+
return fmt.Errorf("failed to close data writer: %w", err)
269276
}
270277

271278
return nil

0 commit comments

Comments
 (0)