Files
bssapp/svc/internal/mailer/mailer.go
2026-02-11 17:46:22 +03:00

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
}