IMAP 与 POP3

本页目录

IMAP 让客户端操作服务器上的邮箱、邮件状态和内容,适合多端维护共同视图;POP3 主要提供 maildrop 中消息的列举、下载与删除。两者都可以把邮件缓存在本地,也都不是“下载后必须立即删除”。真正的区别在于服务器向客户端提供了多少可同步的状态与操作。

能力IMAPPOP3
组织消息多邮箱、服务器端状态与搜索以当前 maildrop 消息列表为中心
读取可以只取头部、正文段或附件部分RETR 获取整封;可选 TOP 获取头及部分行
删除标记与清除分离,也有 MOVE 等操作DELE 标记,正常 QUIT 进入更新
多端一致性有 UID、flags、同步扩展等机制没有同等级的共享文件夹/已读同步模型
离线依赖已下载内容与本地操作队列下载后可离线读取
通知IDLE 接收邮箱变化提示基础协议通常由客户端轮询

本文以 IMAP4rev2(RFC 9051)为主要语义依据,遇到 IMAP4rev1 服务要按 CAPABILITY 判断扩展,不能把新旧响应例子随意混用。RFC 9051

IMAP 会话:登录不等于已经选中邮箱

连接经历未认证、已认证、已选择邮箱、退出等状态。LIST 在已认证后列出邮箱;SELECT 选择可读写邮箱,EXAMINE 获取只读视图;FETCH、STORE 等针对已选择邮箱工作。认证成功不会自动意味着所有后续命令都以 INBOX 为目标。

命令带客户端标签,如 a003;相同标签的最终 OK/NO/BAD 表示该命令完成。以 * 开头的未标记响应可以报告数据或状态变化,不是另一条命令的最终应答;+ 是继续输入提示。客户端解析器应处理交错响应,而不是把“读到一行”当作一次调用结束。

示例应如何解读
* 42 EXISTS当前邮箱有 42 封,不是新增 42 封
* OK [UIDVALIDITY 700]当前 UID 命名空间的有效性值
* OK [UIDNEXT 109]对未来 UID 的预测边界,不保证下一封恰好 109
a003 OK [READ-WRITE] SELECT completed选择命令完成,可继续邮箱操作

旧 IMAP4rev1 的 RECENT / \Recent 在 rev2 中已废弃;它也从来不能直接当作跨所有设备一致的未读数量。未读通常依据 \Seen 是否存在。

序号、UID、UIDVALIDITY:缓存键为什么需要三部分

消息序号是当前邮箱中的位置,从 1 开始。前面的邮件被清除后,后面的序号向前移动。UID 在同一邮箱、同一 UIDVALIDITY 下稳定递增但可以有空洞;它不是账户范围的全局消息 ID。

删除会改变当前位置,不改变幸存邮件在该 UIDVALIDITY 下的 UID 序号 1 UID 101 序号 2 UID 104 序号 3 UID 108 序号 1 UID 104 序号 2 UID 108 移除原序号 1 / UID 101 UID 属于邮箱与 UIDVALIDITY 的作用域;跨邮箱移动后的 UID 不能直接沿用。

假设客户端看到序号 2 对应 UID 104,然后另一个客户端移除了序号 1。如果继续按记住的位置执行 STORE 2,当前指向的可能已经是 UID 108。同步器应使用 UID 操作和完整作用域,并及时更新收到的删除/序号变化信息。

uids = [101, 104, 108]
remembered_sequence = 2
remembered_uid = uids[remembered_sequence - 1]
assert remembered_uid == 104
uids.pop(0)  # 原序号1被清除
assert uids[remembered_sequence - 1] == 108
assert uids.index(remembered_uid) + 1 == 1
old_key = ('INBOX', 700, 104)
new_epoch_key = ('INBOX', 701, 104)
assert old_key != new_epoch_key
print('序号移动与 UIDVALIDITY 缓存隔离模型通过')

实际缓存键还应包含账户/服务器标识。UIDVALIDITY 改变时,旧 UID 映射不能继续用来定位服务器邮件;需要重新枚举并建立关联,而不是把旧离线删除命令直接套到新 UID 104。复制或移动到另一邮箱后,目标 UID 也可能改变,支持的 UID 映射响应有助于更新缓存。UIDPLUS RFC 4315

