mail/imap.go

371 lines
8.2 KiB
Go
Raw Permalink Normal View History

2025-08-01 00:54:13 +08:00
package mail
import (
"bytes"
"fmt"
"math"
"time"
"apigo.cc/gojs"
"apigo.cc/gojs/goja"
"github.com/emersion/go-imap/v2"
"github.com/emersion/go-imap/v2/imapclient"
"github.com/jhillyerd/enmime"
"github.com/ssgo/u"
)
type Mail struct {
Uid uint32
From []string
To []string
Cc []string
Bcc []string
Subject string
Date string
Text string
Html string
Attachments []Attachment
Embeds []Attachment
Size int64
Flags []string
}
// 接收选项
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
}
type MailboxInfo struct {
Name string
Delimiter string
Attrs []string
}
type RecvResult struct {
List []Mail
Tag uint32
}
type IMAPClient struct {
client *imapclient.Client
}
func (obj *IMAPClient) Close(vm *goja.Runtime) {
obj.client.Logout().Wait()
obj.client.Close()
}
func (obj *Mailbox) OpenIMAP(vm *goja.Runtime) (*IMAPClient, error) {
logger := gojs.GetLogger(vm)
// 连接服务器
addr := fmt.Sprintf("%s:%d", obj.imapHost, obj.imapPort)
imapClient, err := imapclient.DialTLS(addr, nil)
if err != nil {
logger.Error("failed to connect to IMAP server", "addr", addr, "err", err)
return nil, err
}
// 发送客户端标识
imapClient.ID(&imap.IDData{
Name: conf.ClientName,
Version: conf.ClientVersion,
Vendor: conf.ClientVendor,
SupportURL: conf.ClientSupportURL,
OS: conf.ClientOS,
})
// 登录
if err := imapClient.Login(obj.username, obj.password).Wait(); err != nil {
logger.Error("IMAP login failed", "user", obj.username, "err", err)
return nil, err
}
return &IMAPClient{client: imapClient}, nil
}
func (obj *IMAPClient) ListMailbox(vm *goja.Runtime) ([]MailboxInfo, error) {
logger := gojs.GetLogger(vm)
mailboxes, err := obj.client.List("", "%", nil).Collect()
if err != nil {
logger.Error("failed to list mailboxes", "err", err)
return nil, err
}
list := []MailboxInfo{}
for _, mbox := range mailboxes {
// fmt.Println(u.JsonP(mbox), 111)
attrs := []string{}
for _, attr := range mbox.Attrs {
attrs = append(attrs, string(attr))
}
list = append(list, MailboxInfo{
Name: mbox.Mailbox,
Delimiter: string(mbox.Delim),
Attrs: attrs,
})
}
return list, nil
}
func (obj *IMAPClient) Recv(vm *goja.Runtime, opt *RecvOption) (*RecvResult, error) {
logger := gojs.GetLogger(vm)
// 创建默认选项
if opt == nil {
opt = &RecvOption{
Mailbox: "INBOX",
Unseen: true,
}
}
rr := &RecvResult{Tag: opt.Tag, List: []Mail{}}
// 设置默认限制
if opt.Limit == 0 {
opt.Limit = 20
}
// 选择邮箱
mailboxName := opt.Mailbox
if mailboxName == "" {
mailboxName = "INBOX"
}
// 选择邮箱
c := obj.client
selectCmd := c.Select(mailboxName, nil)
_, err := selectCmd.Wait()
if err != nil {
logger.Error("failed to select mailbox", "mailbox", mailboxName, "err", err)
return rr, err
}
limit := imap.UID(opt.Limit)
startUID := imap.UID(opt.Tag) + 1 // 从上一次最大UID+1开始
endUID := imap.UID(math.MaxUint32) // 始终使用最大值,确保搜索时能拿到足够的结果
criteria := &imap.SearchCriteria{
UID: []imap.UIDSet{[]imap.UIDRange{{Start: startUID, Stop: endUID}}},
}
// 添加基本搜索条件
if opt.Unseen {
criteria.NotFlag = append(criteria.NotFlag, imap.FlagSeen)
}
if opt.Read {
criteria.Flag = append(criteria.Flag, imap.FlagSeen)
}
if opt.Answered {
criteria.Flag = append(criteria.Flag, imap.FlagAnswered)
}
if opt.Flagged {
criteria.Flag = append(criteria.Flag, imap.FlagFlagged)
}
if opt.Deleted {
criteria.Flag = append(criteria.Flag, imap.FlagDeleted)
}
// 添加时间范围条件
if opt.Since != "" {
criteria.Since = u.ParseTime(opt.Since)
}
if opt.Before != "" {
criteria.Before = u.ParseTime(opt.Before)
}
// 添加自定义搜索条件
if len(opt.Search) > 0 {
criteria.Text = opt.Search
}
if len(opt.Body) > 0 {
criteria.Body = opt.Body
}
if len(opt.Subject) > 0 {
for _, word := range opt.Subject {
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
Key: "Subject",
Value: word,
})
}
}
if len(opt.From) > 0 {
for _, word := range opt.From {
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
Key: "From",
Value: word,
})
}
}
if len(opt.To) > 0 {
for _, word := range opt.To {
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
Key: "To",
Value: word,
})
}
}
if len(opt.Cc) > 0 {
for _, word := range opt.Cc {
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
Key: "Cc",
Value: word,
})
}
}
if len(opt.Bcc) > 0 {
for _, word := range opt.Bcc {
criteria.Header = append(criteria.Header, imap.SearchCriteriaHeaderField{
Key: "Bcc",
Value: word,
})
}
}
if len(opt.Not) > 0 {
criteria.Not = []imap.SearchCriteria{}
criteria.Not = append(criteria.Not, imap.SearchCriteria{
Text: opt.Not,
})
}
r, err := c.UIDSearch(criteria, nil).Wait()
if err != nil {
logger.Error("failed to search messages", "err", err)
return rr, err
}
var uidSet imap.UIDSet
maxUID := startUID
var uidCount imap.UID
if all, ok := r.All.(imap.UIDSet); ok {
for _, set := range all {
n := set.Stop - set.Start
if uidCount+n > limit {
set.Stop -= (uidCount + n - limit)
}
maxUID = set.Stop
uidCount += set.Stop - set.Start
uidSet = append(uidSet, set)
if uidCount >= limit {
break
}
}
}
if len(uidSet) == 0 {
return rr, nil
}
fr, err := c.Fetch(uidSet, &imap.FetchOptions{Envelope: true, BodySection: []*imap.FetchItemBodySection{{}}}).Collect()
if err != nil {
logger.Error("failed to fetch messages", "err", err)
return rr, err
}
for _, msg := range fr {
mail := Mail{
Uid: uint32(msg.UID),
Subject: msg.Envelope.Subject,
From: FormatAddressList(msg.Envelope.From),
To: FormatAddressList(msg.Envelope.To),
Cc: FormatAddressList(msg.Envelope.Cc),
Bcc: FormatAddressList(msg.Envelope.Bcc),
Date: msg.Envelope.Date.Format(time.DateTime),
Size: msg.RFC822Size,
Flags: FormatFlags(msg.Flags),
}
// 解析邮件正文和附件
// fmt.Println("####0", len(msg.BodySection))
for _, section := range msg.BodySection {
if section.Section != nil && section.Section.Specifier == imap.PartSpecifierNone {
// fmt.Println("####1", len(section.Bytes))
env, err := enmime.ReadEnvelope(bytes.NewReader(section.Bytes))
if err == nil {
mail.Text = env.Text
mail.Html = env.HTML
// 处理附件
for _, attach := range env.Attachments {
mail.Attachments = append(mail.Attachments, Attachment{
Name: attach.FileName,
Data: u.Base64(attach.Content),
ContentType: attach.ContentType,
})
}
// 处理内嵌资源
for _, embed := range env.Inlines {
mail.Embeds = append(mail.Embeds, Attachment{
Name: embed.FileName,
Data: u.Base64(embed.Content),
ContentType: embed.ContentType,
})
}
} else {
logger.Error("failed to read envelope", "err", err)
return rr, err
}
}
}
rr.List = append(rr.List, mail)
}
rr.Tag = uint32(maxUID)
return rr, nil
}
func FormatFlags(flags []imap.Flag) []string {
result := make([]string, len(flags))
for i, flag := range flags {
result[i] = string(flag)
}
return result
}
func ParseFlags(flags []string) []imap.Flag {
result := make([]imap.Flag, len(flags))
for i, flag := range flags {
result[i] = imap.Flag(flag)
}
return result
}
// 地址格式化
func FormatAddress(addr *imap.Address) string {
if addr == nil {
return ""
}
if addr.Name != "" {
return fmt.Sprintf("%s <%s>", addr.Name, addr.Addr())
}
return addr.Addr()
}
// 地址列表格式化
func FormatAddressList(addrs []imap.Address) []string {
result := make([]string, len(addrs))
for i, addr := range addrs {
result[i] = FormatAddress(&addr)
}
return result
}