package mailer import ( "bytes" "crypto/tls" "crypto/x509" "errors" "fmt" "github.com/emersion/go-message" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" "io" "os" "strings" ) type Mailer struct { auth sasl.Client conf SmtpConfig pool *x509.CertPool } func NewMailer(conf SmtpConfig) (*Mailer, error) { var pool *x509.CertPool = nil if conf.Cert != "" { pool = x509.NewCertPool() open, err := os.Open(conf.Cert) if err != nil { return nil, err } all, err := io.ReadAll(open) if err != nil { return nil, err } if !pool.AppendCertsFromPEM(all) { return nil, errors.New("invalid SMTP certificate") } } return &Mailer{ auth: sasl.NewPlainClient("", conf.Username, conf.Password), conf: conf, pool: pool, }, nil } func (m *Mailer) MailHtml(subject string, to []string, body io.Reader) error { h := message.Header{} h.SetContentType("multipart/alternative", nil) h.SetText("From", m.conf.From) h.SetText("To", strings.Join(to, ", ")) h.SetText("Subject", subject) h.SetText("MIME-version", "1.0") msg := new(bytes.Buffer) w, err := message.CreateWriter(msg, h) if err != nil { return fmt.Errorf("failed to create message writer: %w", err) } h2 := message.Header{} h2.SetContentType("text/html", map[string]string{"charset": "UTF-8"}) part, err := w.CreatePart(h2) if err != nil { return fmt.Errorf("failed to create part: %w", err) } _, err = io.Copy(part, body) if err != nil { return fmt.Errorf("failed to copy body into part: %w", err) } err = part.Close() if err != nil { return fmt.Errorf("failed to close part: %w", err) } err = w.Close() if err != nil { return fmt.Errorf("failed to close message writer: %w", err) } return m.sendMail(to, msg) } func (m *Mailer) sendMail(to []string, body io.Reader) error { var err error var client *smtp.Client if m.conf.Tls { client, err = smtp.DialTLS(m.conf.Server, &tls.Config{RootCAs: m.pool}) if err != nil { return fmt.Errorf("failed to dial tls: %w", err) } } else { client, err = smtp.Dial(m.conf.Server) if err != nil { return fmt.Errorf("failed to dial: %w", err) } } if m.conf.StartTls { if ok, _ := client.Extension("STARTTLS"); ok { if err = client.StartTLS(nil); err != nil { return fmt.Errorf("failed to start tls: %w", err) } } } if err = client.Auth(m.auth); err != nil { return fmt.Errorf("failed to add auth: %w", err) } if err = client.Mail(m.conf.Username, nil); err != nil { return fmt.Errorf("failed to start mail: %w", err) } for _, i := range to { if err = client.Rcpt(i); err != nil { return fmt.Errorf("failed to add recipient: %w", err) } } wc, err := client.Data() if err != nil { return fmt.Errorf("failed to start mail data: %w", err) } if _, err = io.Copy(wc, body); err != nil { return fmt.Errorf("failed to : %w", err) } if err = wc.Close(); err != nil { return fmt.Errorf("failed to close: %w", err) } if err = client.Quit(); err != nil { return fmt.Errorf("failed to quit: %w", err) } return nil }