@@ -58,11 +58,10 @@ func (e *EmailNotifier) Validate(encryptor encryption.FieldEncryptor) error {
5858
5959func (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;\n Content-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