Golang使用go-imap移动两类邮件报指定消息集无效的解决方法
我们有用于接收客户邮件的邮箱,对邮件做后续处理前,需要先校验邮件是否符合约定规则:包括邮件主题需匹配指定正则、单封邮件仅允许携带1个附件等。当前使用如下代码实现邮件校验与分类移动逻辑:
import ( "fmt" "imaptest/src/db" "io" "log" "path/filepath" "strings" "github.com/emersion/go-imap" "github.com/emersion/go-imap/client" "github.com/emersion/go-message" "github.com/iglin/go-config" ) (...) //Channel for the messages messages := make(chan *imap.Message, 10) //Channel for retrieve if an error appeared done := make(chan error, 1) go func() { seqset := new(imap.SeqSet) seqset.AddRange(1, mbox.Messages) //Fetch the messages and push them into the channel done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchRFC822}, messages) }() //SeqSet for invalid messages seqsetErrorMessages := new(imap.SeqSet) //SeqSet for valid messages seqsetValidMessages := new(imap.SeqSet) for msg := range messages { //Different checks like are there attachments //Matches the subject with a specific regex if isMessageValid(msg) { //Message is valid seqsetValidMessages.AddNum(msg.SeqNum) } else { //Message is invalid seqsetErrorMessages.AddNum(msg.SeqNum) } } //Check if there was an error when fetching the messages if err := <-done; err != nil { log.Fatal(err) } //Move all invalid messages to error if !seqsetErrorMessages.Empty() { if err := c.Move(seqsetErrorMessages, FOLDER_ERROR); err != nil { log.Fatalf("Error on move to %s: %v", FOLDER_ERROR, err) } } //Move all valid messages to toExport if !seqsetValidMessages.Empty() { if err := c.Move(seqsetValidMessages, FOLDER_OUT); err != nil { log.Fatalf("Error on move to %s: %v", FOLDER_OUT, err) } }
代码中FOLDER_OUT和FOLDER_ERROR是预先定义的字符串常量,对应邮箱内已经存在的两个目标文件夹。
当源文件夹内所有邮件均为有效邮件、或均为无效邮件时,邮件移动逻辑可正常执行,所有邮件都能成功移动到对应文件夹;但当源文件夹内同时存在有效、无效两类邮件时,程序会抛出如下错误:
Error on move to error: The specified message set is invalid.
之前曾尝试替换实现逻辑:使用msg.Uid搭配c.UidMove()方法,替代原有msg.SeqNum搭配c.Move()的实现,但仍然会触发完全相同的错误。
报错核心原因是IMAP协议中的邮件序号(SeqNum)为动态连续分配值:
从源文件夹移动走任意一批邮件后,剩余留存邮件的SeqNum会立即重排,保持从1开始的连续序列。遍历邮件阶段提前收集的有效邮件SeqSet,在第一次移动完无效邮件后,已经和服务器侧实际的邮件序号不匹配,因此会返回「消息集无效」错误。
当文件夹内全为有效或全为无效邮件时,仅会执行一次Move操作,不存在序号重排场景,因此逻辑可正常运行。
之前替换UID方案仍然报错,是因为Fetch邮件时未将imap.FetchUid加入拉取字段列表,导致获取到的msg.Uid均为默认零值,收集的UidSet本身就是非法的。
可任选以下一种方案修复,优先推荐使用UID方案,稳定性更高:
- 方案1:使用永久不变的UID做移动标识(推荐)
UID是邮件在文件夹内的永久唯一标识,不会因为其他邮件被移动、删除发生变化,分两次移动也不会出现标识不匹配问题,修复步骤:- 修改Fetch调用的拉取字段,新增
imap.FetchUid,确保可以正确获取每封邮件的唯一UID:done <- c.Fetch(seqset, []imap.FetchItem{imap.FetchEnvelope, imap.FetchRFC822, imap.FetchUid}, messages) - 收集集合时改用UID存储,移动时调用
c.UidMove()方法即可。
- 修改Fetch调用的拉取字段,新增
- 方案2:保留SeqNum逻辑,调整移动顺序
如果不想修改为UID逻辑,只需要调整两次Move的执行顺序:永远先移动SeqNum数值更大的那一组邮件。移动大序号邮件时,不会改变更小序号邮件的SeqNum值,大序号邮件全部移走后,再移动剩余的小序号邮件,之前收集的小序号SeqSet仍然有效。
实现时需要自行编写方法遍历SeqSet获取两个集合的最大序号,比较后先执行大序号集合的移动操作,再执行小序号集合的移动即可。 - 方案3:两次移动间重新拉取邮件列表
第一次移动完其中一类邮件后,重新选中源文件夹拉取剩余所有邮件的序号,再执行第二次移动。该方案需要多发起一次网络请求,执行效率更低,不做优先推荐。
内容的提问来源于stack exchange,提问作者HarleyDavidson

