package email import ( "context" "fmt" "log/slog" "net/smtp" "github.com/tim/cairnquire/apps/server/internal/config" ) type Sender interface { Send(ctx context.Context, to []string, subject, body string) error } type SMTPConfig struct { Host string Port int From string } type SMTPSender struct { cfg SMTPConfig logger *slog.Logger } func NewSMTPSender(cfg config.EmailConfig, logger *slog.Logger) *SMTPSender { return &SMTPSender{ cfg: SMTPConfig{ Host: cfg.SMTPHost, Port: cfg.SMTPPort, From: cfg.From, }, logger: logger, } } func (s *SMTPSender) Send(ctx context.Context, to []string, subject, body string) error { if s.cfg.Host == "" { return fmt.Errorf("email host not configured") } addr := fmt.Sprintf("%s:%d", s.cfg.Host, s.cfg.Port) msg := []byte(fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\r\n", s.cfg.From, to[0], subject, body)) s.logger.Info("sending email", "to", to, "subject", subject, "addr", addr) // Use SMTP without auth for local testing (Mailpit/MailHog) var auth smtp.Auth err := smtp.SendMail(addr, auth, s.cfg.From, to, msg) if err != nil { return fmt.Errorf("send email: %w", err) } return nil } type NoOpSender struct { logger *slog.Logger } func NewNoOpSender(logger *slog.Logger) *NoOpSender { return &NoOpSender{logger: logger} } func (n *NoOpSender) Send(ctx context.Context, to []string, subject, body string) error { n.logger.Info("email noop", "to", to, "subject", subject, "body_len", len(body)) return nil }