some more email work for smtp errors

This commit is contained in:
Cipher Vance
2025-11-15 18:48:14 -06:00
parent 9444bab05f
commit 85e49c9e9f

View File

@@ -1,25 +1,19 @@
package email package email
import ( import (
"crypto/tls"
"fmt" "fmt"
"log" "log"
"net/smtp"
"net/url" "net/url"
"strings" "strings"
"time"
"github.com/rideaware/admin-panel/internal/config" "github.com/rideaware/admin-panel/internal/config"
"github.com/rideaware/admin-panel/internal/database" "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. // 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. // 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 <email>" 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) { func SendUpdate(subject, body string) (string, error) {
subscribers, err := database.GetAllEmails() subscribers, err := database.GetAllEmails()
if err != nil { if err != nil {
@@ -28,6 +22,7 @@ func SendUpdate(subject, body string) (string, error) {
if len(subscribers) == 0 { if len(subscribers) == 0 {
return "No subscribers found.", nil return "No subscribers found.", nil
} }
var succeeded, failed int var succeeded, failed int
for _, email := range subscribers { for _, email := range subscribers {
if send(subject, body, email) { if send(subject, body, email) {
@@ -36,88 +31,118 @@ func SendUpdate(subject, body string) (string, error) {
failed++ failed++
} }
} }
if err := database.LogNewsletter(subject, body); err != nil { if err := database.LogNewsletter(subject, body); err != nil {
log.Printf("Error logging newsletter: %v", err) log.Printf("Error logging newsletter: %v", err)
} }
if failed == 0 { if failed == 0 {
return fmt.Sprintf("Email sent to all %d subscribers.", succeeded), nil 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 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. // send constructs and sends an HTML newsletter update to the specified recipient
// 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.
func send(subject, body, recipient string) bool { func send(subject, body, recipient string) bool {
cfg := config.Current cfg := config.Current
var opts []mail.ClientOption log.Printf("Attempting to send email to %s via %s:%d", recipient, cfg.SMTPServer, cfg.SMTPPort)
opts = append(opts,
mail.WithPort(cfg.SMTPPort),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(cfg.SMTPUser),
mail.WithPassword(cfg.SMTPPassword),
mail.WithTimeout(10*time.Second),
)
// Use SSL for port 465, STARTTLS for others addr := fmt.Sprintf("%s:%d", cfg.SMTPServer, cfg.SMTPPort)
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)
unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s", unsubLink := fmt.Sprintf("https://%s/unsubscribe?email=%s",
cfg.BaseURL, url.QueryEscape(recipient)) cfg.BaseURL, url.QueryEscape(recipient))
// Build HTML body with unsubscribe link
htmlBody := buildHTMLBody(body, unsubLink) htmlBody := buildHTMLBody(body, unsubLink)
m.SetBodyString(mail.TypeTextHTML, htmlBody) message := buildMessage(cfg.SenderEmail, recipient, subject, htmlBody)
if err := client.Send(m); err != nil { // Create TLS connection
log.Printf("Failed to send email to %s: %v", recipient, err) 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 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) log.Printf("Update email sent to: %s", recipient)
return true 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. // 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 { func buildHTMLBody(body, unsubLink string) string {
footer := fmt.Sprintf( footer := fmt.Sprintf(
"<br><br><hr><p style='font-size: 12px; color: #666;'>If you ever wish to unsubscribe, "+ "<br><br><hr><p style='font-size: 12px; color: #666;'>If you ever wish to unsubscribe, "+
"please click <a href='%s'>here</a>.</p>", "please click <a href='%s'>here</a>.</p>",
unsubLink) unsubLink)
// If body contains closing html tag, insert before it
if strings.Contains(strings.ToLower(body), "</html>") { if strings.Contains(strings.ToLower(body), "</html>") {
return strings.Replace(body, "</html>", footer+"</html>", 1) return strings.Replace(body, "</html>", footer+"</html>", 1)
} }
// If body contains closing body tag, insert before it
if strings.Contains(strings.ToLower(body), "</body>") { if strings.Contains(strings.ToLower(body), "</body>") {
return strings.Replace(body, "</body>", footer+"</body>", 1) return strings.Replace(body, "</body>", footer+"</body>", 1)
} }
// Otherwise just append
return body + footer return body + footer
} }