package mailer import ( "context" "fmt" "net" "net/mail" "net/smtp" "strconv" "strings" "time" "trustcontact/internal/config" ) type SMTPMailer struct { host string port int username string password string from string fromName string } func NewSMTPMailer(cfg *config.Config) (*SMTPMailer, error) { if strings.TrimSpace(cfg.SMTP.Host) == "" { return nil, fmt.Errorf("smtp host is required") } if cfg.SMTP.Port <= 0 { return nil, fmt.Errorf("smtp port must be > 0") } if strings.TrimSpace(cfg.SMTP.From) == "" { return nil, fmt.Errorf("smtp from is required") } return &SMTPMailer{ host: cfg.SMTP.Host, port: cfg.SMTP.Port, username: cfg.SMTP.Username, password: cfg.SMTP.Password, from: cfg.SMTP.From, fromName: cfg.SMTP.FromName, }, nil } func (m *SMTPMailer) Send(ctx context.Context, to, subject, htmlBody, textBody string) error { if ctx != nil { if err := ctx.Err(); err != nil { return err } } addr := net.JoinHostPort(m.host, strconv.Itoa(m.port)) msg := m.buildMessage(to, subject, htmlBody, textBody) var auth smtp.Auth if m.username != "" { auth = smtp.PlainAuth("", m.username, m.password, m.host) } if err := smtp.SendMail(addr, auth, m.from, []string{to}, []byte(msg)); err != nil { return err } if ctx != nil { if err := ctx.Err(); err != nil { return err } } return nil } func (m *SMTPMailer) buildMessage(to, subject, htmlBody, textBody string) string { boundary := fmt.Sprintf("mixed-%d", time.Now().UTC().UnixNano()) fromAddr := (&mail.Address{Name: m.fromName, Address: m.from}).String() toAddr := (&mail.Address{Address: to}).String() headers := []string{ fmt.Sprintf("From: %s", fromAddr), fmt.Sprintf("To: %s", toAddr), fmt.Sprintf("Subject: %s", subject), "MIME-Version: 1.0", fmt.Sprintf("Content-Type: multipart/alternative; boundary=%q", boundary), "", } parts := []string{ fmt.Sprintf("--%s", boundary), "Content-Type: text/plain; charset=UTF-8", "", textBody, fmt.Sprintf("--%s", boundary), "Content-Type: text/html; charset=UTF-8", "", htmlBody, fmt.Sprintf("--%s--", boundary), "", } return strings.Join(append(headers, parts...), "\r\n") }