Ruby的Net::IMAP库里,uid_fetch 和 seq_fetch 是两个经常被拿来对比的方法。它们看起来都用于获取邮件数据,但底层分别对应IMAP协议的UID FETCH命令和SEQUENCE FETCH命令。如果不清楚UID与序列号(sequence number)的差异,很容易在删除邮件、移动邮件或者多客户端同步时拿到错误的数据。这篇文章先把两者的定位方式讲清楚,再通过代码示例说明各自的参数和适用场景。

UID与序列号不是一回事
IMAP服务器在管理一个文件夹时,会给每封邮件同时分配两种编号。序列号是邮件在当前文件夹里的位置编号,从1开始连续递增。比如收件箱有5封邮件,它们的序列号就是1、2、3、4、5。这个编号非常直观,但它是动态的:一旦删除第2封,原来的第3封会立刻变成第2封,后面的邮件依次前移。也就是说,序列号只在当前选中的文件夹视图中有效,并且会随着邮件的增删而发生变化。
UID则不同,它是服务器为邮件分配的唯一标识,在同一文件夹内不会被前面的删除操作影响。只要邮件本身没有被删除或者文件夹没有被重建,UID就保持不变。可以把它理解成邮件的身份证号,而序列号更像是排队时的临时站位。IMAP协议还通过UIDVALIDITY来标识UID的稳定性:如果文件夹被重建,UIDVALIDITY会变化,此时旧的UID就可能失效。因此在使用UID做长期缓存时,还需要关注UIDVALIDITY是否改变。
require 'net/imap'
imap = Net::IMAP.new('imap.ipipp.com')
imap.login('user@ipipp.com', 'password')
imap.select('INBOX')
# 获取所有邮件的序列号
seqs = imap.search(['ALL'])
puts "序列号:#{seqs.inspect}"
# 获取所有邮件的UID
uids = imap.uid_search(['ALL'])
puts "UID:#{uids.inspect}"
运行后可以发现,在一个从未整理的邮箱里,两者的值可能恰好相同,但这只是初始状态下的巧合。一旦发生过删除,序列号会重新排列,而UID仍然保留原来的值。因此不能把两者混用,也不应该假设UID和序列号在任何时候都相等。
uid_fetch与seq_fetch的参数和返回结构
从Ruby方法签名上看,uid_fetch 和 seq_fetch 几乎一致,都接收两个参数:第一个参数是编号集合,第二个参数是要获取的属性列表。区别在于前者期望传入UID,后者期望传入序列号。编号集合可以是单个整数、整数数组或者Range对象,属性列表通常用字符串数组,例如["ENVELOPE", "FLAGS", "INTERNALDATE"]。如果省略属性列表,服务器可能只返回默认的基础信息。
下面这段代码先拿到第一封邮件的序列号和UID,再分别调用两个方法。注意变量名要对应清楚,避免传入错误类型。
# 假设已经 select('INBOX')
first_seq = imap.search(['ALL']).first
first_uid = imap.uid_search(['ALL']).first
# 按序列号获取
seq_data = imap.seq_fetch(first_seq, ['ENVELOPE', 'FLAGS', 'UID'])
puts "seq_fetch 返回:#{seq_data.first.seqno} / UID #{seq_data.first.attr['UID']}"
# 按UID获取
uid_data = imap.uid_fetch(first_uid, ['ENVELOPE', 'FLAGS', 'UID'])
puts "uid_fetch 返回:#{uid_data.first.seqno} / UID #{uid_data.first.attr['UID']}"
两个方法返回的都是Net::IMAP::FetchData对象数组。每个FetchData里包含seqno和attr两部分。seqno始终是邮件当前的序列号,即使用uid_fetch查询也是如此;attr是一个哈希,键是属性名,值是对应的数据。如果属性里包含UID,就可以在attr["UID"]里取到UID值。这个设计让两种调用方式可以交叉核对。
另外,uid_fetch 允许在一次请求中传入UID集合,但不要把它和序列号混在一个数组里。如果想同时获取多封邮件,可以写成imap.uid_fetch([10, 11, 12], ['FLAGS'])或者imap.uid_fetch(10..20, ['ENVELOPE'])。seq_fetch 的集合写法完全一样,只是数字含义不同。
实际应用中该怎么选
如果需要跨多次连接、跨不同请求准确定位同一封邮件,应该优先使用UID。例如把邮件同步到本地数据库时,记录UID作为主键,下次连接后用uid_fetch按UID补取新增或变更的数据。因为序列号在连接期间可能因为其他客户端删除邮件而改变,用它做持久标识并不可靠。只要服务器没有重建文件夹,UID通常能保证同一封邮件在多次会话中保持一致。
如果只是本次会话中临时操作当前视图,比如打开收件箱后快速查看第几封,序列号会简单一些。可以通过imap.search(['UNSEEN'])直接得到未读邮件的序列号,然后seq_fetch读取。很多IMAP客户端在展示列表时本身就用序号,这时用seq_fetch可以减少一次UID转换,代码也更直观。
一个常见的坑是:先用search得到序列号列表,处理过程中删除了其中一封,然后继续用旧的序列号调用seq_fetch,结果可能拿到另一封邮件。要避免这个问题,可以在获取列表后立即处理,或者在删除后重新查询。更稳妥的方式是用uid_search和uid_fetch,因为UID不会因为前面的删除而前移。
还有一种场景是标记邮件状态。比如把第3封标记为已读,可以用seq_store;如果要精确到某一封已知UID的邮件,用uid_store更安全。两者对应的逻辑和fetch完全一致。总的来说,凡是要保存标识或跨操作引用,UID是更好的选择;凡是一次性、短暂的当前列表操作,序列号也完全够用。理解这一层后,就不会再纠结于方法名相似的问题了。