本章导读

IMAP 是 POP3(第 118 章)的"服务器权威"对偶:邮件留在服务端、文件夹/标记/搜索/FETCH 部分获取都在服务器上做——多设备同步的基石。协议复杂度也高一个量级:tagged 响应模型(每命令带唯一 tag,服务器响应按 tag 关联——支持流水线);literal 机制(含任意字节的参数与响应走 {N} 长度声明——同步 literal 要等续行确认、LITERAL+ 免同步);untagged 事件流(EXISTS/RECENT/FETCH 更新随时插队)。ximap 的分层:协议原语(imap:响应/literal/capability/命令——不建邮箱对象不构造搜索条件,未知扩展按原始文本可达)→客户端(imap_client:两级命令模型——低层显式 tag 流水线 + 顺序便利层)→命令便利层(imap_command:SELECT/LIST/SEARCH/FETCH/IDLE 的安全构造)→ 专项层(auth/body/message/append/compress)。

121.1引入

IMAP 的三个机制值得开场。其一:literal 流式。FETCH 一封 20 MB 邮件,响应里是 {20971520} 后跟原始字节——客户端必须按明确长度分段读:ReadLiteral 用调用方缓冲逐段取、未读完前不能读后续响应——"消息大小不会变成固定内存上限"是协议层的保证(与 POP3 逐行流式同族、但按长度而非按行)。其二:IDLE 推送。IDLE 命令让服务器在新邮件到达时主动推 untagged 事件——"长连接等新邮件"的服务器版(SSE 的邮件版)。其三:流水线。低层 Send 显式 tag——发多条再统一 Receive 按到达顺序收事件、按 tag 关联 completion——IMAP 的并发原语(连接复用的吞吐形态)。

注入防御同款在场:CommandWrite 拒 atom 错误/控制字符/线路注入/超限;imap_command 的邮箱名"按 IMAP string 规则校验和引用,不能通过 CRLF 注入额外命令"。

121.2概念

响应模型与 literal

1响应三类tagged(命令完成——按 tag 关联)/ untagged(* 开头——事件流插队)/ continuation(+ 开头——literal 续行)
2状态五态OK/NO/BAD/PREAUTH/BYE(稳定识别)
3literal 三形态同步 {N}(等客户端续行确认)/ LITERAL+ {N+}(免同步直发)/ binary ~{N}

literal 预算双向:发送侧(APPEND 大邮件——第 120 章组合)与接收侧(FETCH 大 literal)。接收侧的契约:HasLiteral 事件先查 Event.Literal.Size——超应用预算立即 Abort(不必消费正文——"先看菜单再决定吃不吃"的内存防御);低层流式无默认上限(上限归应用);imap_message 便利层自带 64 MiB 预算(两者不共享——各层各策)。

两级命令模型

  • 低层(Send/SendParts/Write/Continue/Receive/ReadLiteral):显式 tag 流水线——多发命令、按序收事件、tag 关联;SendParts 参数片间插空格直写线路(不构造整条临时命令)。视图借线路缓冲——下一次 Receive/Next/ReadLiteral 后失效(第 118 章行视图的同族纪律,这里多了 literal 读取也是失效点)。
  • 顺序层(Begin/BeginParts/Next):自动唯一 tag;Next 把 untagged(XMAIL_NEXT_ITEM)与目标 completion(XMAIL_NEXT_END)分态产出——循环写法统一;活动顺序命令未完不能开下一条。

命令便利层:SELECT 摘要与流式结果

imap_command 覆盖常用命令但不建重量级对象:Select/Examine 产零分配 ximapmailboxinfo(Present 位区分"未返回"与"值为零"——第 102 章存在位的邮箱版;ReadOnly 取最终 completion 的访问模式;MailboxInfoUpdate 合并后续 EXISTS/RECENT 事件到同一摘要)。管理命令(CREATE/DELETE/RENAME/SUBSCRIBE/CHECK/UNSELECT/CLOSE)完整消费同步完成;流式命令(LIST/STATUS/SEARCH/FETCH/STORE/COPY/MOVE/EXPUNGE)只安全构造并开始——结果经 Next+ReadLiteral 流式消费(便利层不截流)。IDLE:进入后 Done 结束——推送事件走同一 Receive 循环。

