ilk
This commit is contained in:
148
svc/internal/mailer/mailer.go
Normal file
148
svc/internal/mailer/mailer.go
Normal file
@@ -0,0 +1,148 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user