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 }