TLS、压缩与失败语义

TLS:隐式/STARTTLS(tagged OK 完整消费后握手;升级后重新 CAPABILITY——不沿用明文快照;验证器必须)——与 SMTP/POP3 同族三连。COMPRESS(imap_compress):认证后协商 DEFLATE 压缩线路——高延迟链路的带宽优化。失败语义三分:线路 FAILED(网络/取消/超时/协议错序/解析失败——"无法保证下一字节位于命令边界",连接不可复用);命令 NO/BAD(服务器对完整命令的否定——客户端保留可恢复状态、结构化错误返回——下一命令继续);Abort(无等待中止、可重复、FAILED 与最近 completion 保留到 Destroy 供诊断)。

121.3示例

本章命令在仓库根目录执行,构建器按清单选择模块、公共头和平台链接库,并输出构建及依赖测试日志。终端块摘录范例自身的输出。第二个程序在进程内启动回环服务器,只使用固定演示数据;真实服务的主机、端口、CA 与运行时凭据配置见相应客户端库的 README。

第一个完整程序:协议原语

下面的程序来自 examples/imap/protocol——离线的响应与命令闭环:

extlibs/ximap/examples/protocol/main.c源码 ↗
#include <stdio.h>
#include <ximap.h>



/* 解析服务器响应并构建一条 IMAP 命令。 */
int main(void)
{
	ximapresponseview Response;
	char arrCommand[64];
	size_t iSize;

	if ( !xrtImapResponseParse(
		XRT_STR_LITERAL("A001 OK authenticated"),
		&Response
	) || !xrtImapCommandWrite(
		XRT_STR_LITERAL("A002"),
		XRT_STR_LITERAL("SELECT"),
		XRT_STR_LITERAL("\"INBOX\""),
		0,
		arrCommand,
		sizeof(arrCommand),
		&iSize
	) ) {
		return 1;
	}
	printf("status=%d command=%.*s",
		(int)Response.Status, (int)iSize, arrCommand);
	return 0;
}
$ python tools/build.py --manifest extlibs/ximap/config/modules.json --suite imap --no-single --jobs 4status=1 command=A002 SELECT "INBOX"

刚才发生了什么。 ① 程序用 xrtImapResponseParse 解析固定 tagged OK 响应,再用 xrtImapCommandWrite 构造带明确 tag 和邮箱参数的 SELECT 命令,输出状态与命令字节。② 命令中的邮箱参数已经带双引号,构造器校验线路语法与注入边界;调用方按参数类型选择 quoted string 或 literal。响应分类、literal 与转义的其他边界由同一套件的协议测试覆盖,实际读取正文仍由客户端状态机处理。

第二个完整程序:只读邮箱查询

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

extlibs/ximap/examples/offline/main.c源码 ↗
#include <ximap.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;
}

