本章导读

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 命令层同款纪律)。

客户端:状态机与分层入口

AUTHORIZATION认证成功(USER/PASS 或 SASL) →TRANSACTION
TRANSACTION多行命令(RETR/TOP/LIST/UIDL 全量) →MULTILINE
MULTILINE单点终止行(Next 自动恢复) →TRANSACTION
TRANSACTIONQuit(提交 DELE 标记) →UPDATE
任意线路错误(不可复用) →FAILED

分层入口四级: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——离线的响应与命令闭环:

extlibs/xpop3/examples/protocol/main.c源码 ↗
#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;
}
$ python tools/build.py --manifest extlibs/xpop3/config/modules.json --suite pop3 --no-single --jobs 4messages=12 command=RETR 1

刚才发生了什么。 ① 程序用 xrtPop3StatParse 解析固定 STAT 响应,取得邮件数量,再用 xrtPop3CommandWrite 生成 RETR 命令并输出。数量与字节数都保留 64 位语义,命令参数接受协议层校验。② 协议解析与命令构建不建立连接,适合作为自定义客户端或测试服务器的原语。响应、UIDL 及多行数据的其他边界由同一套件的协议测试覆盖;这个短程序展示其中两条入口。

第二个完整程序:本地收取会话

第二个程序来自 extlibs/xpop3/examples/offline/main.c.

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;
}
$ python tools/build.py --manifest extlibs/xpop3/config/modules.json --suite pop3_offline_example --no-single --jobs 4offline POP3 retrieval: 3 message lines

刚才发生了什么。 ① 程序创建本地监听器与服务器线程,通过自定义解析器连接回环地址。此夹具不实现 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 无并发。

常见错误

坑 1:把行视图存起来继续收

症状:收完后处理缓存行——内容已是下一行的字节。

原因:行的稳定性边界是"下一次线路读取"——POP3 客户端的最激进借用。要跨行保存必须复制。

错误写法
while ( Next(&Line) ) {
	rows[n++] = Line;   /* 存视图:下一次 Next 全部失效 */
}
process(rows);   /* 全是最后一行 */
正确写法
while ( xrtPop3ClientNext(pClient, &Line) == OK ) {
	process_now(Line);          /* 即时消费 */
	/* 或复制:append_own_store(Line); */
}
坑 2:明文连接上发 USER/PASS

症状:AllowPlaintext 一开了之——内网抓包全是凭据。

原因:默认拒绝是保护不是障碍;兼容老服务器的正确路径是 STLS(CAPA 声明就升级),不是放弃加密。

错误写法
Config.AllowPlaintext = true;   /* 图省事:明文凭据 */
Auth(pClient, User, Pass);
正确写法
if ( capa_has_stls() ) {
	Stls(pClient, ...);   /* 先升级 */
}
Auth(pClient, User, Pass);   /* 加密隧道内 */
坑 3:以为 DELE 立即删除

症状: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 个条目)

点击查看完整签名、参数约束、返回值与错误说明