本章导读
POP3 是收件三协议(POP3/IMAP/第 119 章)里最简单的:下载式模型——连上、认证、列邮件、逐封取、(可选)删、告别。简单不等于没讲究:多行响应的 dot transparency(RETR/TOP 的正文行首点转义与单点终止——与 SMTP DATA 对称的反向操作);流式收取(Begin/Next 逐行产出、不分配整封邮件——每行借用内部缓冲、只稳定到下一次读取:直写文件/增量 MIME/自有存储的三条出路);UIDL 的身份语义(唯一 ID 支撑"只取新邮件"的客户端逻辑);SASL 认证族(USER/PASS 明文的默认拒绝与 SASL PLAIN 加 OAuth 机制);STLS 升级(CAPA 声明→+OK→原位升级→重新 CAPA)。xpop3 的分层:协议原语(离线)+客户端(同步状态机:AUTHORIZATION→TRANSACTION→MULTILINE→UPDATE)+可选 message 层(有界聚合)。
120.1引入
POP3 与 IMAP 的模型差异决定选型:POP3 假设"取走即本地"——服务器是暂存投递点,客户端是权威存储;IMAP(第 119 章)假设"服务器是权威"——邮件留服务器、文件夹/标记/搜索都在服务端。POP3 的简单带来低资源占用与离线友好;IMAP 的丰富带来多设备同步。今天多数场景是"收件即转存"(拉到本地数据库)——POP3 的流式收取正合适。
Begin/Next 的逐行借用值得注意:RETR 一封 20 MB 的邮件,任意时刻内存里只有当前行——Next 产出的行视图只稳定到下一次线路读取(下一行到来即覆盖)。这比"整封缓冲"激进得多——它把"收邮件"变成了"读文件"的形态(第 41 章 IO 流的同构)。
120.2概念
协议原语:响应、事实与命令
- 响应:
xrtPop3ReplyParse区分+OK/-ERR并借状态文本;StatParse(64 位数量与字节——大邮箱不溢出)、ListParse/UidlParse(多行项;UIDL 不复制不截断)。 - dot 家族复用:多行终止与去点转义直接用
mail_net的MailLineRead/MailDotLine/MailDotDecodeWrite——不维护 POP3 专用副本(与 SMTP DATA 同一族原语的两个方向)。 - 能力:
CapabilityParse未知扩展也返名称与参数;Capability只为常用标准给稳定标记。 - 命令:
CommandWrite通用兜底(容量含结尾零;拒控制字符/CR/LF 注入/512B——与 SMTP 命令层同款纪律)。
客户端:状态机与分层入口
分层入口四级:Send/Line(最低层线路——SASL continuation 与未知扩展的口)→ Receive(+OK/-ERR 解析)→ Command(构建+状态响应)→ Begin/Next(多行逐行)。标准命令全建在公开入口上——STAT/LIST×2/UIDL×2/RETR/TOP/DELE/RSET/NOOP/QUIT 是组装不是私路:自定义扩展与官方命令同权。
流式收取与可选聚合
RETR/TOP 的 Begin/Next:每行去 dot transparency、不分配整封邮件、行视图借内部接收缓冲(稳定到下次读取)。三条出路:直写文件(第 45 章)、增量 MIME 解析(第 116 章模块逐步喂)、自有消息存储。可选 pop3_message 层:同一状态机上加有界 RetrWrite/TopWrite(上限内聚合为 owned 字节)与 MIME 树入口——只要逐行路径就不带聚合闭包(裁剪自由)。
认证:明文默认拒绝与 SASL 族
pop3_auth:传统 USER/PASS + RFC 5034 SASL(PLAIN 与两种 OAuth 机制)。凭据纪律:配置只借用凭据;Base64 与明文临时缓冲调用结束前清零(第 77 章密码工程的协议层兑现)。默认拒绝明文传输发凭据——AllowPlaintext 只用于明确控制的兼容环境(与第 106 章 Basic 的 TLS 底线同款立场)。服务器拒凭据停留 AUTHORIZATION(可重试);线路错误进 FAILED。
TLS:隐式与 STLS
基础客户端 110 明文;pop3_client_tls 两形态:隐式 TLS(连接即握手);STLS(CAPA 明确声明→发 STLS→+OK 完整消费后才握手→握手后重新 CAPA——升级前能力不泄漏到安全会话,与 SMTP STARTTLS 的重 EHLO 同理)。必须提供 Verifier。
120.3示例
本章命令在仓库根目录执行,构建器按清单选择模块、公共头和平台链接库,并输出构建及依赖测试日志。终端块摘录范例自身的输出。第二个程序在进程内启动回环服务器,只使用固定演示数据;真实服务的主机、端口、CA 与运行时凭据配置见相应客户端库的 README。
第一个完整程序:协议原语
下面的程序来自 examples/pop3/protocol——离线的响应与命令闭环:
#include <stdio.h> #include <xpop3.h> /* 解析 STAT 并构建 RETR 命令。 */ int main(void) { xpop3stat Stat; char arrCommand[32]; size_t iSize; if ( !xrtPop3StatParse(XRT_STR_LITERAL("+OK 12 4096"), &Stat) || !xrtPop3CommandWrite( XRT_STR_LITERAL("RETR"), XRT_STR_LITERAL("1"), arrCommand, sizeof(arrCommand), &iSize ) ) { return 1; } printf("messages=%llu command=%.*s", (unsigned long long)Stat.Messages, (int)iSize, arrCommand); return 0; }
刚才发生了什么。 ① 程序用 xrtPop3StatParse 解析固定 STAT 响应,取得邮件数量,再用 xrtPop3CommandWrite 生成 RETR 命令并输出。数量与字节数都保留 64 位语义,命令参数接受协议层校验。② 协议解析与命令构建不建立连接,适合作为自定义客户端或测试服务器的原语。响应、UIDL 及多行数据的其他边界由同一套件的协议测试覆盖;这个短程序展示其中两条入口。
第二个完整程序:本地收取会话
第二个程序来自 extlibs/xpop3/examples/offline/main.c.
#include <xpop3.h> #include <stdio.h> #include <string.h> #include <math.h> static inline double exampleTimerLimit(int64 Timeout) { return Timeout == XRT_WAIT_FOREVER ? INFINITY : xrtTimer() + (double)Timeout / 1000.0; } static inline bool exampleTimerExpired(double Limit) { return xrtTimer() >= Limit; } static inline int64 exampleTimerRemaining(double Limit) { double Ms; if (Limit == INFINITY) return XRT_WAIT_FOREVER; Ms = ceil((Limit - xrtTimer()) * 1000.0); return Ms <= 0 ? 0 : Ms >= 0x1p63 ? INT64_MAX : (int64)Ms; } /* 只供本进程的回环 POP3 演示服务器使用。 */ static xnetaddr ExampleAddress; typedef struct example_server { xnetlistener* Listener; double Deadline; bool Success; } example_server; static xnetaddrlist* exampleResolve(cstr sHost, xnetfamily Family, ptr pData) { xnetaddr Address = ExampleAddress; (void)Family; (void)pData; if(strcmp(sHost, "pop3.example.invalid") != 0) return NULL; Address.Port = 0u; return xrtNetAddrListCreate(&Address, 1u); } static bool exampleSend(xnetstream* pStream, cstr sText, double Deadline) { size_t iSize = strlen(sText); for(;;) { xnetresult Result = xrtNetStreamSend(pStream, sText, iSize); if(Result == XNET_RESULT_OK) return true; if((Result != XNET_RESULT_AGAIN) || !xrtNetStreamWait( pStream, XNET_STREAM_WAIT_WRITE,exampleTimerRemaining(Deadline), NULL)) return false; } } static bool exampleExpect(xnetstream* pStream, cstr sExpected, double Deadline) { size_t iSize = strlen(sExpected); size_t iUsed = 0u; while(iUsed < iSize) { xnetbytes* pBytes = xrtNetStreamRecv( pStream, iSize - iUsed,exampleTimerRemaining(Deadline), NULL); xbytesview Bytes; if(pBytes == NULL) return false; Bytes = xrtNetBytesView(pBytes); if((Bytes.Size == 0u) || (memcmp(Bytes.Data, sExpected + iUsed, Bytes.Size) != 0)) { xrtNetBytesDestroy(pBytes); return false; } iUsed += Bytes.Size; xrtNetBytesDestroy(pBytes); } return true; } static int32 exampleServer(ptr pData) { example_server* pServer = (example_server*)pData; xnetstream* pStream = xrtNetListenerAcceptWait( pServer->Listener,exampleTimerRemaining(pServer->Deadline), NULL); bool bOk; if(pStream == NULL) return 1; bOk = exampleSend(pStream, "+OK ready\r\n", pServer->Deadline) && exampleExpect(pStream, "USER demo\r\n", pServer->Deadline) && exampleSend(pStream, "+OK user\r\n", pServer->Deadline) && exampleExpect(pStream, "PASS demo\r\n", pServer->Deadline) && exampleSend(pStream, "+OK mailbox\r\n", pServer->Deadline) && exampleExpect(pStream, "RETR 1\r\n", pServer->Deadline) && exampleSend(pStream, "+OK message\r\nSubject: offline example\r\n\r\n" "Hello from loopback.\r\n.\r\n", pServer->Deadline) && exampleExpect(pStream, "QUIT\r\n", pServer->Deadline) && exampleSend(pStream, "+OK bye\r\n", pServer->Deadline) && xrtNetStreamClose(pStream) && xrtNetStreamWait(pStream, XNET_STREAM_WAIT_CLOSE,exampleTimerRemaining(pServer->Deadline), NULL); pServer->Success = bOk; xrtNetStreamDestroy(pStream); return bOk ? 0 : 2; } int main(void) { xnetengineconfig EngineConfig; xnetresolverconfig ResolverConfig; xnetlistenconfig ListenConfig; xpop3clientconfig ClientConfig; example_server Server = { 0 }; xnetengine* pEngine = NULL; xnetresolver* pResolver = NULL; xnetlistener* pListener = NULL; xpop3client* pClient = NULL; xthread* pThread = NULL; double Deadline = exampleTimerLimit(INT64_C(10000)); xstrview Line; xmailnext Next; size_t iLines = 0u; bool bContentOk = true; bool bOk = false; xrtNetEngineConfigInit(&EngineConfig); EngineConfig.Backend = XNET_PORT_SELECT; EngineConfig.Workers = 1u; pEngine = xrtNetEngineCreate(&EngineConfig); if((pEngine == NULL) || !xrtNetEngineStart(pEngine)) goto Done; xrtNetListenConfigInit(&ListenConfig); if(!xrtNetAddrLoopback(&ListenConfig.Address, XNET_FAMILY_IPV4, 0u)) goto Done; pListener = xrtNetListen(pEngine, &ListenConfig, NULL, NULL, NULL); if((pListener == NULL) || !xrtNetListenerLocal(pListener, &ExampleAddress)) goto Done; xrtNetResolverConfigInit(&ResolverConfig); ResolverConfig.Workers = 1u; ResolverConfig.Lookup = exampleResolve; ResolverConfig.CacheEntries = 0u; pResolver = xrtNetResolverCreate(&ResolverConfig); if(pResolver == NULL) goto Done; Server.Listener = pListener; Server.Deadline = Deadline; pThread = xrtThreadCreate(exampleServer, &Server, 0u); if(pThread == NULL) goto Done; xrtPop3ClientConfigInit(&ClientConfig); ClientConfig.Net.Engine = pEngine; ClientConfig.Net.Resolver = pResolver; ClientConfig.Net.Host = "pop3.example.invalid"; ClientConfig.Net.Port = ExampleAddress.Port; ClientConfig.ReadCapabilities = false; pClient = xrtPop3ClientOpen(&ClientConfig,exampleTimerRemaining(Deadline), NULL); /* 明文凭据仅用于进程内回环演示;真实服务请用 client 范例的 TLS。 */ if((pClient == NULL) || !xrtPop3ClientLogin(pClient, XRT_STR_LITERAL("demo"), XRT_STR_LITERAL("demo"), true,exampleTimerRemaining(Deadline), NULL) || !xrtPop3ClientRetr(pClient, 1u,exampleTimerRemaining(Deadline), NULL)) goto Done; for(;;) { Next = xrtPop3ClientNext(pClient, &Line,exampleTimerRemaining(Deadline), NULL); if(Next != XMAIL_NEXT_ITEM) break; if(iLines == 0u) bContentOk = bContentOk && (Line.Size == strlen("Subject: offline example")) && (memcmp(Line.Data, "Subject: offline example", Line.Size) == 0); else if(iLines == 1u) bContentOk = bContentOk && (Line.Size == 0u); else if(iLines == 2u) bContentOk = bContentOk && (Line.Size == strlen("Hello from loopback.")) && (memcmp(Line.Data, "Hello from loopback.", Line.Size) == 0); else bContentOk = false; iLines++; } if((Next != XMAIL_NEXT_END) || !bContentOk || (iLines != 3u) || !xrtPop3ClientQuit(pClient,exampleTimerRemaining(Deadline), NULL)) goto Done; bOk = true; Done: if((pClient != NULL) && !bOk) (void)xrtPop3ClientAbort(pClient); xrtPop3ClientDestroy(pClient); if(pThread != NULL) { bool bThreadDone = xrtThreadWait(pThread) == XWAIT_OK; bOk = bOk && bThreadDone && Server.Success && (xrtThreadExitCode(pThread) == 0); xrtThreadDestroy(pThread); } if(pListener != NULL) { (void)xrtNetListenerClose(pListener); while(xrtNetListenerState(pListener) != XNET_LISTENER_CLOSED) xrtThreadYield(); xrtNetListenerDestroy(pListener); } if(pResolver != NULL) (void)xrtNetResolverDestroy(pResolver); if(pEngine != NULL) (void)xrtNetEngineDestroy(pEngine); if(!bOk) { fputs("offline POP3 retrieval failed\n", stderr); return 1; } puts("offline POP3 retrieval: 3 message lines"); return 0; }
刚才发生了什么。 ① 程序创建本地监听器与服务器线程,通过自定义解析器连接回环地址。此夹具不实现 CAPA,所以配置关闭自动能力读取;对真实服务应按服务器行为选择该配置。② 客户端完成 USER/PASS,执行 RETR 并逐行消费 xrtPop3ClientNext 的结果。程序逐字节核对主题行、空行和正文,最终必须恰好得到三行及完整多行终止响应。③ 客户端完成 QUIT 后,程序等待服务器线程并销毁连接、监听器、解析器和引擎,只有内容校验及收尾全部成功才打印成功行。本地演示显式允许固定明文凭据,这个设置限定在回环夹具;真实服务器范例使用 TLS 和运行时凭据。此程序不删除邮件,下载和服务器删除是两个独立操作。
120.4契约
- 协议/客户端分层:原语不依赖网络;客户端是状态机组装;标准命令建在公开入口上。
- 64 位事实:STAT 数量与字节 64 位;UIDL 不复制不截断。
- dot 家族共享:多行终止/去转义复用 mail_net 原语——无 POP3 专用副本。
- 注入纪律:CommandWrite 拒控制/CR/LF/512B——与 SMTP 同款。
- 流式收取:RETR/TOP 逐行借用(稳定到下次读取);不分配整封邮件;三条出路(文件/增量 MIME/自有存储)。
- 可选聚合:message 层有界 RetrWrite/TopWrite——逐线路径不携带聚合闭包。
- 凭据清零:借用配置;临时缓冲调用结束前清零;默认拒绝明文发凭据。
- 认证语义:拒凭据停留 AUTHORIZATION;线路错误 FAILED 不可复用。
- TLS:隐式/STLS;+OK 完整消费后才握手;升级后重新 CAPA;验证器必须。
- UPDATE 语义:DELE 标记在 Quit 的 UPDATE 生效——Abort 放弃删除。
- 所有权:共享对象借不销毁;Worker 回调禁调;单 Client 无并发。
常见错误
症状:收完后处理缓存行——内容已是下一行的字节。
原因:行的稳定性边界是"下一次线路读取"——POP3 客户端的最激进借用。要跨行保存必须复制。
while ( Next(&Line) ) { rows[n++] = Line; /* 存视图:下一次 Next 全部失效 */ } process(rows); /* 全是最后一行 */
while ( xrtPop3ClientNext(pClient, &Line) == OK ) { process_now(Line); /* 即时消费 */ /* 或复制:append_own_store(Line); */ }
症状:AllowPlaintext 一开了之——内网抓包全是凭据。
原因:默认拒绝是保护不是障碍;兼容老服务器的正确路径是 STLS(CAPA 声明就升级),不是放弃加密。
Config.AllowPlaintext = true; /* 图省事:明文凭据 */ Auth(pClient, User, Pass);
if ( capa_has_stls() ) { Stls(pClient, ...); /* 先升级 */ } Auth(pClient, User, Pass); /* 加密隧道内 */
症状:DELE 后服务器崩了/客户端 Abort——重启后邮件还在,用户以为"删了又回来"。
原因:POP3 的删除是两阶段:DELE 只打标记、Quit 的 UPDATE 才提交——协议对"误删"的防御。要"必然删除"就完整 Quit;要"也许删除"(试探性处理)就 Abort 回滚。
Dele(pClient, 1); /* 崩溃/Abort:邮件未删——以为是 bug */
Dele(pClient, 1); /* 打标记 */ ... xrtPop3ClientQuit(...); /* UPDATE 提交——真删 */ /* 处理失败时 Abort:标记回滚——安全网 */
编程练习
构造 +OK/-ERR/STAT/List/Uidl 多行样本走原语解析。验收标准:64 位数量正确;UIDL 全长保留;多行终止识别正确。
UIDL 列表对照本地已见集合(第 18 章 Map)→ 只 RETR 新 UID → 处理后记录 → 可选 DELE+Quit。验收标准:重复运行零重复收取;UIDL 稳定性(同邮件同 ID);中断后重跑不丢不重。
POP3 流式收取→逐行喂第 116 章消息视图/MIME 增量解析→提取头与正文→写入本地 mbox/JSONL 存储。20 MB 大邮件。验收标准:全程内存恒定(MB 级);转存后经第 116 章解析还原完整;断点续收(UIDL 对账)。
小结
| 知识点 | 速查 |
|---|---|
| 模型 | 下载式:取走即本地;UIDL 是"只取新"的身份 |
| 状态机 | AUTHORIZATION→TRANSACTION→MULTILINE→UPDATE(Quit 提交) |
| 响应 | +OK/-ERR 两态;STAT 64 位;UIDL 不截断 |
| dot 家族 | 与 SMTP DATA 对称;复用 mail_net 原语无专用副本 |
| 四级入口 | Send/Line→Receive→Command→Begin/Next——标准命令同权 |
| 流式收取 | 逐行借用稳定到下次读取;零整封分配;文件/增量/存储三出路 |
| 可选聚合 | message 层有界 RetrWrite——逐线路径零闭包 |
| 凭据 | 默认拒明文;临时缓冲调用内清零;SASL 三机制 |
| TLS | 隐式/STLS;+OK 消费后才握手;重 CAPA;验证器必须 |
| 删除语义 | DELE 打标记、Quit UPDATE 提交——Abort 回滚 |
📖 本章涉及的 API 参考手册:xpop3-pop3(9 个条目)xpop3-pop3_client(29 个条目)xmail-mail(3 个条目)
点击查看完整签名、参数约束、返回值与错误说明