fix(imap): FETCH BODY/BODYSTRUCTURE 解析失败时服务器 panic 导致客户端只能取到部分邮件
根因:backendutil.FetchBodyStructure 对部分消息返回 nil(典型场景: - message/rfc822 附件为 base64 编码时库内不解码,把编码文本当嵌套 消息头解析报错(转发邮件场景,如 .mail-monitor/forward.py 转发) - multipart 缺少结束边界(截断)时 extended 解析报错) buildIMAPMessage 未处理 nil,go-imap 格式化 FETCH 响应时在 send() 协程 nil 指针解引用 panic,连接中断——Thunderbird 只取到崩溃前已 发送的几封邮件,手机客户端一直卡在"正在获取邮件"。 修复: - BodyStructure 解析失败时降级为 text/plain 单段结构,杜绝 nil - FetchBodySection 返回 nil 时跳过该 section,不再写入 nil literal - 新增集成测试 TestFetchBodyMalformedMIME 覆盖两类畸形 MIME
This commit is contained in:
@@ -461,6 +461,15 @@ func (m *imapMailbox) buildIMAPMessage(dbMsg *db.Message, seqNum uint32, items [
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
imapMsg.BodyStructure, _ = backendutil.FetchBodyStructure(hdr, body, item == imap.FetchBodyStructure)
|
imapMsg.BodyStructure, _ = backendutil.FetchBodyStructure(hdr, body, item == imap.FetchBodyStructure)
|
||||||
}
|
}
|
||||||
|
// 防御:FetchBodyStructure 对部分合法/畸形 MIME 会失败并返回
|
||||||
|
// nil(典型:message/rfc822 附件为 base64 编码时库内不解码
|
||||||
|
// 直接按嵌套消息解析头;或 multipart 边界截断)。BodyStructure
|
||||||
|
// 为 nil 时 go-imap 格式化 FETCH 响应会在 send() 协程 panic
|
||||||
|
// (nil 指针解引用),连接中断导致客户端只收到部分邮件甚至
|
||||||
|
// 一直卡在同步。解析失败时降级为 text/plain 单段结构。
|
||||||
|
if imapMsg.BodyStructure == nil {
|
||||||
|
imapMsg.BodyStructure = fallbackBodyStructure(rawMsg)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
section, err := imap.ParseBodySectionName(item)
|
section, err := imap.ParseBodySectionName(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -471,13 +480,33 @@ func (m *imapMailbox) buildIMAPMessage(dbMsg *db.Message, seqNum uint32, items [
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
literal, _ := backendutil.FetchBodySection(hdr, body, section)
|
literal, _ := backendutil.FetchBodySection(hdr, body, section)
|
||||||
imapMsg.Body[section] = literal
|
if literal != nil {
|
||||||
|
imapMsg.Body[section] = literal
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return imapMsg, nil
|
return imapMsg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fallbackBodyStructure 构造一个 text/plain 单段 BodyStructure,用于
|
||||||
|
// MIME 解析失败的消息(保证 FETCH BODY/BODYSTRUCTURE 不因 nil 崩溃)。
|
||||||
|
func fallbackBodyStructure(raw []byte) *imap.BodyStructure {
|
||||||
|
size := uint32(len(raw))
|
||||||
|
lines := uint32(bytes.Count(raw, []byte{'\n'}))
|
||||||
|
if len(raw) > 0 && raw[len(raw)-1] != '\n' {
|
||||||
|
lines++
|
||||||
|
}
|
||||||
|
return &imap.BodyStructure{
|
||||||
|
MIMEType: "text",
|
||||||
|
MIMESubType: "plain",
|
||||||
|
Params: map[string]string{"charset": "utf-8"},
|
||||||
|
Encoding: "8bit",
|
||||||
|
Size: size,
|
||||||
|
Lines: lines,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func messageRawData(msg *db.Message) []byte {
|
func messageRawData(msg *db.Message) []byte {
|
||||||
if msg.RawData != "" {
|
if msg.RawData != "" {
|
||||||
return []byte(msg.RawData)
|
return []byte(msg.RawData)
|
||||||
|
|||||||
@@ -188,3 +188,95 @@ func TestSeqStoreClientSelfNumbered(t *testing.T) {
|
|||||||
// 客户端意图是标记最新一封(ids[2])为已读
|
// 客户端意图是标记最新一封(ids[2])为已读
|
||||||
assertReadState(t, stores, ids[2], true)
|
assertReadState(t, stores, ids[2], true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestFetchBodyMalformedMIME 回归:消息包含无法解析的 MIME(base64 编码的
|
||||||
|
// message/rfc822 附件 / 截断的 multipart)时,FETCH BODY/BODYSTRUCTURE
|
||||||
|
// 不得因 nil BodyStructure 触发服务器 panic(否则连接中断,客户端只收到
|
||||||
|
// 部分邮件或一直卡在同步)。修复前 go-imap send() 协程会 nil 指针崩溃。
|
||||||
|
func TestFetchBodyMalformedMIME(t *testing.T) {
|
||||||
|
stores, addr := startIntegrationServer(t)
|
||||||
|
|
||||||
|
// 1) base64 编码的 message/rfc822 附件(转发邮件场景):
|
||||||
|
// backendutil.FetchBodyStructure 不解码 base64,直接把编码文本
|
||||||
|
// 当嵌套消息头解析 → "malformed MIME header line" 错误。
|
||||||
|
rfc822Body := "UmVjZWl2ZWQ6IGZyb20gb3V0Ym91bmQuY2kuaWNsb3VkLmNvbSAodW5rbm93biBbMTI3LjAuMC4yKVxuXHQgYnkgcDAwLWljbG91ZG10YS1hc210cC11cy1jZW50cmFsLTFrLTEwMC1wZXJjZW50LTggKFBvc3RmaXgpIHdpdGggRVNNVFBTIGlkIDIxRTlBMThDQURDRjM4MlxuXHQgZm9yIDxkc2hAbG12ZS5uZXQ+OyBTdW4sIDE2IEF1ZyAyMDI2IDEzOjU4OjIxICswMDAwIChVVEMpXG5YLUlDTC1SZXBJZDogRURWY1BlQ3RlWG4tZ0Z1T0xxUWhfSjZvcE9fN1B2OEtsOW1mMDg2VUFxZ29zXG5EYXRlOiBTdW4sIDE2IEF1ZyAyMDI2IDEzOjU4OjIxICswMDAwXG5Gcm9tOiBkYXZpZEB5YW5kZXguY29tXG5UbzogZHNoQGxtdmUubmV0XG5NZXNzYWdlLUlEOiA8QTIxNzBEMTEtMkI1MC00MTQwLTlEQTMtMkI3M0U2RUIwQTc4QHlhbmRleC5jb20+XG5TdWJqZWN0OiB0ZXN0XG5cbmhlbGxvXG4="
|
||||||
|
msgWithRFC822 := &db.Message{
|
||||||
|
UserID: 1,
|
||||||
|
Folder: "INBOX",
|
||||||
|
FromAddr: "alice@example.com",
|
||||||
|
ToAddr: "alice@example.com",
|
||||||
|
Subject: "fwd",
|
||||||
|
Date: time.Now().Add(-2 * time.Hour),
|
||||||
|
RawData: "From: alice@example.com\r\n" +
|
||||||
|
"To: alice@example.com\r\n" +
|
||||||
|
"Subject: fwd\r\n" +
|
||||||
|
"MIME-Version: 1.0\r\n" +
|
||||||
|
"Content-Type: multipart/mixed; boundary=\"==fwd==\"\r\n\r\n" +
|
||||||
|
"--==fwd==\r\n" +
|
||||||
|
"Content-Type: text/plain; charset=\"utf-8\"\r\n" +
|
||||||
|
"Content-Transfer-Encoding: 8bit\r\n\r\n" +
|
||||||
|
"正文\r\n\r\n" +
|
||||||
|
"--==fwd==\r\n" +
|
||||||
|
"Content-Type: message/rfc822\r\n" +
|
||||||
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
|
"Content-Disposition: attachment; filename=\"original.eml\"\r\n" +
|
||||||
|
"MIME-Version: 1.0\r\n\r\n" +
|
||||||
|
rfc822Body + "\r\n" +
|
||||||
|
"--==fwd==--\r\n",
|
||||||
|
}
|
||||||
|
// 2) 截断的 multipart(缺少结束边界):BODYSTRUCTURE(extended) 解析报错
|
||||||
|
msgTruncated := &db.Message{
|
||||||
|
UserID: 1,
|
||||||
|
Folder: "INBOX",
|
||||||
|
FromAddr: "alice@example.com",
|
||||||
|
ToAddr: "alice@example.com",
|
||||||
|
Subject: "truncated",
|
||||||
|
Date: time.Now().Add(-1 * time.Hour),
|
||||||
|
RawData: "From: alice@example.com\r\n" +
|
||||||
|
"To: alice@example.com\r\n" +
|
||||||
|
"Subject: truncated\r\n" +
|
||||||
|
"MIME-Version: 1.0\r\n" +
|
||||||
|
"Content-Type: multipart/alternative; boundary=\"==trunc==\"\r\n\r\n" +
|
||||||
|
"--==trunc==\r\n" +
|
||||||
|
"Content-Type: text/plain\r\n\r\n" +
|
||||||
|
"hello\r\n",
|
||||||
|
// 无结束边界
|
||||||
|
}
|
||||||
|
if err := stores.Mails.Create(msgWithRFC822); err != nil {
|
||||||
|
t.Fatalf("create msg: %v", err)
|
||||||
|
}
|
||||||
|
if err := stores.Mails.Create(msgTruncated); err != nil {
|
||||||
|
t.Fatalf("create msg: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := loginAndSelect(t, addr)
|
||||||
|
|
||||||
|
seqset := new(imap.SeqSet)
|
||||||
|
seqset.AddRange(1, 2)
|
||||||
|
|
||||||
|
// BODY:历史上 message/rfc822 消息解析失败 → nil BodyStructure → panic
|
||||||
|
msgs := make(chan *imap.Message, 10)
|
||||||
|
if err := c.Fetch(seqset, []imap.FetchItem{imap.FetchBody}, msgs); err != nil {
|
||||||
|
t.Fatalf("fetch body: %v", err)
|
||||||
|
}
|
||||||
|
got := 0
|
||||||
|
for range msgs {
|
||||||
|
got++
|
||||||
|
}
|
||||||
|
if got != 2 {
|
||||||
|
t.Fatalf("FETCH BODY 返回 %d/2 封", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BODYSTRUCTURE:截断 multipart 在 extended 解析时报错 → nil → panic
|
||||||
|
msgs2 := make(chan *imap.Message, 10)
|
||||||
|
if err := c.Fetch(seqset, []imap.FetchItem{imap.FetchBodyStructure}, msgs2); err != nil {
|
||||||
|
t.Fatalf("fetch bodystructure: %v", err)
|
||||||
|
}
|
||||||
|
got2 := 0
|
||||||
|
for range msgs2 {
|
||||||
|
got2++
|
||||||
|
}
|
||||||
|
if got2 != 2 {
|
||||||
|
t.Fatalf("FETCH BODYSTRUCTURE 返回 %d/2 封", got2)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user