/* 本地演示域名只解析到本进程的回环 IMAP 服务。 */
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, "imap.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 imap.example.invalid ready\r\n",
		pServer->Deadline) &&
		exampleExpect(pStream, "A00000001 CAPABILITY\r\n",
			pServer->Deadline) &&
		exampleSend(pStream,
			"* CAPABILITY IMAP4rev1\r\n"
			"A00000001 OK capability complete\r\n",
			pServer->Deadline) &&
		exampleExpect(pStream,
			"A00000002 LOGIN \"demo\" \"demo\"\r\n",
			pServer->Deadline) &&
		exampleSend(pStream, "A00000002 OK login complete\r\n",
			pServer->Deadline) &&
		exampleExpect(pStream, "A00000003 EXAMINE \"INBOX\"\r\n",
			pServer->Deadline) &&
		exampleSend(pStream,
			"* 2 EXISTS\r\n"
			"* 0 RECENT\r\n"
			"* OK [UIDVALIDITY 42] valid\r\n"
			"A00000003 OK [READ-ONLY] examined\r\n",
			pServer->Deadline) &&
		exampleExpect(pStream, "A00000004 LOGOUT\r\n",
			pServer->Deadline) &&
		exampleSend(pStream,
			"* BYE signing off\r\n"
			"A00000004 OK logout complete\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;
	ximapclientconfig ClientConfig;
	ximapauthconfig Auth;
	ximapmailboxinfo Mailbox;
	example_server Server = { 0 };
	xnetengine* pEngine = NULL;
	xnetresolver* pResolver = NULL;
	xnetlistener* pListener = NULL;
	ximapclient* pClient = NULL;
	xthread* pThread = NULL;
	double Deadline = exampleTimerLimit(INT64_C(10000));
	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;
	xrtImapClientConfigInit(&ClientConfig);
	ClientConfig.Net.Engine = pEngine;
	ClientConfig.Net.Resolver = pResolver;
	ClientConfig.Net.Host = "imap.example.invalid";
	ClientConfig.Net.Port = ExampleAddress.Port;
	pClient = xrtImapClientOpen(&ClientConfig,exampleTimerRemaining(Deadline), NULL);
	if ( pClient == NULL ) goto Done;
	xrtImapAuthConfigInit(&Auth);
	Auth.Method = XIMAP_AUTH_LOGIN;
	Auth.Username = XRT_STR_LITERAL("demo");
	Auth.Secret = XRT_STR_LITERAL("demo");
	/* 明文凭据仅供进程内回环演示;真实服务使用 TLS 范例。 */
	Auth.AllowPlaintext = true;
	xrtImapMailboxInfoInit(&Mailbox);
	if ( !xrtImapClientAuth(pClient, &Auth,exampleTimerRemaining(Deadline), NULL) ||
		!xrtImapClientExamine(pClient, XRT_STR_LITERAL("INBOX"),
			&Mailbox,exampleTimerRemaining(Deadline), NULL) ||
		(Mailbox.Exists != 2u) || (Mailbox.Recent != 0u) ||
		(Mailbox.UidValidity != 42u) || !Mailbox.ReadOnly ||
		(xrtImapClientState(pClient) != XIMAP_CLIENT_SELECTED) ||
		!xrtImapClientLogout(pClient,exampleTimerRemaining(Deadline), NULL) ) goto Done;
	bOk = true;

Done:
	if ( (pClient != NULL) && !bOk ) (void)xrtImapClientAbort(pClient);
	xrtImapClientDestroy(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 IMAP EXAMINE failed\n", stderr);
		return 1;
	}
	puts("offline IMAP EXAMINE: INBOX has 2 messages (read-only)");
	return 0;
}
$ python tools/build.py --manifest extlibs/ximap/config/modules.json --suite imap_offline_example --no-single --jobs 4offline IMAP EXAMINE: INBOX has 2 messages (read-only)

刚才发生了什么。 ① 程序在进程内创建回环 IMAP 服务器,客户端实际读取欢迎响应并建立顺序命令会话。服务器与客户端使用固定演示账户完成 LOGIN;明文认证的允许配置只用于这个本地夹具。② 客户端调用 xrtImapClientExamine,读取 INBOX 的只读状态与两封邮件计数,验证选中状态及返回的邮箱事实,再等待命令的 tagged completion。EXAMINE 不改变服务器消息标记,适合展示只读检查的最短路径。③ LOGOUT 后程序等待服务器线程正常结束,关闭并销毁监听器、解析器和网络引擎;成功输出代表命令结果与服务器脚本均已核对。压缩扩展需要另行建立认证后的协商与流解码流程,压缩测试和真实客户端配置见库的 README。

121.4契约

  • 原语无模型:不建邮箱对象/不构造搜索/FETCH 模型——未知扩展按原始文本可达。
  • 响应三类五态:tagged/untagged/continuation;OK/NO/BAD/PREAUTH/BYE 稳定识别;全视图借用。
  • literal 契约:解析只给长度与标记;数据分段读取;未读完不能读后续响应——消息大小≠内存上限。
  • literal 预算:低层无默认上限(应用自查 Size、超限立即 Abort);message 层 64 MiB 独立预算。
  • 两级模型:低层显式 tag 流水线(SendParts 直写线路)/顺序层自动 tag+分态 Next;顺序命令不并发。
  • 视图失效点:Receive/Next/ReadLiteral 任一发生即失效线路缓冲视图。
  • 命令便利层:SELECT 零分配摘要(Present 位/事件合并);管理命令同步完成;流式命令不截流。
  • 注入防御:命令/邮箱名注入拒绝;控制数据走 literal。
  • TLS:隐式/STARTTLS;升级后重 CAPABILITY;验证器必须。
  • 压缩:认证后 COMPRESS;配置先 Valid。
  • 失败三分:线路 FAILED 不可复用/命令 NO-BAD 可恢复/Abort 幂等留诊断。

