Smail/backend/dto/parsemail/imap.go
smail 733612bcfb
Some checks are pending
Security Checks / security (push) Waiting to run
修复IMAP回复邮件关联头
2026-10-06 05:27:11 +08:00

134 lines
4.0 KiB
Go
Raw Permalink 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 parsemail
import (
"bytes"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"time"
"git.pcux.cn/xuans/Smail/config"
"git.pcux.cn/xuans/Smail/utils/context"
"github.com/emersion/go-message"
"github.com/emersion/go-message/mail"
)
// BuildIMAPBytes 为同一封存储邮件生成稳定的 MIME,确保分段读取与大小、结构一致。
// 收件视图的密送信息由调用方过滤;此方法只用于 IMAP,不能用于 SMTP 投递。
func (e *Email) BuildIMAPBytes(_ *context.Context) ([]byte, error) {
if e == nil || e.From == nil || e.From.EmailAddress == "" {
return nil, errors.New("invalid sender")
}
encoded, err := json.Marshal(e)
if err != nil {
return nil, err
}
digest := sha256.Sum256(encoded)
boundary := fmt.Sprintf("smail_%x", digest[:16])
var h mail.Header
date, err := time.Parse(time.RFC3339, e.Date)
if err != nil {
date, err = time.ParseInLocation(time.DateTime, e.Date, time.Local)
}
if err != nil {
// 历史异常日期不能使用当前时间,否则重复 FETCH 的字节内容会变化。
date = time.Unix(0, 0).UTC()
}
h.SetDate(date)
messageID := e.MsgID
if messageID == "" {
cfg := config.CurrentConfig()
if cfg == nil || cfg.Domain == "" {
return nil, errors.New("message id domain is not configured")
}
messageID = fmt.Sprintf("%d@%s", e.MessageId, cfg.Domain)
}
h.SetMessageID(messageID)
h.SetSubject(e.Subject)
h.SetMsgIDList("In-Reply-To", ThreadMessageIDs(e.InReplyTo))
h.SetMsgIDList("References", ThreadMessageIDs(e.ThreadReferences))
sender := e.Sender
if sender == nil {
sender = e.From
}
for _, field := range []struct {
name string
users []*User
}{
{"From", []*User{e.From}}, {"Sender", []*User{sender}},
{"To", e.To}, {"Cc", e.Cc}, {"Bcc", e.Bcc}, {"Reply-To", e.ReplyTo},
} {
if len(field.users) == 0 {
continue
}
addresses := make([]*mail.Address, 0, len(field.users))
for _, user := range field.users {
if user == nil || user.EmailAddress == "" {
return nil, errors.New("invalid recipient")
}
addresses = append(addresses, &mail.Address{Name: user.Name, Address: user.EmailAddress})
}
h.SetAddressList(field.name, addresses)
}
h.SetContentType("multipart/mixed", map[string]string{"boundary": boundary + "_mixed"})
var output bytes.Buffer
writer, err := message.CreateWriter(&output, h.Header)
if err != nil {
return nil, err
}
var alternative message.Header
alternative.SetContentType("multipart/alternative", map[string]string{"boundary": boundary + "_alternative"})
inline, err := writer.CreatePart(alternative)
if err != nil {
return nil, err
}
writePart := func(parent *message.Writer, header message.Header, content []byte) error {
header.Set("Content-Transfer-Encoding", "base64")
part, err := parent.CreatePart(header)
if err != nil {
return err
}
return writeMessagePart(part, content)
}
if len(e.Text) > 0 || len(e.HTML) == 0 {
var header message.Header
header.SetContentType("text/plain", map[string]string{"charset": "utf-8"})
if err := writePart(inline, header, e.Text); err != nil {
return nil, err
}
}
if len(e.HTML) > 0 {
var header message.Header
header.SetContentType("text/html", map[string]string{"charset": "utf-8"})
if err := writePart(inline, header, e.HTML); err != nil {
return nil, err
}
}
if err := inline.Close(); err != nil {
return nil, err
}
for _, attachment := range e.Attachments {
if attachment == nil {
return nil, errors.New("nil attachment")
}
var header mail.AttachmentHeader
contentType := attachment.ContentType
if contentType == "" {
contentType = "application/octet-stream"
}
header.Set("Content-Type", contentType)
header.SetFilename(attachment.Filename)
if attachment.ContentID != "" {
header.Set("Content-ID", fmt.Sprintf("<%s>", attachment.ContentID))
}
if err := writePart(writer, header.Header, attachment.Content); err != nil {
return nil, err
}
}
if err := writer.Close(); err != nil {
return nil, err
}
return output.Bytes(), nil
}