259 lines
6.5 KiB
Go
259 lines
6.5 KiB
Go
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
|
||
}
|