fix(imap): 修复 Thunderbird 未读状态丢失(SQLite 并发写 + 客户端 seq 错位)
A. SQLite 并发写失败被吞(高概率根因): - DSN 追加 _busy_timeout=5000&_journal_mode=WAL&_synchronous=NORMAL, WAL 下读不阻塞写,消除瞬时 SQLITE_BUSY - UpdateMessagesFlags 不再吞 MarkReadState/MarkFlagged 错误: 出错记日志并返回 error(客户端收到 NO 会重试) - Web MarkRead/Delete 同款静默丢错补日志 B. 客户端 seq 视图错位(集成测试复现后修复): - 真实服务器+脚本客户端测试复现:客户端按日期倒序自编号发 seq 式 STORE 时,旧排序(id ASC)把最旧邮件标为已读 - 规范排序改为 date DESC, id DESC(最新在前),与主流客户端 默认视图一致;buildNewMessageUpdate 改用 seqOf 取真实序号 - 新增 UID STORE / 服务器下发 seq STORE / 自编号 seq STORE 集成测试 注:go-imap v1.2.1 存在库内 *conn.silent() 数据竞争(启用推送后 必然触发),集成测试以 //go:build !race 排除,-race 下由单元测试 覆盖推送逻辑
This commit is contained in:
@@ -45,14 +45,14 @@ func (b *imapBackend) Updates() <-chan backend.Update {
|
||||
}
|
||||
|
||||
// buildNewMessageUpdate 为一条新投递到 mailbox 的邮件构造 IMAP 更新。
|
||||
// seq 取该邮件在邮箱中的序号(通知时机在入库之后,取当前列表长度)。
|
||||
// seq 取该邮件在邮箱中的实际序号(最新在前,通常为 1)。
|
||||
func buildNewMessageUpdate(stores *store.Stores, userEmail, mailbox string, msg *db.Message) *backend.MessageUpdate {
|
||||
if stores == nil || msg == nil || userEmail == "" || mailbox == "" {
|
||||
return nil
|
||||
}
|
||||
seq := uint32(1)
|
||||
if msgs, err := stores.Mails.ListAllByUserAndFolder(msg.UserID, mailbox); err == nil {
|
||||
seq = uint32(len(msgs))
|
||||
seq := seqOf(stores, msg.UserID, mailbox, msg.ID)
|
||||
if seq == 0 {
|
||||
seq = 1
|
||||
}
|
||||
|
||||
imapMsg := imap.NewMessage(seq, []imap.FetchItem{imap.FetchUid, imap.FetchFlags, imap.FetchInternalDate, imap.FetchRFC822Size, imap.FetchEnvelope})
|
||||
@@ -751,6 +751,10 @@ func (m *imapMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap
|
||||
return err
|
||||
}
|
||||
|
||||
// 记录首个持久化错误:SQLite 忙/锁等瞬时失败必须让客户端感知
|
||||
// (返回 NO 触发重试),否则已读/星标会静默丢失。
|
||||
var firstErr error
|
||||
|
||||
for i, dbMsg := range dbMessages {
|
||||
var match bool
|
||||
if uid {
|
||||
@@ -770,9 +774,15 @@ func (m *imapMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap
|
||||
applyFlag := func(flag string, enabled bool) {
|
||||
switch flag {
|
||||
case "\\Seen":
|
||||
_ = m.stores.Mails.MarkReadState(dbMsg.ID, enabled)
|
||||
if err := m.stores.Mails.MarkReadState(dbMsg.ID, enabled); err != nil && firstErr == nil {
|
||||
log.Printf("IMAP: mark read state for msg %d failed: %v", dbMsg.ID, err)
|
||||
firstErr = err
|
||||
}
|
||||
case "\\Flagged":
|
||||
_ = m.stores.Mails.MarkFlagged(dbMsg.ID, enabled)
|
||||
if err := m.stores.Mails.MarkFlagged(dbMsg.ID, enabled); err != nil && firstErr == nil {
|
||||
log.Printf("IMAP: mark flagged for msg %d failed: %v", dbMsg.ID, err)
|
||||
firstErr = err
|
||||
}
|
||||
case "\\Deleted":
|
||||
if enabled {
|
||||
m.deleted[dbMsg.ID] = true
|
||||
@@ -807,7 +817,7 @@ func (m *imapMailbox) UpdateMessagesFlags(uid bool, seqset *imap.SeqSet, op imap
|
||||
pushUpdate(m.user.updates, buildFlagsUpdate(m.stores, m.user.email, m.name, fresh, deleted))
|
||||
}
|
||||
|
||||
return nil
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// CopyMessages copies messages to another mailbox.
|
||||
|
||||
Reference in New Issue
Block a user