134 lines
4.0 KiB
Go
134 lines
4.0 KiB
Go
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
|
||
}
|