From 85e49c9e9f31ce0226303ce45bce21e907b7565c Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Sat, 15 Nov 2025 18:48:14 -0600 Subject: [PATCH] some more email work for smtp errors --- internal/email/email.go | 125 ++++++++++++++++++++++++---------------- 1 file changed, 75 insertions(+), 50 deletions(-) diff --git a/internal/email/email.go b/internal/email/email.go index 9f550f5..4fa27e1 100644 --- a/internal/email/email.go +++ b/internal/email/email.go @@ -1,25 +1,19 @@ package email import ( + "crypto/tls" "fmt" "log" + "net/smtp" "net/url" "strings" - "time" "github.com/rideaware/admin-panel/internal/config" "github.com/rideaware/admin-panel/internal/database" - - "github.com/wneessen/go-mail" ) // SendUpdate sends a newsletter with the given subject and body to all subscriber emails stored in the database. // It returns a human-readable status message and, when subscriber retrieval fails, the underlying error. -// - If retrieving subscribers fails: returns "Failed to retrieve subscribers" and the error. -// - If no subscribers are found: returns "No subscribers found." and nil. -// - If sending to a specific subscriber fails: returns "Failed to send to " and nil. -// - On success: returns "Email has been sent to all subscribers." and nil. -// Note: logging the newsletter entry in the database is attempted after sending and any logging failure is non-fatal. func SendUpdate(subject, body string) (string, error) { subscribers, err := database.GetAllEmails() if err != nil { @@ -28,6 +22,7 @@ func SendUpdate(subject, body string) (string, error) { if len(subscribers) == 0 { return "No subscribers found.", nil } + var succeeded, failed int for _, email := range subscribers { if send(subject, body, email) { @@ -36,88 +31,118 @@ func SendUpdate(subject, body string) (string, error) { failed++ } } + if err := database.LogNewsletter(subject, body); err != nil { log.Printf("Error logging newsletter: %v", err) } + if failed == 0 { return fmt.Sprintf("Email sent to all %d subscribers.", succeeded), nil } return fmt.Sprintf("Sent to %d/%d subscribers; %d failed.", succeeded, succeeded+failed, failed), nil } -// send constructs and sends an HTML newsletter update to the specified recipient using the current SMTP configuration. -// It embeds an unsubscribe link for the recipient and returns true if the message was sent successfully, false if client creation, message setup, or sending fails. +// send constructs and sends an HTML newsletter update to the specified recipient func send(subject, body, recipient string) bool { cfg := config.Current - var opts []mail.ClientOption - opts = append(opts, - mail.WithPort(cfg.SMTPPort), - mail.WithSMTPAuth(mail.SMTPAuthPlain), - mail.WithUsername(cfg.SMTPUser), - mail.WithPassword(cfg.SMTPPassword), - mail.WithTimeout(10*time.Second), - ) + log.Printf("Attempting to send email to %s via %s:%d", recipient, cfg.SMTPServer, cfg.SMTPPort) - // Use SSL for port 465, STARTTLS for others - if cfg.SMTPPort == 465 { - opts = append(opts, mail.WithSSL()) - } else { - opts = append(opts, mail.WithTLSPolicy(mail.TLSMandatory)) - } - - client, err := mail.NewClient(cfg.SMTPServer, opts...) - if err != nil { - log.Printf("Failed to create mail client: %v", err) - return false - } - defer client.Close() - - m := mail.NewMsg() - if err := m.From(cfg.SenderEmail); err != nil { - log.Printf("Failed to set from: %v", err) - return false - } - if err := m.To(recipient); err != nil { - log.Printf("Failed to set to: %v", err) - return false - } - m.Subject(subject) + addr := fmt.Sprintf("%s:%d", cfg.SMTPServer, cfg.SMTPPort) unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s", cfg.BaseURL, url.QueryEscape(recipient)) - // Build HTML body with unsubscribe link htmlBody := buildHTMLBody(body, unsubLink) - m.SetBodyString(mail.TypeTextHTML, htmlBody) + message := buildMessage(cfg.SenderEmail, recipient, subject, htmlBody) - if err := client.Send(m); err != nil { - log.Printf("Failed to send email to %s: %v", recipient, err) + // Create TLS connection + tlsconfig := &tls.Config{ + ServerName: cfg.SMTPServer, + } + + conn, err := tls.Dial("tcp", addr, tlsconfig) + if err != nil { + log.Printf("Failed to connect to SMTP %s: %v", addr, err) return false } + defer conn.Close() + + // Create SMTP client + client, err := smtp.NewClient(conn, cfg.SMTPServer) + if err != nil { + log.Printf("Failed to create SMTP client: %v", err) + return false + } + defer client.Close() + + // Authenticate + auth := smtp.PlainAuth("", cfg.SMTPUser, cfg.SMTPPassword, cfg.SMTPServer) + if err := client.Auth(auth); err != nil { + log.Printf("SMTP auth failed for %s: %v", cfg.SMTPUser, err) + return false + } + + // Send the email + if err := client.Mail(cfg.SenderEmail); err != nil { + log.Printf("MAIL command failed: %v", err) + return false + } + + if err := client.Rcpt(recipient); err != nil { + log.Printf("RCPT command failed for %s: %v", recipient, err) + return false + } + + w, err := client.Data() + if err != nil { + log.Printf("DATA command failed: %v", err) + return false + } + + _, err = w.Write([]byte(message)) + if err != nil { + log.Printf("Failed to write message: %v", err) + return false + } + + err = w.Close() + if err != nil { + log.Printf("Failed to close DATA: %v", err) + return false + } + + client.Quit() log.Printf("Update email sent to: %s", recipient) return true } +func buildMessage(from, to, subject, body string) string { + msg := fmt.Sprintf("From: %s\r\n", from) + msg += fmt.Sprintf("To: %s\r\n", to) + msg += fmt.Sprintf("Subject: %s\r\n", subject) + msg += "MIME-Version: 1.0\r\n" + msg += "Content-Type: text/html; charset=\"utf-8\"\r\n" + msg += "\r\n" + msg += body + return msg +} + // buildHTMLBody constructs the final HTML email body by appending an unsubscribe footer to the user-provided content. -// It handles both complete HTML documents and HTML fragments. func buildHTMLBody(body, unsubLink string) string { footer := fmt.Sprintf( "


If you ever wish to unsubscribe, "+ "please click here.

", unsubLink) - // If body contains closing html tag, insert before it if strings.Contains(strings.ToLower(body), "") { return strings.Replace(body, "", footer+"", 1) } - // If body contains closing body tag, insert before it if strings.Contains(strings.ToLower(body), "") { return strings.Replace(body, "", footer+"", 1) } - // Otherwise just append return body + footer } \ No newline at end of file