package mailer import ( "context" "crypto/tls" "fmt" "net" "net/smtp" "strings" "juwan-backend/app/email/mq/internal/config" ) type Sender struct { conf config.MailConf } type Message struct { To []string Cc []string Bcc []string Subject string Body string IsHTML bool } func NewSender(conf config.MailConf) (*Sender, error) { if strings.TrimSpace(conf.Host) == "" { return nil, fmt.Errorf("mail host is required") } if conf.Port <= 0 { return nil, fmt.Errorf("mail port is required") } if strings.TrimSpace(conf.FromAddress) == "" { return nil, fmt.Errorf("mail from address is required") } if conf.UseSSL && conf.UseStartTLS { return nil, fmt.Errorf("mail config invalid: UseSSL and UseStartTLS cannot both be true") } return &Sender{conf: conf}, nil } func (s *Sender) Send(ctx context.Context, msg Message) error { toList := compactAddresses(msg.To) if len(toList) == 0 { return fmt.Errorf("mail recipients are empty") } ccList := compactAddresses(msg.Cc) bccList := compactAddresses(msg.Bcc) allRecipients := append(append([]string{}, toList...), ccList...) allRecipients = append(allRecipients, bccList...) addr := fmt.Sprintf("%s:%d", s.conf.Host, s.conf.Port) var ( client *smtp.Client err error ) tlsConfig := &tls.Config{ ServerName: s.conf.Host, InsecureSkipVerify: s.conf.InsecureSkipVerify, } if s.conf.UseSSL { conn, dialErr := tls.DialWithDialer((&net.Dialer{}), "tcp", addr, tlsConfig) if dialErr != nil { return fmt.Errorf("smtp ssl dial failed(%s): %w", addr, dialErr) } client, err = smtp.NewClient(conn, s.conf.Host) } else { dialer := &net.Dialer{} conn, dialErr := dialer.DialContext(ctx, "tcp", addr) if dialErr != nil { return fmt.Errorf("smtp dial failed(%s): %w", addr, dialErr) } client, err = smtp.NewClient(conn, s.conf.Host) } if err != nil { return fmt.Errorf("smtp create client failed: %w", err) } defer client.Close() if s.conf.UseStartTLS { if err = client.StartTLS(tlsConfig); err != nil { return fmt.Errorf("smtp starttls failed: %w", err) } } if strings.TrimSpace(s.conf.Username) != "" { auth := smtp.PlainAuth("", s.conf.Username, s.conf.Password, s.conf.Host) if err = client.Auth(auth); err != nil { return fmt.Errorf("smtp auth failed: %w", err) } } if err = client.Mail(s.conf.FromAddress); err != nil { return fmt.Errorf("smtp mail from failed: %w", err) } for _, rcpt := range allRecipients { if err = client.Rcpt(rcpt); err != nil { return fmt.Errorf("smtp rcpt to(%s) failed: %w", rcpt, err) } } w, err := client.Data() if err != nil { return fmt.Errorf("smtp data start failed: %w", err) } bodyType := "text/plain; charset=UTF-8" if msg.IsHTML { bodyType = "text/html; charset=UTF-8" } headers := []string{ fmt.Sprintf("From: %s", formatFrom(s.conf.FromName, s.conf.FromAddress)), fmt.Sprintf("To: %s", strings.Join(toList, ",")), fmt.Sprintf("Subject: %s", msg.Subject), "MIME-Version: 1.0", fmt.Sprintf("Content-Type: %s", bodyType), } if len(ccList) > 0 { headers = append(headers, fmt.Sprintf("Cc: %s", strings.Join(ccList, ","))) } if strings.TrimSpace(s.conf.ReplyTo) != "" { headers = append(headers, fmt.Sprintf("Reply-To: %s", strings.TrimSpace(s.conf.ReplyTo))) } raw := strings.Join(headers, "\r\n") + "\r\n\r\n" + msg.Body if _, err = w.Write([]byte(raw)); err != nil { _ = w.Close() return fmt.Errorf("smtp write body failed: %w", err) } if err = w.Close(); err != nil { return fmt.Errorf("smtp data close failed: %w", err) } if err = client.Quit(); err != nil { return fmt.Errorf("smtp quit failed: %w", err) } return nil } func formatFrom(name, address string) string { trimmedName := strings.TrimSpace(name) trimmedAddress := strings.TrimSpace(address) if trimmedName == "" { return trimmedAddress } return fmt.Sprintf("%s <%s>", trimmedName, trimmedAddress) } func compactAddresses(input []string) []string { result := make([]string, 0, len(input)) for _, item := range input { trimmed := strings.TrimSpace(item) if trimmed == "" { continue } result = append(result, trimmed) } return result }