fix(imap): 新邮件推送补发 EXISTS 通知,修复 Apple Mail 等客户端需手动同步
问题:go-imap 更新通道只能表达 FETCH/EXPUNGE 类更新,新邮件到达时 客户端(IDLE 中)只收到裸 FETCH。不少客户端(如 Apple Mail)只认 RFC 2177 要求的 EXISTS 才会唤醒拉取,表现为'必须手动点击同步'。 修复:PushNewMessage 先向所有已选中该邮箱的匹配连接直接写入 '* N EXISTS'(Dovecot 同款行为),再广播 FETCH 更新。 实测 IDLE 客户端按序收到 '* 21 EXISTS' + '* 1 FETCH (UID ...)'。
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
"mail_go/internal/store"
|
"mail_go/internal/store"
|
||||||
"mail_go/internal/tlsutil"
|
"mail_go/internal/tlsutil"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
"github.com/emersion/go-imap/backend"
|
"github.com/emersion/go-imap/backend"
|
||||||
imapserver "github.com/emersion/go-imap/server"
|
imapserver "github.com/emersion/go-imap/server"
|
||||||
)
|
)
|
||||||
@@ -65,6 +66,12 @@ func (s *IMAPServer) PushNewMessage(userEmail string, msg *db.Message) {
|
|||||||
if update == nil {
|
if update == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// 先发 EXISTS 通知再发 FETCH 更新:RFC 2177(IDLE)要求新邮件到达
|
||||||
|
// 时服务器发送 EXISTS,不少客户端(如 Apple Mail)只认 EXISTS 才会
|
||||||
|
// 唤醒并主动拉取,仅裸 FETCH 更新会被忽略(表现为"必须手动同步")。
|
||||||
|
if count, err := s.stores.Mails.CountByUserAndFolder(msg.UserID, "INBOX"); err == nil {
|
||||||
|
s.pushExists(userEmail, "INBOX", uint32(count))
|
||||||
|
}
|
||||||
s.broadcastUpdate(update, userEmail, msg.ID)
|
s.broadcastUpdate(update, userEmail, msg.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +119,40 @@ func (s *IMAPServer) broadcastUpdate(update backend.Update, userEmail string, ms
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// pushExists 向所有监听器中「已登录该用户且已选中该邮箱」的连接直接写入
|
||||||
|
// 未请求的 "* N EXISTS" 响应(绕过 go-imap 更新通道——其仅支持 FETCH/
|
||||||
|
// EXPUNGE 类更新,无法表达 EXISTS)。通道满时非阻塞丢弃,与广播一致。
|
||||||
|
func (s *IMAPServer) pushExists(userEmail, mailbox string, exists uint32) {
|
||||||
|
s.beMu.Lock()
|
||||||
|
srvs := append([]*imapserver.Server(nil), s.srvs...)
|
||||||
|
s.beMu.Unlock()
|
||||||
|
|
||||||
|
for _, srv := range srvs {
|
||||||
|
srv.ForEachConn(func(conn imapserver.Conn) {
|
||||||
|
ctx := conn.Context()
|
||||||
|
if ctx == nil || ctx.User == nil || ctx.Mailbox == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.User.Username() != userEmail || ctx.Mailbox.Name() != mailbox {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case ctx.Responses <- existsResponse(exists):
|
||||||
|
default:
|
||||||
|
log.Printf("IMAP: EXISTS 推送通道已满,丢弃 (user=%s mailbox=%s)", userEmail, mailbox)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// existsResponse 序列化为 "* N EXISTS\r\n"。
|
||||||
|
type existsResponse uint32
|
||||||
|
|
||||||
|
func (n existsResponse) WriteTo(w *imap.Writer) error {
|
||||||
|
_, err := fmt.Fprintf(w, "* %d EXISTS\r\n", uint32(n))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// cloneUpdate 按类型复制一条 backend.Update:载荷(消息/序号)共享,
|
// cloneUpdate 按类型复制一条 backend.Update:载荷(消息/序号)共享,
|
||||||
// 但 Username/Mailbox/Done channel 重置为独立实例。
|
// 但 Username/Mailbox/Done channel 重置为独立实例。
|
||||||
func cloneUpdate(u backend.Update) backend.Update {
|
func cloneUpdate(u backend.Update) backend.Update {
|
||||||
|
|||||||
Reference in New Issue
Block a user