2026-05-13 01:30:41 +08:00
|
|
|
package mail
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"apigo.cc/go/log"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
instances = make(map[string]*Mailbox)
|
|
|
|
|
instMu sync.RWMutex
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Mailbox 邮件客户端实例,实现 starter.Service 接口
|
|
|
|
|
type Mailbox struct {
|
|
|
|
|
name string
|
|
|
|
|
config *MailboxConfig
|
|
|
|
|
handlers []mailHandler
|
|
|
|
|
logger *log.Logger
|
|
|
|
|
lastUID uint32
|
|
|
|
|
ctx context.Context
|
|
|
|
|
cancel context.CancelFunc
|
|
|
|
|
wg sync.WaitGroup
|
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type mailHandler struct {
|
|
|
|
|
opt *RecvOption
|
|
|
|
|
fn func(Mail)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attachment 附件或内嵌资源信息
|
|
|
|
|
type Attachment struct {
|
|
|
|
|
Name string // 文件名
|
|
|
|
|
Path string // 本地路径(发送时使用)
|
|
|
|
|
Data []byte // 文件数据
|
|
|
|
|
ContentType string // 内容类型
|
|
|
|
|
ContentID string // 内嵌资源 ID (CID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mail 邮件内容,支持富媒体
|
|
|
|
|
type Mail struct {
|
|
|
|
|
UID uint32
|
|
|
|
|
From string
|
|
|
|
|
To []string
|
|
|
|
|
Cc []string
|
|
|
|
|
Bcc []string
|
|
|
|
|
Subject string
|
|
|
|
|
Date string
|
|
|
|
|
Text string // 纯文本正文
|
|
|
|
|
HTML string // HTML 正文
|
|
|
|
|
Attachments []Attachment // 普通附件
|
|
|
|
|
Embeds []Attachment // 内嵌资源(如图片)
|
|
|
|
|
Size int64 // 邮件大小
|
|
|
|
|
Flags []string // 邮件标记
|
|
|
|
|
Headers map[string][]string // 原始请求头
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecvOption 接收选项,支持复杂的过滤规则
|
|
|
|
|
type RecvOption struct {
|
|
|
|
|
Tag uint32
|
|
|
|
|
Mailbox string
|
|
|
|
|
Unseen bool
|
|
|
|
|
Read bool
|
|
|
|
|
Answered bool
|
|
|
|
|
Flagged bool
|
|
|
|
|
Deleted bool
|
|
|
|
|
Since string
|
|
|
|
|
Before string
|
|
|
|
|
Limit uint32
|
|
|
|
|
MarkAsRead bool
|
|
|
|
|
|
|
|
|
|
Subject []string
|
|
|
|
|
Search []string
|
|
|
|
|
Body []string
|
|
|
|
|
From []string
|
|
|
|
|
To []string
|
|
|
|
|
Cc []string
|
|
|
|
|
Bcc []string
|
|
|
|
|
Not []string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecvResult 接收结果
|
|
|
|
|
type RecvResult struct {
|
|
|
|
|
List []Mail
|
|
|
|
|
Tag uint32
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 21:55:14 +08:00
|
|
|
// NewWithConfig 是 New 的别名
|
|
|
|
|
func NewWithConfig(cfg *MailboxConfig) (*Mailbox, error) {
|
|
|
|
|
return New(cfg)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MustNew 创建一个邮件客户端实例,如果失败则 panic
|
|
|
|
|
func MustNew(cfg *MailboxConfig) *Mailbox {
|
|
|
|
|
m, err := New(cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send 使用默认邮箱发送邮件
|
|
|
|
|
func Send(to []string, subject, content string, option *SendOption) error {
|
|
|
|
|
m := GetMailbox("default")
|
|
|
|
|
if m == nil {
|
|
|
|
|
return fmt.Errorf("default mailbox not configured")
|
|
|
|
|
}
|
|
|
|
|
return m.Send(to, subject, content, option)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MustSend 使用默认邮箱发送邮件,如果失败则 panic
|
|
|
|
|
func MustSend(to []string, subject, content string, option *SendOption) {
|
|
|
|
|
if err := Send(to, subject, content, option); err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recv 使用默认邮箱接收邮件
|
|
|
|
|
func Recv(opt *RecvOption) (*RecvResult, error) {
|
|
|
|
|
m := GetMailbox("default")
|
|
|
|
|
if m == nil {
|
|
|
|
|
return nil, fmt.Errorf("default mailbox not configured")
|
|
|
|
|
}
|
|
|
|
|
return m.Recv(opt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MustRecv 使用默认邮箱接收邮件,如果失败则 panic
|
|
|
|
|
func MustRecv(opt *RecvOption) *RecvResult {
|
|
|
|
|
res, err := Recv(opt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
panic(err)
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 01:30:41 +08:00
|
|
|
// GetMailbox 获取或创建一个指定名称的邮件客户端实例
|
|
|
|
|
func GetMailbox(name string, configs ...*MailboxConfig) *Mailbox {
|
|
|
|
|
instMu.RLock()
|
|
|
|
|
m, ok := instances[name]
|
|
|
|
|
instMu.RUnlock()
|
|
|
|
|
if ok {
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(configs) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m, _ = New(configs[0])
|
|
|
|
|
if m != nil {
|
|
|
|
|
m.name = name
|
|
|
|
|
instMu.Lock()
|
|
|
|
|
instances[name] = m
|
|
|
|
|
instMu.Unlock()
|
|
|
|
|
}
|
|
|
|
|
return m
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New 创建一个邮件客户端实例
|
|
|
|
|
func New(cfg *MailboxConfig) (*Mailbox, error) {
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
return nil, fmt.Errorf("config is nil")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mCfg := *cfg
|
|
|
|
|
// 自动补全逻辑保持不变...
|
|
|
|
|
if mCfg.SMTPHost == "" || mCfg.IMAPHost == "" {
|
|
|
|
|
a := strings.SplitN(mCfg.Username, "@", 2)
|
|
|
|
|
if len(a) == 2 {
|
|
|
|
|
if mCfg.SMTPHost == "" {
|
|
|
|
|
mCfg.SMTPHost = "smtp." + a[1]
|
|
|
|
|
}
|
|
|
|
|
if mCfg.SMTPPort == 0 {
|
|
|
|
|
mCfg.SMTPPort = 465
|
|
|
|
|
}
|
|
|
|
|
if mCfg.IMAPHost == "" {
|
|
|
|
|
mCfg.IMAPHost = "imap." + a[1]
|
|
|
|
|
}
|
|
|
|
|
if mCfg.IMAPPort == 0 {
|
|
|
|
|
mCfg.IMAPPort = 993
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if mCfg.SenderMail == "" {
|
|
|
|
|
mCfg.SenderMail = mCfg.Username
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m := &Mailbox{
|
|
|
|
|
config: &mCfg,
|
|
|
|
|
logger: log.DefaultLogger,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, rule := range cfg.Rules {
|
|
|
|
|
r := rule
|
|
|
|
|
m.On(&r, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return m, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// On 在指定实例上注册邮件到达回调
|
|
|
|
|
func (m *Mailbox) On(opt *RecvOption, handler func(Mail)) {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
defer m.mu.Unlock()
|
|
|
|
|
m.handlers = append(m.handlers, mailHandler{opt: opt, fn: handler})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Recv 接收邮件
|
|
|
|
|
func (m *Mailbox) Recv(opt *RecvOption) (*RecvResult, error) {
|
|
|
|
|
return m.recv(opt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- starter.Service Implementation ---
|
|
|
|
|
|
|
|
|
|
func (m *Mailbox) Start(ctx context.Context, logger *log.Logger) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
if m.cancel != nil {
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if logger != nil {
|
|
|
|
|
m.logger = logger
|
|
|
|
|
}
|
|
|
|
|
m.ctx, m.cancel = context.WithCancel(ctx)
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
interval := m.config.PollInterval
|
|
|
|
|
if interval <= 0 {
|
|
|
|
|
interval = 1 * time.Minute
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m.wg.Add(1)
|
|
|
|
|
go m.watch(interval)
|
|
|
|
|
|
|
|
|
|
m.logger.Info("Mail service started", "name", m.name, "username", m.config.Username)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Mailbox) Stop(ctx context.Context) error {
|
|
|
|
|
m.mu.Lock()
|
|
|
|
|
if m.cancel == nil {
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
m.cancel()
|
|
|
|
|
m.mu.Unlock()
|
|
|
|
|
m.wg.Wait()
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Mailbox) Health() error {
|
|
|
|
|
if m.config == nil {
|
|
|
|
|
return fmt.Errorf("mailbox not configured")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Internal Logic ---
|
|
|
|
|
|
|
|
|
|
func (m *Mailbox) watch(interval time.Duration) {
|
|
|
|
|
defer m.wg.Done()
|
|
|
|
|
ticker := time.NewTicker(interval)
|
|
|
|
|
defer ticker.Stop()
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-m.ctx.Done():
|
|
|
|
|
return
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
m.poll()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (m *Mailbox) poll() {
|
|
|
|
|
m.mu.RLock()
|
|
|
|
|
handlers := make([]mailHandler, len(m.handlers))
|
|
|
|
|
copy(handlers, m.handlers)
|
|
|
|
|
m.mu.RUnlock()
|
|
|
|
|
|
|
|
|
|
if len(handlers) == 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i := range handlers {
|
|
|
|
|
h := &handlers[i]
|
|
|
|
|
opt := *h.opt
|
|
|
|
|
if opt.Tag == 0 {
|
|
|
|
|
opt.Tag = m.lastUID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res, err := m.Recv(&opt)
|
|
|
|
|
if err != nil {
|
|
|
|
|
m.logger.Error("Mail poll failed", "err", err, "name", m.name)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if res.Tag > m.lastUID {
|
|
|
|
|
m.lastUID = res.Tag
|
|
|
|
|
}
|
|
|
|
|
if h.opt.Tag < res.Tag {
|
|
|
|
|
h.opt.Tag = res.Tag
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, mail := range res.List {
|
|
|
|
|
if h.fn != nil {
|
|
|
|
|
h.fn(mail)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|