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 }