144 lines
3.6 KiB
Go
144 lines
3.6 KiB
Go
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
|
||
}
|