fix: generate Message-ID once and persist to DB (#328)
Previously, a random suffix was generated every time BuildBytes or ForwardBuildBytes was called, causing IMAP/POP3 clients to treat the same message as a new message on every sync and breaking conversation threading. Changes: - Add msg_id column to the email table (synced automatically via Sync2) - Generate a crypto/rand-based RFC-compliant Message-ID once at email creation time (send.go for outgoing, read_content.go for incoming) - Preserve the original Message-ID from externally received emails - Load the persisted MsgID when rebuilding Email from DB records - Use the stored MsgID in BuildBytes, ForwardBuildBytes and the IMAP ENVELOPE response; fall back to the old numeric format for legacy rows Fixes the MSGID_SHORT SpamAssassin penalty and Gmail rejection while keeping Message-IDs stable across syncs. Co-authored-by: root <root@Claw-ai.tail1478c.ts.net> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
826ac26636
commit
23c2fbbdd5
@ -191,6 +191,7 @@ func Send(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
CronSendTime: time.Now(),
|
||||
Status: 1,
|
||||
CreateTime: time.Now(),
|
||||
MsgID: parsemail.GenerateMsgID(config.Instance.Domain),
|
||||
}
|
||||
|
||||
_, err = db.Instance.Insert(&modelEmail)
|
||||
@ -202,6 +203,7 @@ func Send(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
e.MessageId = cast.ToInt64(modelEmail.Id)
|
||||
e.MsgID = modelEmail.MsgID
|
||||
|
||||
async.New(ctx).Process(func(p any) {
|
||||
errMsg := ""
|
||||
|
||||
@ -2,6 +2,7 @@ package parsemail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@ -68,9 +69,18 @@ type Email struct {
|
||||
Date string
|
||||
Status int // 0未发送,1已发送,2发送失败,3删除,5广告邮件
|
||||
MessageId int64
|
||||
MsgID string // RFC-compliant Message-ID, persisted in DB
|
||||
Size int
|
||||
}
|
||||
|
||||
// GenerateMsgID creates an RFC-compliant Message-ID unique enough to avoid spam filters.
|
||||
// It must be called once at email creation time and the result persisted to DB.
|
||||
func GenerateMsgID(domain string) string {
|
||||
randBytes := make([]byte, 16)
|
||||
_, _ = rand.Read(randBytes)
|
||||
return fmt.Sprintf("%x.%d@%s", randBytes, time.Now().UnixNano(), domain)
|
||||
}
|
||||
|
||||
// Xss filter policy
|
||||
var (
|
||||
strictPolicy *bluemonday.Policy
|
||||
@ -193,6 +203,7 @@ func NewEmailFromModel(d models.Email) *Email {
|
||||
|
||||
return &Email{
|
||||
MessageId: cast.ToInt64(d.Id),
|
||||
MsgID: d.MsgID,
|
||||
From: &User{
|
||||
Name: d.FromName,
|
||||
EmailAddress: d.FromAddress,
|
||||
@ -218,6 +229,10 @@ func NewEmailFromReader(to []string, r io.Reader, size int) *Email {
|
||||
}
|
||||
|
||||
ret.Size = size
|
||||
// Preserve the original Message-ID from the sender so it is stored and reused consistently.
|
||||
if mid := m.Header.Get("Message-Id"); mid != "" {
|
||||
ret.MsgID = strings.TrimPrefix(strings.TrimSuffix(strings.TrimSpace(mid), ">"), "<")
|
||||
}
|
||||
ret.From = buildUser(m.Header.Get("From"))
|
||||
|
||||
smtpTo := buildUsers(to)
|
||||
@ -402,7 +417,11 @@ func (e *Email) ForwardBuildBytes(ctx *context.Context, sender *models.User) []b
|
||||
h.SetAddressList("Sender", senderAddress)
|
||||
h.SetAddressList("To", to)
|
||||
h.SetText("Subject", e.Subject)
|
||||
h.SetMessageID(fmt.Sprintf("%d@%s", e.MessageId, config.Instance.Domain))
|
||||
if e.MsgID != "" {
|
||||
h.SetMessageID(e.MsgID)
|
||||
} else {
|
||||
h.SetMessageID(fmt.Sprintf("%d@%s", e.MessageId, config.Instance.Domain))
|
||||
}
|
||||
if len(e.Cc) != 0 {
|
||||
cc := []*mail.Address{}
|
||||
for _, user := range e.Cc {
|
||||
@ -553,7 +572,11 @@ func (e *Email) BuildBytes(ctx *context.Context, dkim bool) []byte {
|
||||
} else {
|
||||
h.SetDate(time.Now())
|
||||
}
|
||||
h.SetMessageID(fmt.Sprintf("%d@%s", e.MessageId, config.Instance.Domain))
|
||||
if e.MsgID != "" {
|
||||
h.SetMessageID(e.MsgID)
|
||||
} else {
|
||||
h.SetMessageID(fmt.Sprintf("%d@%s", e.MessageId, config.Instance.Domain))
|
||||
}
|
||||
h.SetAddressList("From", from)
|
||||
h.SetAddressList("Sender", from)
|
||||
h.SetAddressList("To", to)
|
||||
|
||||
@ -88,8 +88,13 @@ func buildEnvelope(email *response.EmailResponseData, traEmail *parsemail.Email)
|
||||
replyTo = from
|
||||
}
|
||||
|
||||
// Message-ID
|
||||
messageID := fmt.Sprintf("<%d@%s>", email.Id, config.Instance.Domain)
|
||||
// Message-ID: use the persisted value so IMAP clients see a stable ID on every sync.
|
||||
var messageID string
|
||||
if email.MsgID != "" {
|
||||
messageID = fmt.Sprintf("<%s>", email.MsgID)
|
||||
} else {
|
||||
messageID = fmt.Sprintf("<%d@%s>", email.Id, config.Instance.Domain)
|
||||
}
|
||||
|
||||
return &imap.Envelope{
|
||||
Date: email.CreateTime,
|
||||
|
||||
@ -221,6 +221,11 @@ func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserI
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
msgID := email.MsgID
|
||||
if msgID == "" {
|
||||
msgID = parsemail.GenerateMsgID(config.Instance.Domain)
|
||||
}
|
||||
|
||||
modelEmail := models.Email{
|
||||
Type: cast.ToInt8(emailType),
|
||||
Subject: email.Subject,
|
||||
@ -242,6 +247,7 @@ func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserI
|
||||
Status: cast.ToInt8(email.Status),
|
||||
CreateTime: time.Now(),
|
||||
CronSendTime: time.Now(),
|
||||
MsgID: msgID,
|
||||
}
|
||||
|
||||
_, err := db.Instance.Insert(&modelEmail)
|
||||
@ -252,6 +258,7 @@ func saveEmail(ctx *context.Context, size int, email *parsemail.Email, sendUserI
|
||||
|
||||
if modelEmail.Id > 0 {
|
||||
email.MessageId = cast.ToInt64(modelEmail.Id)
|
||||
email.MsgID = modelEmail.MsgID
|
||||
}
|
||||
// 收信人信息
|
||||
var users []*models.User
|
||||
|
||||
@ -30,6 +30,7 @@ type Email struct {
|
||||
Error sql.NullString `xorm:"error text comment('投递错误信息')" json:"error"`
|
||||
SendDate time.Time `xorm:"send_date comment('投递时间')" json:"send_date"`
|
||||
CreateTime time.Time `xorm:"create_time created" json:"create_time"`
|
||||
MsgID string `xorm:"msg_id varchar(255) notnull default('') comment('RFC-compliant Message-ID, generated once on creation')" json:"msg_id"`
|
||||
}
|
||||
|
||||
func (d *Email) TableName() string {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user