常见错误

坑 1:literal 事件先读后查大小

症状:HasLiteral 直接开始 ReadLiteral——下一封是 500 MB 邮件(历史归档),读到一半内存或预算崩。

原因:契约明示"先检查 Event.Literal.Size"——大小在事件里、读取在决定后。低层无默认上限=上限是你的责任。

错误写法
if ( Event.HasLiteral ) {
	while ( ReadLiteral(...) ) { consume(); }   /* 未查大小:500MB 直灌 */
}
正确写法
if ( Event.HasLiteral ) {
	if ( Event.Literal.Size > MyBudget ) {
		xrtImapClientAbort(pClient);   /* 超预算:立即中止不吃正文 */
		return;
	}
	while ( ReadLiteral(...) ) { consume(); }
}
坑 2:流水线响应按发送顺序等 completion

症状:死等命令 A 的 tagged 响应——服务器实际先回了 B 的(或先插了 untagged 事件)。

原因:IMAP 响应按服务器到达顺序回,不承诺与发送同序;untagged 事件随时插队——按 tag 关联、按到达消费,别按发送序等待。

错误写法
send(A); send(B);
wait_tagged(A);   /* 服务器可能先回 B:挂等 */
wait_tagged(B);
正确写法
send(A); send(B);
while ( Receive(&Event) ) {
	if ( Event.kind == TAGGED ) { dispatch_by_tag(Event.Tag); }
	else { handle_untagged(Event); }   /* 插队事件即时处理 */
}
坑 3:视图跨过下一次 Receive

症状:保存的响应视图/fragment 读到错乱——线路缓冲已被下一次读取覆盖。

原因:失效点是三个(Receive/Next/ReadLiteral)——比 POP3 的"下次读取"更细:literal 读取也会动缓冲。要跨调用保存就复制。

错误写法
Subject = Event.ResponseView;   /* 存视图 */
Receive(&Event2);               /* 缓冲已换 */
use(Subject);                   /* 错乱 */
正确写法
Subject = copy_view(Event.ResponseView);   /* 跨调用先复制 */
Receive(&Event2);
use(Subject);

编程练习

基础:响应与 literal 解析矩阵

构造 tagged/untagged/continuation 与同步/LITERAL+/binary literal 样本走原语。验收标准:五态识别正确;三形态 literal 长度与标记解析一致。

进阶:SELECT 与新邮件监视

SELECT 收摘要(Present 位验证)→ IDLE 进入→模拟 EXISTS 推送→MailboxInfoUpdate 合并→Done 退出。验收标准:摘要的 EXISTS/RECENT 随事件更新;IDLE 期间的 untagged 分态处理正确。

挑战:流水线 FETCH 抓取器

显式 tag 流水线并发 4 条 FETCH(不同邮件区间)——按到达消费、tag 关联、literal 分段写盘。验收标准:乱序响应正确关联;20 MB 邮件内存恒定;预算超限路径干净 Abort。

小结

知识点速查
模型服务器权威:文件夹/标记/搜索/FETCH 部分获取都在服务端
响应tagged(tag 关联)/untagged(插队事件)/continuation(literal 续行)
literal同步 {N}/LITERAL+ {N+}/binary ~{N};解析只给长度——数据分段读
预算纪律事件先查 Size 再决定;低层无默认上限;message 层 64 MiB 独立
两级模型低层流水线(显式 tag)/顺序层(自动 tag+分态 Next)
视图失效Receive/Next/ReadLiteral 三失效点——跨调用必复制
命令层SELECT 零分配摘要(Present 位);流式命令不截流
IDLE服务器推送——Done 结束;事件走同一 Receive
TLS/压缩STARTTLS 后重 CAPABILITY;COMPRESS 配置先 Valid
失败三分线路 FAILED 不可复用/NO-BAD 可恢复继续/Abort 幂等

📖 本章涉及的 API 参考手册:ximap-imap(13 个条目)ximap-imap_client(25 个条目)ximap-imap_command(23 个条目)

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