UIDNEXT 与星号范围的细节

UIDNEXT 的值用于预测未来 UID 分配,UID 仍可以跳号。另一个容易误判的点是范围允许端点逆序:若邮箱最大 UID 为 108,UID FETCH 109:* 中星号代表最大的 UID,不能把它简单理解为永远返回空集。客户端应使用适当的搜索/同步方法,并核验返回 UID 是否真的高于本地水位。

不存在的指定 UID 通常被忽略,命令 OK 不保证找到了目标邮件。因此操作完成、目标仍存在、目标状态符合预期是三个不同观察。

删除前一封后,序号2还是原来的邮件吗?

清除第一封后,序号2转而指向UID108,UID104却移动到序号1。再重建邮箱,即使重新出现104,旧缓存键也不能复用;变化的是身份作用域,不只是列表重新排序。

正在呈现知识画面
删除前一封后,序号2还是原来的邮件吗?

序号随当前位置移动,UID依赖邮箱和UIDVALIDITY作用域。数字相同不代表对象身份相同。

取内容、改 flags 与按字节读 literal

UID FETCH 104 (UID FLAGS RFC822.SIZE BODY.PEEK[HEADER.FIELDS (FROM SUBJECT DATE)]) 可以先取展示列表所需的字段,避免下载整个大附件。BODY.PEEK[...] 不因读取内容而隐式设置 \Seen;普通 BODY[...] 的相应获取行为可能设置它。

服务端返回内容时可以使用 {N} literal,N 表示后续原始字节数。正文里可能含换行、括号或看起来像响应的文本,解析器必须精确读 N 字节,之后才恢复协议解析,不能简单按行寻找右括号。

payload = '主题:状态\r\n'.encode('utf-8')
assert len(payload) == 17  # 五个中文字符15 B,加CRLF2 B
wire = b'{17}\r\n' + payload + b')\r\na004 OK done\r\n'
header, rest = wire.split(b'\r\n', 1)
size = int(header[1:-1])
literal, tail = rest[:size], rest[size:]
assert literal.decode('utf-8') == '主题:状态\r\n'
assert tail.startswith(b')\r\na004 OK')
assert len(literal) != len(literal.decode('utf-8'))
print('UTF-8 literal 按字节定界通过')

标记已读可用 UID STORE 104 +FLAGS.SILENT (\Seen)。+FLAGS 增加指定标记;用 FLAGS 替换完整集合可能覆盖其他客户端的更新。.SILENT 抑制由该操作产生的相应 flags 数据回显,并不关闭所有服务器状态通知。

删除与移动:操作不是同一个“删除”按钮语义

设置 \Deleted 只是标记;EXPUNGE 才清除选中邮箱内标记删除的邮件。普通 EXPUNGE 可能连另一个客户端标记的邮件一起清除,不能假设只处理自己刚操作的 UID。支持时使用限定 UID 集合的 UID EXPUNGE 可以缩小范围。

MOVE 把邮件从一个邮箱移到另一个邮箱。旧客户端通过 COPY、标记删除与 EXPUNGE 模拟时存在中间状态与失败窗口;专用 MOVE 能避免部分组合操作的问题,但多消息操作仍要处理部分失败,不能把整组搬迁描述成应用数据库式全有或全无事务。MOVE RFC 6851

CLOSE 在可写选择状态下可能清除已标记删除的消息;UNSELECT 退出选中状态而不执行这一清除。做只读检查时优先理解 EXAMINE 语义,不要为了“退出邮箱”随意调用有删除后果的操作。

IDLE 是变化通知,不是持久事件日志

客户端发送带标签的 IDLE,服务器返回 + 后保持该命令等待,期间可以报告 EXISTS 或 flags 等变化。客户端要执行下一条普通命令时,先发送不带标签的 DONE,等待 IDLE 的标签完成响应,再继续。

时刻示例交互客户端下一步
开始等待a010 IDLE,服务端 + idling持续读未标记响应
新状态提示* 43 EXISTS记录需要同步,不把数字当 UID
结束等待DONE,服务端 a010 OK ...此后发起 FETCH/SEARCH 等
连接断开没有持久事件位置保证重连、检查 UIDVALIDITY、补齐状态

