feat: email adapter with SMTP TLS support
This commit is contained in:
@@ -22,7 +22,7 @@ func NewSender(channelType string, smtpCfg *config.SMTPConfig) (ChannelSender, e
|
||||
case "bark":
|
||||
return &BarkSender{}, nil
|
||||
case "email":
|
||||
return nil, fmt.Errorf("email sender not yet implemented (see Task 10)")
|
||||
return NewEmailSender(*smtpCfg), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown channel type: %s", channelType)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package adapter
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"aiaa-notification-service/internal/config"
|
||||
)
|
||||
|
||||
type emailConfig struct {
|
||||
To []string `json:"to"`
|
||||
}
|
||||
|
||||
type EmailSender struct {
|
||||
cfg config.SMTPConfig
|
||||
}
|
||||
|
||||
func NewEmailSender(cfg config.SMTPConfig) *EmailSender {
|
||||
return &EmailSender{cfg: cfg}
|
||||
}
|
||||
|
||||
func (s *EmailSender) Type() string { return "email" }
|
||||
|
||||
func (s *EmailSender) Send(title, content string, config json.RawMessage) error {
|
||||
var ecfg emailConfig
|
||||
if err := json.Unmarshal(config, &ecfg); err != nil {
|
||||
return fmt.Errorf("parse email config: %w", err)
|
||||
}
|
||||
if len(ecfg.To) == 0 {
|
||||
return fmt.Errorf("email: 'to' list is empty")
|
||||
}
|
||||
|
||||
msg := buildEmail(s.cfg.From, ecfg.To, title, content)
|
||||
addr := fmt.Sprintf("%s:%d", s.cfg.Host, s.cfg.Port)
|
||||
|
||||
if s.cfg.Port == 465 {
|
||||
return s.sendTLS(addr, ecfg.To, msg)
|
||||
}
|
||||
return s.sendSTARTTLS(addr, ecfg.To, msg)
|
||||
}
|
||||
|
||||
func (s *EmailSender) sendTLS(addr string, to []string, msg []byte) error {
|
||||
tlsCfg := &tls.Config{ServerName: s.cfg.Host}
|
||||
conn, err := tls.Dial("tcp", addr, tlsCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tls dial: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, s.cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp client: %w", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
return s.authAndSend(client, to, msg)
|
||||
}
|
||||
|
||||
func (s *EmailSender) sendSTARTTLS(addr string, to []string, msg []byte) error {
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("dial: %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
client, err := smtp.NewClient(conn, s.cfg.Host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("smtp client: %w", err)
|
||||
}
|
||||
defer client.Quit()
|
||||
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
tlsCfg := &tls.Config{ServerName: s.cfg.Host}
|
||||
if err := client.StartTLS(tlsCfg); err != nil {
|
||||
return fmt.Errorf("starttls: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return s.authAndSend(client, to, msg)
|
||||
}
|
||||
|
||||
func (s *EmailSender) authAndSend(client *smtp.Client, to []string, msg []byte) error {
|
||||
if s.cfg.User != "" {
|
||||
auth := smtp.PlainAuth("", s.cfg.User, s.cfg.Password, s.cfg.Host)
|
||||
if err := client.Auth(auth); err != nil {
|
||||
return fmt.Errorf("auth: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := client.Mail(s.cfg.From); err != nil {
|
||||
return fmt.Errorf("mail: %w", err)
|
||||
}
|
||||
for _, recipient := range to {
|
||||
if err := client.Rcpt(recipient); err != nil {
|
||||
return fmt.Errorf("rcpt %s: %w", recipient, err)
|
||||
}
|
||||
}
|
||||
w, err := client.Data()
|
||||
if err != nil {
|
||||
return fmt.Errorf("data: %w", err)
|
||||
}
|
||||
_, err = w.Write(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write: %w", err)
|
||||
}
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func buildEmail(from string, to []string, subject, body string) []byte {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("From: %s\r\n", from))
|
||||
sb.WriteString(fmt.Sprintf("To: %s\r\n", strings.Join(to, ", ")))
|
||||
sb.WriteString(fmt.Sprintf("Subject: %s\r\n", subject))
|
||||
sb.WriteString("MIME-Version: 1.0\r\n")
|
||||
sb.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
|
||||
sb.WriteString("\r\n")
|
||||
sb.WriteString(body)
|
||||
return []byte(sb.String())
|
||||
}
|
||||
Reference in New Issue
Block a user