103 lines
2.5 KiB
Go
103 lines
2.5 KiB
Go
// 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()
|
|
}
|