Files
mengpost/mengpost-backend/services/imap.go

259 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package services
import (
"context"
"crypto/tls"
"fmt"
"io"
"strings"
"time"
"github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/emersion/go-message/mail"
imapid "github.com/emersion/go-imap-id"
"mengpost-backend/models"
)
// MailSummary 是列表页展示的邮件摘要.
type MailSummary struct {
UID uint32 `json:"uid"`
Subject string `json:"subject"`
From string `json:"from"`
Date time.Time `json:"date"`
Seen bool `json:"seen"`
Mailbox string `json:"mailbox"`
}
// MailDetail 邮件详情, 包含正文.
type MailDetail struct {
MailSummary
To []string `json:"to"`
Text string `json:"text"`
HTML string `json:"html"`
}
func dial(acc *models.Account) (*client.Client, error) {
addr := fmt.Sprintf("%s:%d", acc.IMAPHost, acc.IMAPPort)
if acc.UseTLS {
cfg := IMAPTLSConfig(acc)
if cfg == nil {
cfg = &tls.Config{MinVersion: tls.VersionTLS12, ServerName: acc.IMAPHost}
}
return client.DialTLS(addr, cfg)
}
return client.Dial(addr)
}
func imapAuthenticate(c *client.Client, acc *models.Account) error {
if acc.AuthType == models.AuthTypeOAuthMicrosoft {
if AppCfg == nil {
return fmt.Errorf("服务未配置微软 OAuth")
}
mt, err := RefreshMicrosoftAccess(context.Background(), AppCfg, acc.OAuthRefreshToken)
if err != nil {
return err
}
if mt.RefreshToken != "" {
_ = models.UpdateAccountOAuthRefresh(acc.ID, mt.RefreshToken)
}
if err := c.Authenticate(NewXOAUTH2Client(acc.Email, mt.AccessToken)); err != nil {
return err
}
} else {
if err := c.Login(acc.Email, acc.Password); err != nil {
return err
}
}
// Outlook/Exchange IMAP 在 AUTH 后对 RFC2971 ID 常返回 “Command Argument Error.12”;网易等仍需 ID。
if !imapShouldSendClientID(acc) {
return nil
}
return imapSendClientID(c)
}
func imapShouldSendClientID(acc *models.Account) bool {
if acc.AuthType == models.AuthTypeOAuthMicrosoft {
return false
}
if IsMicrosoftConsumerEmail(acc.Email) && strings.EqualFold(acc.IMAPHost, "outlook.office365.com") {
return false
}
return true
}
// imapSendClientID 在 LOGIN 之后、SELECT/LIST 等之前发送 RFC2971 ID网易 163/126/yeah 等要求,否则报 Unsafe Login
func imapSendClientID(c *client.Client) error {
ic := imapid.NewClient(c)
_, err := ic.ID(imapid.ID{
imapid.FieldName: "MengPost",
imapid.FieldVersion: "1.0",
imapid.FieldVendor: "MengPost",
imapid.FieldOS: "Go",
})
return err
}
// ListMailboxes 列出账户下可用的邮件夹名称.
func ListMailboxes(acc *models.Account) ([]string, error) {
c, err := dial(acc)
if err != nil {
return nil, err
}
defer c.Logout()
if err := imapAuthenticate(c, acc); err != nil {
return nil, err
}
ch := make(chan *imap.MailboxInfo, 20)
done := make(chan error, 1)
go func() { done <- c.List("", "*", ch) }()
var out []string
for m := range ch {
out = append(out, m.Name)
}
if err := <-done; err != nil {
return nil, err
}
return out, nil
}
// FetchMessages 获取指定邮件夹中最近 n 封邮件摘要.
func FetchMessages(acc *models.Account, mailbox string, limit int) ([]MailSummary, error) {
if mailbox == "" {
mailbox = "INBOX"
}
if limit <= 0 {
limit = 30
}
c, err := dial(acc)
if err != nil {
return nil, err
}
defer c.Logout()
if err := imapAuthenticate(c, acc); err != nil {
return nil, err
}
mbox, err := c.Select(mailbox, true)
if err != nil {
return nil, err
}
if mbox.Messages == 0 {
return []MailSummary{}, nil
}
from := uint32(1)
if mbox.Messages > uint32(limit) {
from = mbox.Messages - uint32(limit) + 1
}
seq := new(imap.SeqSet)
seq.AddRange(from, mbox.Messages)
items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchUid}
ch := make(chan *imap.Message, limit)
done := make(chan error, 1)
go func() { done <- c.Fetch(seq, items, ch) }()
var out []MailSummary
for msg := range ch {
sum := MailSummary{UID: msg.Uid, Mailbox: mailbox}
if msg.Envelope != nil {
sum.Subject = msg.Envelope.Subject
sum.Date = msg.Envelope.Date
if len(msg.Envelope.From) > 0 {
a := msg.Envelope.From[0]
sum.From = fmt.Sprintf("%s <%s@%s>", a.PersonalName, a.MailboxName, a.HostName)
}
}
for _, f := range msg.Flags {
if f == imap.SeenFlag {
sum.Seen = true
}
}
out = append(out, sum)
}
if err := <-done; err != nil {
return nil, err
}
// 最新的在前
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
out[i], out[j] = out[j], out[i]
}
return out, nil
}
// FetchMessage 根据 UID 获取单封邮件完整内容.
func FetchMessage(acc *models.Account, mailbox string, uid uint32) (*MailDetail, error) {
if mailbox == "" {
mailbox = "INBOX"
}
c, err := dial(acc)
if err != nil {
return nil, err
}
defer c.Logout()
if err := imapAuthenticate(c, acc); err != nil {
return nil, err
}
if _, err := c.Select(mailbox, false); err != nil {
return nil, err
}
seq := new(imap.SeqSet)
seq.AddNum(uid)
section := &imap.BodySectionName{}
items := []imap.FetchItem{imap.FetchEnvelope, imap.FetchFlags, imap.FetchUid, section.FetchItem()}
ch := make(chan *imap.Message, 1)
done := make(chan error, 1)
go func() { done <- c.UidFetch(seq, items, ch) }()
msg := <-ch
if err := <-done; err != nil {
return nil, err
}
if msg == nil {
return nil, fmt.Errorf("message not found")
}
d := &MailDetail{MailSummary: MailSummary{UID: msg.Uid, Mailbox: mailbox}}
if msg.Envelope != nil {
d.Subject = msg.Envelope.Subject
d.Date = msg.Envelope.Date
if len(msg.Envelope.From) > 0 {
a := msg.Envelope.From[0]
d.From = fmt.Sprintf("%s <%s@%s>", a.PersonalName, a.MailboxName, a.HostName)
}
for _, a := range msg.Envelope.To {
d.To = append(d.To, fmt.Sprintf("%s@%s", a.MailboxName, a.HostName))
}
}
for _, f := range msg.Flags {
if f == imap.SeenFlag {
d.Seen = true
}
}
r := msg.GetBody(section)
if r == nil {
return d, nil
}
mr, err := mail.CreateReader(r)
if err != nil {
b, _ := io.ReadAll(r)
d.Text = string(b)
return d, nil
}
for {
p, err := mr.NextPart()
if err == io.EOF {
break
} else if err != nil {
break
}
switch h := p.Header.(type) {
case *mail.InlineHeader:
ct, _, _ := h.ContentType()
b, _ := io.ReadAll(p.Body)
if strings.Contains(ct, "html") {
d.HTML = string(b)
} else {
d.Text = string(b)
}
}
}
return d, nil
}