You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
// Package notify 内的 notifier_email.go 实现 EmailNotifier:通过 SMTP(net/smtp)
|
||||
// 把通知投递为邮件。它实现 Notifier 接口,由 Dispatcher 在配置启用时注册。
|
||||
//
|
||||
// 投递前置条件(任一不满足则静默跳过,返回 nil——per-channel 失败/跳过对
|
||||
// Dispatcher 非致命):
|
||||
// - 全局 cfg.Enabled 为 true;
|
||||
// - 用户 notification_prefs.email_enabled 为 true;
|
||||
// - 消息 severity 达到用户 min_severity;
|
||||
// - 能解析出该用户的收件邮箱。
|
||||
//
|
||||
// SMTP 调用经 transport 抽象注入,测试用 fake transport 断言收件人/正文形态,
|
||||
// 绝不真的发邮件;生产用 smtpTransport(net/smtp.SendMail)。
|
||||
package notify
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// EmailConfig 是 EmailNotifier 的配置,对应 cfg.Notify.Email。
|
||||
type EmailConfig struct {
|
||||
Enabled bool
|
||||
SMTPHost string
|
||||
Port int
|
||||
From string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// addr 返回 "host:port" 形式的 SMTP 地址。
|
||||
func (c EmailConfig) addr() string {
|
||||
return fmt.Sprintf("%s:%d", c.SMTPHost, c.Port)
|
||||
}
|
||||
|
||||
// EmailLookup 把 userID 解析为收件邮箱。由 user.Service 适配注入,避免 notify
|
||||
// 反向依赖 user 包造成的循环导入。
|
||||
type EmailLookup interface {
|
||||
EmailForUser(ctx context.Context, userID uuid.UUID) (string, error)
|
||||
}
|
||||
|
||||
// SMTPTransport 抽象一次 SMTP 发送,便于测试注入 fake。参数与 net/smtp.SendMail
|
||||
// 对齐:addr 形如 "host:port",auth 可为 nil(无认证),from 为发件人,to 为收件人
|
||||
// 列表,msg 为完整 RFC 822 报文(含头)。
|
||||
type SMTPTransport interface {
|
||||
Send(addr string, auth smtp.Auth, from string, to []string, msg []byte) error
|
||||
}
|
||||
|
||||
// smtpTransport 是 SMTPTransport 的生产实现,直接委托 net/smtp.SendMail。
|
||||
type smtpTransport struct{}
|
||||
|
||||
func (smtpTransport) Send(addr string, auth smtp.Auth, from string, to []string, msg []byte) error {
|
||||
return smtp.SendMail(addr, auth, from, to, msg)
|
||||
}
|
||||
|
||||
// EmailNotifier 通过 SMTP 投递邮件,实现 Notifier。
|
||||
type EmailNotifier struct {
|
||||
cfg EmailConfig
|
||||
prefs PrefsRepository
|
||||
lookup EmailLookup
|
||||
tr SMTPTransport
|
||||
log *slog.Logger
|
||||
}
|
||||
|
||||
// NewEmailNotifier 构造 EmailNotifier。tr 为 nil 时用生产 net/smtp 实现;
|
||||
// log 为 nil 时回落到 slog.Default。
|
||||
func NewEmailNotifier(cfg EmailConfig, prefs PrefsRepository, lookup EmailLookup, tr SMTPTransport, log *slog.Logger) *EmailNotifier {
|
||||
if tr == nil {
|
||||
tr = smtpTransport{}
|
||||
}
|
||||
if log == nil {
|
||||
log = slog.Default()
|
||||
}
|
||||
return &EmailNotifier{cfg: cfg, prefs: prefs, lookup: lookup, tr: tr, log: log}
|
||||
}
|
||||
|
||||
// Name 返回 ChannelEmail。
|
||||
func (n *EmailNotifier) Name() Channel { return ChannelEmail }
|
||||
|
||||
// Send 在满足全部前置条件时发送一封邮件;任一条件不满足静默返回 nil。
|
||||
// SMTP 调用失败返回错误(Dispatcher 仅记日志,不影响其它通道)。
|
||||
func (n *EmailNotifier) Send(ctx context.Context, msg Message) error {
|
||||
if !n.cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
p, err := n.prefs.Get(ctx, msg.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !p.EmailEnabled {
|
||||
return nil
|
||||
}
|
||||
if !meetsMinSeverity(msg.Severity, p.MinSeverity) {
|
||||
return nil
|
||||
}
|
||||
to, err := n.lookup.EmailForUser(ctx, msg.UserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(to) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var auth smtp.Auth
|
||||
if n.cfg.Username != "" {
|
||||
auth = smtp.PlainAuth("", n.cfg.Username, n.cfg.Password, n.cfg.SMTPHost)
|
||||
}
|
||||
body := buildEmailMessage(n.cfg.From, to, msg)
|
||||
if err := n.tr.Send(n.cfg.addr(), auth, n.cfg.From, []string{to}, body); err != nil {
|
||||
return fmt.Errorf("smtp send: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildEmailMessage 组装一封最小 RFC 822 纯文本邮件报文(含 From/To/Subject 头)。
|
||||
// 主题前缀用 severity 便于客户端过滤;正文带标题、正文与可选链接。
|
||||
func buildEmailMessage(from, to string, msg Message) []byte {
|
||||
subject := msg.Title
|
||||
if subject == "" {
|
||||
subject = msg.Topic
|
||||
}
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "From: %s\r\n", from)
|
||||
fmt.Fprintf(&b, "To: %s\r\n", to)
|
||||
fmt.Fprintf(&b, "Subject: [%s] %s\r\n", strings.ToUpper(string(msg.Severity)), subject)
|
||||
b.WriteString("MIME-Version: 1.0\r\n")
|
||||
b.WriteString("Content-Type: text/plain; charset=\"UTF-8\"\r\n")
|
||||
b.WriteString("\r\n")
|
||||
if msg.Body != "" {
|
||||
b.WriteString(msg.Body)
|
||||
b.WriteString("\r\n")
|
||||
}
|
||||
if msg.Link != "" {
|
||||
fmt.Fprintf(&b, "\r\n%s\r\n", msg.Link)
|
||||
}
|
||||
return []byte(b.String())
|
||||
}
|
||||
Reference in New Issue
Block a user