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:
dsh
2026-08-19 08:52:39 -04:00
parent 07ba01a6f6
commit 3a4636c2c5
2 changed files with 122 additions and 1 deletions
+30 -1
View File
@@ -461,6 +461,15 @@ func (m *imapMailbox) buildIMAPMessage(dbMsg *db.Message, seqNum uint32, items [
if err == nil {
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:
section, err := imap.ParseBodySectionName(item)
if err != nil {
@@ -471,13 +480,33 @@ func (m *imapMailbox) buildIMAPMessage(dbMsg *db.Message, seqNum uint32, items [
return nil, err
}
literal, _ := backendutil.FetchBodySection(hdr, body, section)
imapMsg.Body[section] = literal
if literal != nil {
imapMsg.Body[section] = literal
}
}
}
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 {
if msg.RawData != "" {
return []byte(msg.RawData)