Smail/backend/dto/parsemail/imap.go
smail 6f79441585 修复审查发现的认证配置邮件协议及前端问题
补齐安全运行时清理、配置候选恢复、泛域名证书事务提交、邮件副本与协议边界、插件隐私和安装回滚回归,并更新修复状态及待验收清单。
2026-09-06 13:02:02 +08:00

132 lines
3.8 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 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)
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
}