package notify import ( "context" "crypto/tls" "fmt" "log" "net/smtp" "os" "strconv" "strings" ) type Message struct { To []string Subject string Body string } type Sender interface { Send(context.Context, Message) error } type SMTP struct { Host string Port int Username string Password string From string TLS bool } type LogSender struct { Logger *log.Logger } func NewFromEnv(logger *log.Logger) Sender { host := strings.TrimSpace(os.Getenv("SCOUT_SMTP_HOST")) if host == "" { return LogSender{Logger: logger} } port, err := strconv.Atoi(envOr("SCOUT_SMTP_PORT", "587")) if err != nil || port <= 0 || port > 65535 { port = 587 } return SMTP{ Host: host, Port: port, Username: os.Getenv("SCOUT_SMTP_USERNAME"), Password: os.Getenv("SCOUT_SMTP_PASSWORD"), From: envOr("SCOUT_SMTP_FROM", os.Getenv("SCOUT_SMTP_USERNAME")), TLS: strings.EqualFold(os.Getenv("SCOUT_SMTP_TLS"), "true"), } } func (sender LogSender) Send(_ context.Context, message Message) error { if sender.Logger != nil { sender.Logger.Printf("邮箱通知未配置 SMTP,记录待发送邮件:收件人=%s,主题=%s", strings.Join(message.To, ","), message.Subject) } return nil } func (sender SMTP) Send(ctx context.Context, message Message) error { if len(message.To) == 0 { return nil } if sender.Host == "" || sender.From == "" { return fmt.Errorf("SMTP 配置不完整") } select { case <-ctx.Done(): return ctx.Err() default: } address := fmt.Sprintf("%s:%d", sender.Host, sender.Port) headers := []string{ "From: " + sender.From, "To: " + strings.Join(message.To, ", "), "Subject: " + message.Subject, "MIME-Version: 1.0", "Content-Type: text/plain; charset=UTF-8", } data := []byte(strings.Join(headers, "\r\n") + "\r\n\r\n" + message.Body) if sender.TLS { connection, err := tls.Dial("tcp", address, &tls.Config{ServerName: sender.Host, MinVersion: tls.VersionTLS12}) if err != nil { return fmt.Errorf("连接 SMTP TLS 服务失败: %w", err) } defer connection.Close() client, err := smtp.NewClient(connection, sender.Host) if err != nil { return fmt.Errorf("创建 SMTP 客户端失败: %w", err) } defer client.Close() if sender.Username != "" { if err := client.Auth(smtp.PlainAuth("", sender.Username, sender.Password, sender.Host)); err != nil { return fmt.Errorf("SMTP 认证失败: %w", err) } } if err := sendWithClient(client, sender.From, message.To, data); err != nil { return err } return client.Quit() } auth := smtp.PlainAuth("", sender.Username, sender.Password, sender.Host) if sender.Username == "" { auth = nil } if err := smtp.SendMail(address, auth, sender.From, message.To, data); err != nil { return fmt.Errorf("发送邮件失败: %w", err) } return nil } func sendWithClient(client *smtp.Client, from string, recipients []string, data []byte) error { if err := client.Mail(from); err != nil { return fmt.Errorf("设置发件人失败: %w", err) } for _, recipient := range recipients { if err := client.Rcpt(recipient); err != nil { return fmt.Errorf("设置收件人失败: %w", err) } } writer, err := client.Data() if err != nil { return fmt.Errorf("打开邮件正文失败: %w", err) } if _, err := writer.Write(data); err != nil { writer.Close() return fmt.Errorf("写入邮件正文失败: %w", err) } if err := writer.Close(); err != nil { return fmt.Errorf("关闭邮件正文失败: %w", err) } return nil } func envOr(name, fallback string) string { if value := os.Getenv(name); value != "" { return value } return fallback }