371 lines
8.2 KiB
Go
371 lines
8.2 KiB
Go
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
|
|
}
|