Scout/internal/notify/email.go
2026-08-30 23:27:30 +08:00

144 lines
3.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}