IMAP 让客户端操作服务器上的邮箱、邮件状态和内容,适合多端维护共同视图;POP3 主要提供 maildrop 中消息的列举、下载与删除。两者都可以把邮件缓存在本地,也都不是“下载后必须立即删除”。真正的区别在于服务器向客户端提供了多少可同步的状态与操作。
| 能力 | IMAP | POP3 |
| 组织消息 | 多邮箱、服务器端状态与搜索 | 以当前 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。
假设客户端看到序号 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) 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作用域。数字相同不代表对象身份相同。
{"id":"imap-position-identity","title":"删除前一封后,序号2还是原来的邮件吗?","summary":"序号随当前位置移动,UID依赖邮箱和UIDVALIDITY作用域。数字相同不代表对象身份相同。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"expunge\" data-i18n=\"expunge\"></button><button id=\"epoch\" data-i18n=\"epoch\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"mailbox\"></h3><div id=\"mailbox\"></div></section><div class=\"metrics\"><div><span data-i18n=\"seq\"></span><strong id=\"seq\"></strong></div><div><span data-i18n=\"uid\"></span><strong id=\"uid\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#mailbox .tr>*{min-height:4.8em}.metrics strong{font-size:16px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let epoch=700,uids=[101,104,108];function draw(){table('mailbox',[['sequence','UID','epoch'],...[0,1,2].map(i=>[uids[i]?i+1:'—',uids[i]||'—',uids[i]?epoch:'—'])]);set('seq',uids[1]?'UID'+uids[1]+' @'+epoch:'∅');set('uid',epoch!==700?'INVALID SCOPE':uids.includes(104)?'UID104 @700':'∅');$('#expunge').disabled=!uids.length;$('#epoch').disabled=epoch!==700;$('#mailbox').dataset.snapshot=JSON.stringify({epoch,uids});}$('#expunge').onclick=()=>{uids.shift();draw()};$('#epoch').onclick=()=>{epoch=701;uids=[101,104,108];draw()};$('#reset').onclick=()=>{epoch=700;uids=[101,104,108];draw()};draw();","audio":false,"strings":{"epoch":"重建邮箱并改变UIDVALIDITY","expunge":"清除当前第一封","heading":"删除前一封后,序号2还是原来的邮件吗?","intro":"保留客户端记住的序号和UID,再清除前一封或切换UIDVALIDITY。","mailbox":"当前邮箱对象","reset":"重置","scope":"固定INBOX与同一账户,UID101/104/108,初始UIDVALIDITY700,记住序号2/UID104。换代后相同数字表示新对象,禁止自动应用旧离线操作。","scopeLabel":"模型范围与约定","seq":"按记住的序号2定位","uid":"按旧作用域UID104定位"}}
取内容、改 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 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 下载即删除”并不是协议规则。客户端可以保留服务器副本,也可以在成功保存本地副本后请求删除。多台设备若都保留副本,可以各自下载,但没有 IMAP 那样的共享已读与文件夹同步语义。
POP3 消息编号只在当前 maildrop 会话中用于定位,跨会话可能变化。可选 UIDL 提供跨会话标识,客户端据此避免重复下载;它不是 RFC 5322 Message-ID。规范甚至允许完全相同副本具有同一 unique-id 的特殊情况,不能把 UIDL 当成可以忽略重复数量的全球唯一 UUID。
本地保存与服务器 UPDATE 也没有共同事务:若先记录“已下载”再持久化内容,客户端崩溃可能漏存;若内容已保存但本地记录尚未提交,重连可能重复下载。QUIT 已执行而应答丢失时,删除结果也有歧义。归档程序需要把本地内容与索引可靠提交,并在重连时重新列举服务器状态。
DELE之后断线,邮件就删除了吗?
先RETR再DELE,服务器副本还在;异常断线后重连,标记消失而邮件保留。重置后走正常QUIT,服务器才删除。若未下载就删除并QUIT,本模型也会暴露本地没有副本的结果。
正在呈现知识画面
DELE之后断线,邮件就删除了吗?RETR复制内容,DELE只标记,正常QUIT才进入UPDATE。异常断线不会把未提交标记自动当作已删除。
{"id":"pop3-delete-commit","title":"DELE之后断线,邮件就删除了吗?","summary":"RETR复制内容,DELE只标记,正常QUIT才进入UPDATE。异常断线不会把未提交标记自动当作已删除。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"retr\" data-i18n=\"retr\"></button><button id=\"dele\" data-i18n=\"dele\"></button><button id=\"rset\" data-i18n=\"rset\"></button><button id=\"quit\" data-i18n=\"quit\"></button><button id=\"drop\" data-i18n=\"drop\"></button><button id=\"reconnect\" data-i18n=\"reconnect\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"state\"></h3><div id=\"state\"></div></section><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#state .tr{grid-template-columns:1fr 1fr}.tr>*{min-height:4em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let server=true,local=false,mark=false,open=true;function draw(){table('state',[['server copy',server?'✓':'∅'],['local saved',local?'✓':'∅'],['DELE mark',mark?'✓':'∅'],['session',open?'TRANSACTION':'CLOSED']]);$('#retr').disabled=$('#dele').disabled=!open||!server||mark;$('#rset').disabled=!open||!mark;$('#quit').disabled=$('#drop').disabled=!open;$('#reconnect').disabled=open;$('#state').dataset.snapshot=JSON.stringify({server,local,mark,open});}$('#retr').onclick=()=>{local=true;draw()};$('#dele').onclick=()=>{mark=true;draw()};$('#rset').onclick=()=>{mark=false;draw()};$('#quit').onclick=()=>{if(mark)server=false;mark=false;open=false;draw()};$('#drop').onclick=()=>{mark=false;open=false;draw()};$('#reconnect').onclick=()=>{open=true;draw()};$('#reset').onclick=()=>{server=true;local=false;mark=false;open=true;draw()};draw();","audio":false,"strings":{"dele":"DELE 标记","drop":"异常断线","heading":"DELE之后断线,邮件就删除了吗?","intro":"把下载、删除标记、正常QUIT和异常断线分别操作,观察服务器与本地副本。","quit":"正常 QUIT","reconnect":"重新连接","reset":"重置","retr":"RETR 下载保存","rset":"RSET 撤销标记","scope":"一封邮件、单客户端;QUIT更新假设成功且响应到达,本地RETR立即持久保存。真实UPDATE失败和响应丢失另见正文,不模拟整个POP3会话。","scopeLabel":"模型范围与约定","state":"副本与会话状态"}}
TLS、认证与选择依据
IMAP 常见 TCP 993、POP3 常见 TCP 995 使用隐式 TLS;143/110 可以分别通过 STARTTLS/STLS 升级,具体能力需协商。发送口令前应完成对服务器身份的验证;是否支持 OAuth 或其他 SASL 机制由服务端能力决定,不能因为连接用了 TLS 就跳过证书校验。邮件访问 TLS RFC 8314
多设备协作、服务器搜索与文件夹同步更符合 IMAP 的状态模型;单向下载到独立归档库可以使用 POP3,但“简单”没有自动消除备份、去重与删除时序问题。IMAP 同步也不是备份:服务器上的误删可以被同步到所有客户端,独立保留版本与恢复点是另一项能力。