170 lines
4.1 KiB
Go
170 lines
4.1 KiB
Go
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
|
|
}
|