规范建议在不超过约 29 分钟的间隔内结束并重发 IDLE,以适应服务器空闲超时;中间 NAT 或移动端休眠仍可能更早断开。IDLE 能提供及时通知,却不承诺固定延迟、一直在线或断线期间消息通知可重放。IMAP IDLE

增量同步与并发冲突

只比较 UIDNEXT 能发现某些新增变化,却不能获知旧邮件 flags 的改变或已清除的 UID。支持 CONDSTORE 的服务以 MODSEQ 标识状态变化;QRESYNC 进一步支持快速重新同步与 VANISHED 删除信息。使用前需要根据协议版本和能力完成启用,不能假设所有服务都实现。CONDSTORE/QRESYNC RFC 7162

假设客户端读到邮件 MODSEQ 20,在离线期间另一个客户端改动至 21。带 UNCHANGEDSINCE 20 的条件 STORE 可拒绝覆盖新状态,并通过 MODIFIED 告知未应用的目标。客户端应重新读取并决定合并,而不是忽略冲突继续推进本地水位。

恢复流程因此是:核验邮箱与 UIDVALIDITY,补新增、变更与删除,确认本地数据已持久化,再保存同步水位。IDLE 提示和同步水位不是同一个东西;更新水位早于保存实际内容,崩溃后可能误认为已完成同步。

POP3:下载、标记与更新分成三个动作

POP3 认证后进入 TRANSACTION,LIST/STAT 查看消息与大小,RETR 下载,DELE 标记删除。只有从事务状态正常 QUIT 才进入 UPDATE,按标记执行删除;RSET 可清除删除标记,异常断线不应仅因之前 DELE 就进入删除更新。POP3 RFC 1939

POP3:RETR 下载,DELE 标记,正常 QUIT 才进入更新 TRANSACTION:已认证 RETR 1 后 DELE 1 下载与标记是两个独立命令 收到 QUIT → UPDATE 按标记执行删除并返回结果 断线或 RSET 异常断线不按标记删除;RSET 清标记 QUIT 的应答丢失时结果仍有歧义;本地保存与服务器删除不是同一个原子事务。

因此“POP3 下载即删除”并不是协议规则。客户端可以保留服务器副本,也可以在成功保存本地副本后请求删除。多台设备若都保留副本,可以各自下载,但没有 IMAP 那样的共享已读与文件夹同步语义。

POP3 消息编号只在当前 maildrop 会话中用于定位,跨会话可能变化。可选 UIDL 提供跨会话标识,客户端据此避免重复下载;它不是 RFC 5322 Message-ID。规范甚至允许完全相同副本具有同一 unique-id 的特殊情况,不能把 UIDL 当成可以忽略重复数量的全球唯一 UUID。

本地保存与服务器 UPDATE 也没有共同事务:若先记录“已下载”再持久化内容,客户端崩溃可能漏存;若内容已保存但本地记录尚未提交,重连可能重复下载。QUIT 已执行而应答丢失时,删除结果也有歧义。归档程序需要把本地内容与索引可靠提交,并在重连时重新列举服务器状态。

DELE之后断线,邮件就删除了吗?

先RETR再DELE,服务器副本还在;异常断线后重连,标记消失而邮件保留。重置后走正常QUIT,服务器才删除。若未下载就删除并QUIT,本模型也会暴露本地没有副本的结果。

正在呈现知识画面
DELE之后断线,邮件就删除了吗?

RETR复制内容,DELE只标记,正常QUIT才进入UPDATE。异常断线不会把未提交标记自动当作已删除。

TLS、认证与选择依据

IMAP 常见 TCP 993、POP3 常见 TCP 995 使用隐式 TLS;143/110 可以分别通过 STARTTLS/STLS 升级,具体能力需协商。发送口令前应完成对服务器身份的验证;是否支持 OAuth 或其他 SASL 机制由服务端能力决定,不能因为连接用了 TLS 就跳过证书校验。邮件访问 TLS RFC 8314

多设备协作、服务器搜索与文件夹同步更符合 IMAP 的状态模型;单向下载到独立归档库可以使用 POP3,但“简单”没有自动消除备份、去重与删除时序问题。IMAP 同步也不是备份:服务器上的误删可以被同步到所有客户端,独立保留版本与恢复点是另一项能力。