// Package notifications provides SMTP email delivery for budget alert notifications. package notifications import ( "crypto/tls" "fmt" "net" "net/smtp" "strings" ) // Config holds SMTP relay configuration. type Config struct { Host string Port int Username string Password string From string FromName string } // Mailer sends transactional emails via SMTP (STARTTLS, PLAIN auth). // Tested against Mailtrap sandbox; compatible with any RFC 4954 relay. type Mailer struct { cfg Config } // New creates a Mailer. Returns an error if the config is obviously invalid. func New(cfg Config) (*Mailer, error) { if cfg.Host == "" { return nil, fmt.Errorf("smtp host is required") } if cfg.Port == 0 { cfg.Port = 587 } return &Mailer{cfg: cfg}, nil } // Message is a single outbound email. type Message struct { To string // single recipient address Subject string HTML string // HTML body } // Send delivers msg via SMTP with STARTTLS + PLAIN auth. func (m *Mailer) Send(msg Message) error { from := m.cfg.From if m.cfg.FromName != "" { from = fmt.Sprintf("%s <%s>", m.cfg.FromName, m.cfg.From) } headers := strings.Join([]string{ fmt.Sprintf("From: %s", from), fmt.Sprintf("To: %s", msg.To), fmt.Sprintf("Subject: %s", msg.Subject), "MIME-Version: 1.0", `Content-Type: text/html; charset="UTF-8"`, "", "", }, "\r\n") body := []byte(headers + msg.HTML) addr := fmt.Sprintf("%s:%d", m.cfg.Host, m.cfg.Port) conn, err := net.Dial("tcp", addr) if err != nil { return fmt.Errorf("smtp dial %s: %w", addr, err) } client, err := smtp.NewClient(conn, m.cfg.Host) if err != nil { return fmt.Errorf("smtp client: %w", err) } defer client.Close() //nolint:errcheck // Upgrade to TLS via STARTTLS if the server supports it (all Mailtrap ports do). if ok, _ := client.Extension("STARTTLS"); ok { if err = client.StartTLS(&tls.Config{ServerName: m.cfg.Host}); err != nil { return fmt.Errorf("starttls: %w", err) } } auth := smtp.PlainAuth("", m.cfg.Username, m.cfg.Password, m.cfg.Host) if err = client.Auth(auth); err != nil { return fmt.Errorf("smtp auth: %w", err) } if err = client.Mail(m.cfg.From); err != nil { return fmt.Errorf("smtp MAIL FROM: %w", err) } if err = client.Rcpt(msg.To); err != nil { return fmt.Errorf("smtp RCPT TO: %w", err) } wc, err := client.Data() if err != nil { return fmt.Errorf("smtp DATA: %w", err) } if _, err = wc.Write(body); err != nil { return fmt.Errorf("smtp write body: %w", err) } return wc.Close() }