149 lines
3.0 KiB
Go
149 lines
3.0 KiB
Go
package mailer
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/smtp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
var ErrInvalidConfig = errors.New("invalid smtp config")
|
|
|
|
type Mailer struct {
|
|
cfg Config
|
|
}
|
|
|
|
type Message struct {
|
|
To []string
|
|
Subject string
|
|
Body string
|
|
BodyHTML string
|
|
}
|
|
|
|
func New(cfg Config) *Mailer {
|
|
return &Mailer{cfg: cfg}
|
|
}
|
|
|
|
func (m *Mailer) Send(ctx context.Context, msg Message) error {
|
|
if len(msg.To) == 0 {
|
|
return errors.New("recipient missing")
|
|
}
|
|
if strings.TrimSpace(msg.Subject) == "" {
|
|
return errors.New("subject missing")
|
|
}
|
|
|
|
// timeout kontrolü (ctx)
|
|
deadline, ok := ctx.Deadline()
|
|
if !ok {
|
|
// default timeout
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, 20*time.Second)
|
|
defer cancel()
|
|
deadline, _ = ctx.Deadline()
|
|
}
|
|
timeout := time.Until(deadline)
|
|
if timeout <= 0 {
|
|
return context.DeadlineExceeded
|
|
}
|
|
|
|
addr := fmt.Sprintf("%s:%d", m.cfg.Host, m.cfg.Port)
|
|
|
|
dialer := net.Dialer{Timeout: timeout}
|
|
conn, err := dialer.DialContext(ctx, "tcp", addr)
|
|
if err != nil {
|
|
return fmt.Errorf("smtp dial: %w", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
c, err := smtp.NewClient(conn, m.cfg.Host)
|
|
if err != nil {
|
|
return fmt.Errorf("smtp client: %w", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
// STARTTLS
|
|
if m.cfg.StartTLS {
|
|
tlsCfg := &tls.Config{
|
|
ServerName: m.cfg.Host,
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
if err := c.StartTLS(tlsCfg); err != nil {
|
|
return fmt.Errorf("starttls: %w", err)
|
|
}
|
|
}
|
|
|
|
// AUTH
|
|
auth := smtp.PlainAuth("", m.cfg.Username, m.cfg.Password, m.cfg.Host)
|
|
if err := c.Auth(auth); err != nil {
|
|
return fmt.Errorf("smtp auth: %w", err)
|
|
}
|
|
|
|
// MAIL FROM
|
|
if err := c.Mail(m.cfg.From); err != nil {
|
|
return fmt.Errorf("mail from: %w", err)
|
|
}
|
|
|
|
// RCPT TO
|
|
for _, rcpt := range msg.To {
|
|
rcpt = strings.TrimSpace(rcpt)
|
|
if rcpt == "" {
|
|
continue
|
|
}
|
|
if err := c.Rcpt(rcpt); err != nil {
|
|
return fmt.Errorf("rcpt %s: %w", rcpt, err)
|
|
}
|
|
}
|
|
|
|
w, err := c.Data()
|
|
if err != nil {
|
|
return fmt.Errorf("data: %w", err)
|
|
}
|
|
|
|
// basit mime
|
|
contentType := "text/plain; charset=UTF-8"
|
|
body := msg.Body
|
|
if strings.TrimSpace(msg.BodyHTML) != "" {
|
|
contentType = "text/html; charset=UTF-8"
|
|
body = msg.BodyHTML
|
|
}
|
|
|
|
raw := buildMIME(m.cfg.From, msg.To, msg.Subject, contentType, body)
|
|
|
|
_, writeErr := w.Write([]byte(raw))
|
|
closeErr := w.Close()
|
|
|
|
if writeErr != nil {
|
|
return fmt.Errorf("write body: %w", writeErr)
|
|
}
|
|
if closeErr != nil {
|
|
return fmt.Errorf("close body: %w", closeErr)
|
|
}
|
|
|
|
if err := c.Quit(); err != nil {
|
|
return fmt.Errorf("quit: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func buildMIME(from string, to []string, subject, contentType, body string) string {
|
|
// Subject UTF-8 basit hali (gerekirse sonra MIME encoded-word ekleriz)
|
|
headers := []string{
|
|
"From: " + from,
|
|
"To: " + strings.Join(to, ", "),
|
|
"Subject: " + subject,
|
|
"MIME-Version: 1.0",
|
|
"Content-Type: " + contentType,
|
|
"",
|
|
}
|
|
return strings.Join(headers, "\r\n") + "\r\n" + body + "\r\n"
|
|
}
|
|
|
|
type MailerInterface interface {
|
|
Send(ctx context.Context, msg Message) error
|
|
}
|