本章导读

xmail 收官章。前四章给了数据层(116)与三个协议(117-119);本章合体:mail_compose(高层组合——xmailmessage/xmailattachment 全部借用调用方数据,xrtMailCompose 一行产完整报文:三层 multipart 自动选型(mixed/alternative/related 按内容组合、boundary 彼此不同)、QP/Base64 自动编码、Bcc 只给提交层不进报文);smtp_submit(最高层发送——struct 一次提交,envelope 从消息推导);地址语法层(mail_address:addr-spec 验证、ASCII 默认与 SMTPUTF8 显式开);闭环(Compose→Valid 静态校验→Submit 会话→附件流式编码全程零整报文副本)。层次总图:从 xrtSmtpSubmit(一行)到 CommandWrite(字节)每层自由下钻。

122.1引入

"发一封带附件的 HTML 邮件"在手工 MIME 时代是二十行胶水:选 multipart 结构(mixed 套 related 套 alternative?)、生成不碰撞的 boundary、QP 编正文、Base64 编附件并折行、编码词处理中文 Subject、算 Content-Type……每一项第 116 章都给了原语——但常用组合就该每次手拼吗?mail_compose 的答案是结构化输入+自动选型:你声明"这是正文、这是 HTML 版、这是两张内联图、这是三个附件"——结构由内容自动推导(三层嵌套自动组、boundary 互异自动生成)、编码按类型自动(文本 QP/附件 Base64 分块)、你只在需要确定性输出(测试/归档)时注入 Date/Message-ID/boundary。

Bcc 的处理是安全设计的样本:密送地址只进入 SMTP 提交层的 envelope 收件人(RCPT TO 有他)、不进 RFC 报文(To/Cc 头没有他)——"密送"语义的协议级正确实现;漏了这层(把 Bcc 写进头)就是抄送,收件人互相可见的隐私事故。

122.2概念

Compose:描述、校验与两出口

1描述xmailmessage(From/Reply-To/To/Cc/Bcc/UTF-8 Subject/Text/Html/
2校验xrtMailMessageValid——只查描述(不生成随机值不分配不调 sink)
3出口一xrtMailComposeWrite(描述, sink, ...)——直写流(附件分块编码进 sink)
4出口二xrtMailCompose(描述, &长度)——完整报文(xrtFree 释放)

确定性输出的钥匙:Date/Message-ID/三种 boundary 可由调用方提供——留空才用当前 UTC/安全随机/安全随机(Message-ID 域留空从 From 推导)。测试与归档场景注入固定值→输出逐字节可复现;生产留空→安全随机。自定义字段不能覆盖 Compose 管理的结构字段——结构字段(Content-Type/boundary/编码声明)的所有权归组合层,你加字段加不破结构。

三层 multipart 自动选型

内容组合结构
有普通附件multipart/mixed
纯文本+HTML 同存multipart/alternative
HTML 带内联资源multipart/related
三者齐三层嵌套——boundary 必须彼此不同(组合层保证)

编码策略:文本 QP(第 116 章 MIME 行宽版);附件按固定小块编码 Base64——"不创建随附件大小增长的编码副本"(10 MB 附件的编码峰值=小块不是 13 MB)。这是第 108 章流式正文哲学在编码层的落点。

地址语法与国际化边界

xrtMailAddressValid 验证 addr-spec 并借出 local-part/domain:默认只接受 ASCII local-part 与 DNS 风格域名(支持 quoted local-part 与 domain-literal);XMAIL_ADDRESS_SMTPUTF8 显式设置才接受国际化 local-part/UTF-8 域名——且 SMTP 客户端仍按服务器能力决定发送(语法过了≠对端收)。层界:这层只管报文语法——路径长度、DNS、IDNA、envelope 限制归更高层(第 89 章分层的邮件版:语法层不掺杂传输策略)。

Submit:最高层的发送闭环

xrtSmtpSubmit(客户端, 消息, 截止, 取消):内部组装 MIME(经 Compose)→ envelope 推导(From→MAIL FROM;To+Cc+Bcc→RCPT TO)→ DATA/BDAT 发送——一行完成"结构化描述→线路"。与第 117 章三层对照:CommandWrite(字节)← smtp_client(会话)← submit(业务)——每层往下钻一行、每层往上升一代劳。

收发闭环全景

1发描述→Valid(静态校验)→Compose(结构+编码)→Submit(envelope+会话+流式发送)
2收POP3/IMAP 流式取→MailMessageParse(视图)→multipart 游标→编码词解码→业务
3存量工程收下即转存(117 章管道)+ 发送即组装(本章)——两端都零整报文驻留

122.3示例

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

第一个完整程序:一行 Compose

下面的程序来自 examples/mail/compose——UTF-8 主题文本邮件的最短路径:

extlibs/xmail/examples/mail/compose/main.c源码 ↗
#include <stdio.h>
#include <xmail.h>

#if defined(_WIN32)
#include <fcntl.h>
#include <io.h>
#endif



/* 使用一行式 Compose 构建带 UTF-8 主题的文本邮件。 */
int main(void)
{
	xmailmessage Message;
	xmailaddress To;
	str sOutput;
	size_t iSize;
	bool bOutputOk;

#if defined(_WIN32)
	if ( _setmode(_fileno(stdout), _O_BINARY) == -1 ) {
		fputs("MIME output mode failed\n", stderr);
		return 1;
	}
#endif

	xrtMailMessageInit(&Message);
	Message.From = (xmailaddress){
		XRT_STR_LITERAL("Sender"),
		XRT_STR_LITERAL("sender@example.com")
	};
	To = (xmailaddress){
		XRT_STR_LITERAL("Receiver"),
		XRT_STR_LITERAL("receiver@example.net")
	};
	Message.To = &To;
	Message.ToCount = 1u;
	Message.Subject = XRT_STR_LITERAL("示例邮件");
	Message.Text = XRT_STR_LITERAL("hello from xmail");

	sOutput = xrtMailCompose(&Message, &iSize);
	if ( sOutput == NULL ) {
		return 1;
	}
	bOutputOk = fwrite(sOutput, 1u, iSize, stdout) == iSize;
	if ( fflush(stdout) != 0 ) bOutputOk = false;
	xrtFree(sOutput);
	if ( !bOutputOk ) {
		fputs("MIME output failed\n", stderr);
		return 1;
	}
	return 0;
}
$ python tools/build.py --manifest extlibs/xmail/config/modules.json --suite mail_compose --no-single --jobs 4(输出完整 MIME 报文——含编码词主题与 QP 正文)

刚才发生了什么。 ① MailMessageInit+五字段赋值(发件人/收件人/Subject 示例邮件/正文)——全部借用(字符串字面量直进结构,零复制零树)。② xrtMailCompose 一行产完整报文:中文 Subject 自动编码词(=?utf-8?B?...?=)、纯文本 QP 编码、Date/Message-ID 安全随机——手工 MIME 二十行的全部决策在这里自动完成。③ 输出 fwrite 后 xrtFree——本入口是"完整报文"形态(要流式改 ComposeWrite+sink:附件分块进 sink)。④ 用第 116 章的 MailMessageParse 解回这个输出——发与收用的是同一套解析词汇,闭环自洽。

第二个完整程序:本地 Submit 提交

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

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

/* 本地演示域名只解析到本进程的回环 SMTP 服务。 */
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, "smtp.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;
}

/* 有界读取 DATA,逐字节读取可避免越过终止符消费下一条命令。 */
static bool exampleExpectMessage(xnetstream* pStream, double Deadline)
{
	char sMessage[8193];
	size_t iUsed = 0u;
	while ( iUsed < sizeof(sMessage) - 1u ) {
		xnetbytes* pBytes = xrtNetStreamRecv(pStream, 1u,exampleTimerRemaining(Deadline), NULL);
		xbytesview Bytes;
		if ( pBytes == NULL ) return false;
		Bytes = xrtNetBytesView(pBytes);
		if ( (Bytes.Size != 1u) || (Bytes.Data[0] == 0u) ) {
			xrtNetBytesDestroy(pBytes);
			return false;
		}
		sMessage[iUsed++] = (char)Bytes.Data[0];
		xrtNetBytesDestroy(pBytes);
		if ( (iUsed >= 5u) &&
			(memcmp(sMessage + iUsed - 5u, "\r\n.\r\n", 5u) == 0) ) {
			sMessage[iUsed] = 0;
			return strstr(sMessage, "Subject: Offline SMTP example") != NULL &&
				strstr(sMessage, "Hello from loopback.") != NULL &&
				strstr(sMessage, "sender@example.test") != NULL &&
				strstr(sMessage, "recipient@example.test") != NULL;
		}
	}
	return false;
}

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, "220 smtp.example.invalid ready\r\n",
		pServer->Deadline) &&
		exampleExpect(pStream, "EHLO offline.example\r\n",
			pServer->Deadline) &&
		exampleSend(pStream, "250 smtp.example.invalid\r\n",
			pServer->Deadline) &&
		exampleExpect(pStream, "MAIL FROM:<sender@example.test>\r\n",
			pServer->Deadline) &&
		exampleSend(pStream, "250 sender accepted\r\n",
			pServer->Deadline) &&
		exampleExpect(pStream, "RCPT TO:<recipient@example.test>\r\n",
			pServer->Deadline) &&
		exampleSend(pStream, "250 recipient accepted\r\n",
			pServer->Deadline) &&
		exampleExpect(pStream, "DATA\r\n", pServer->Deadline) &&
		exampleSend(pStream, "354 send message\r\n", pServer->Deadline) &&
		exampleExpectMessage(pStream, pServer->Deadline) &&
		exampleSend(pStream, "250 queued\r\n", pServer->Deadline) &&
		exampleExpect(pStream, "QUIT\r\n", pServer->Deadline) &&
		exampleSend(pStream, "221 closing\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;
	xsmtpclientconfig ClientConfig;
	xmailmessage Message;
	xmailaddress To;
	example_server Server = { 0 };
	xnetengine* pEngine = NULL;
	xnetresolver* pResolver = NULL;
	xnetlistener* pListener = NULL;
	xsmtpclient* 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;
	xrtSmtpClientConfigInit(&ClientConfig);
	ClientConfig.Net.Engine = pEngine;
	ClientConfig.Net.Resolver = pResolver;
	ClientConfig.Net.Host = "smtp.example.invalid";
	ClientConfig.Net.Port = ExampleAddress.Port;
	ClientConfig.Hello = XRT_STR_LITERAL("offline.example");
	pClient = xrtSmtpClientOpen(&ClientConfig,exampleTimerRemaining(Deadline), NULL);
	if ( pClient == NULL ) goto Done;
	xrtMailMessageInit(&Message);
	Message.From = (xmailaddress){
		XRT_STR_LITERAL(""), XRT_STR_LITERAL("sender@example.test")
	};
	To = (xmailaddress){
		XRT_STR_LITERAL(""), XRT_STR_LITERAL("recipient@example.test")
	};
	Message.To = &To;
	Message.ToCount = 1u;
	Message.Subject = XRT_STR_LITERAL("Offline SMTP example");
	Message.Text = XRT_STR_LITERAL("Hello from loopback.");
	if ( !xrtSmtpSubmit(pClient, &Message,exampleTimerRemaining(Deadline), NULL) ||
		!xrtSmtpClientQuit(pClient,exampleTimerRemaining(Deadline), NULL) ) goto Done;
	bOk = true;

Done:
	if ( (pClient != NULL) && !bOk ) (void)xrtSmtpClientAbort(pClient);
	xrtSmtpClientDestroy(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 SMTP submission failed\n", stderr);
		return 1;
	}
	puts("offline SMTP submission: message accepted");
	return 0;
}
$ python tools/build.py --manifest extlibs/xsmtp/config/modules.json --suite smtp_offline_example --no-single --jobs 4offline SMTP submission: message accepted

刚才发生了什么。 ① 离线程序用与第一例相同的结构化邮件接口填写 From、To、Subject 与 Text,并把描述交给 xrtSmtpSubmit。Compose 负责报文,SMTP 客户端负责 envelope 和 DATA,两层使用各自的公开入口;提交代码位于 xsmtp,MIME 组合代码位于 xmail。② 进程内服务器检查实际收到的主题、正文和 DATA 终止行并返回成功响应,客户端再执行 QUIT。只有客户端和服务器都成功,程序才输出接受信息,因此这是一项可执行的提交闭环。③ 演示收件地址与明文线路只指向本地夹具,不会向外部邮箱发信;配置真实服务时使用提交范例及 README 指定的 CA、账户与运行时凭据。提交成功仅代表服务器接受,不等于目标收件箱已经投递;生产调用方应分别记录提交结果、关闭诊断和独立收信证据。

122.4契约

  • 借用描述:message/attachment 全借用——零隐藏连接/零字典/零递归 owned 树。
  • Valid 纯校验:不生成随机值、不分配、不调 sink——首次网络写入前的静态错误全发现。
  • 两出口:ComposeWrite 直写 sink(附件分块编码)/Compose 完整报文(xrtFree)。
  • 结构自动:三层 mixed/alternative/related 按内容组合;boundary 彼此不同由层保证。
  • 确定性键:Date/Message-ID/boundary 可注入(留空安全随机);Message-ID 域从 From 推导。
  • 字段边界:自定义字段不可覆盖结构字段——结构所有权归组合层。
  • 编码策略:文本 QP;附件固定小块 Base64——零随附件增长的编码副本。
  • Bcc 语义:只进提交层 envelope、不进 RFC 报文——密送协议级正确。
  • 地址默认 ASCII:SMTPUTF8 显式开;语法过≠对端收(能力判断归协议层)。
  • Submit 层:描述→组装→envelope→会话一行;截止/取消贯穿。

常见错误

坑 1:把 Bcc 收件人写进自定义字段

症状:"密送"变成了明送——所有收件人在头里互相可见,隐私事故。

原因:Bcc 的语义由提交层消费(envelope 收件人)实现——报文头没有它是特性。把 Bcc 地址当自定义字段加=手写抄送。

错误写法
message.Bcc = &Secret;
add_custom_field(&message, "Bcc", "secret@x.com");  /* 进报文:明送 */
正确写法
message.Bcc = &Secret;   /* 只设结构字段 */
/* Submit 推导 envelope:RCPT TO 含 secret;报文头无 Bcc——真密送 */
坑 2:附件走 Compose(非 Write)还嫌内存

症状:50 MB 附件经 Compose 完整报文入口——内存里同时有原文+完整报文。

原因:两个出口的语义差:Compose 是"给我完整报文"(必然聚合);流式场景用 ComposeWrite——附件按固定小块编码直进 sink(网络流/文件),峰值=小块。

错误写法
str raw = xrtMailCompose(&Msg, &n);   /* 50MB 聚合:峰值翻倍 */
send_all(raw);
正确写法
/* ComposeWrite + 流式 sink:分块产出直发 */
xrtMailComposeWrite(&Msg, stream_sink, pCtx, ...);  /* 小块直达网络 */
坑 3:测试断言没注入确定性键

症状:两次 Compose 输出对比失败——Date/Message-ID/boundary 每次随机。

原因:留空=安全随机(生产的正确默认);测试要确定性就注入固定值——这是设计好的钥匙不是缺陷。

错误写法
raw1 = xrtMailCompose(&Msg, NULL);
raw2 = xrtMailCompose(&Msg, NULL);
assert(equal(raw1, raw2));   /* 随机 Date/ID:永不相等 */
正确写法
Msg.Date = FixedDate; Msg.MessageId = FixedId;
provide_boundaries(&Msg, "t1", "t2", "t3");
raw1 = Compose(&Msg); raw2 = Compose(&Msg);
assert(equal(raw1, raw2));   /* 确定性:逐字节一致 */

编程练习

基础:三层结构验证

构造三种描述(纯文本+HTML+附件;+内联图)——Compose 输出用第 116 章游标逐层拆解验证结构与 boundary 互异。验收标准:结构选型与表格一致;三层嵌套顺序正确。

进阶:Bcc 闭环实验

带 Bcc 的消息 Submit 到本地测试服务器(或第 104 章自建)——抓线路:RCPT TO 含密送、DATA 报文头无 Bcc。验收标准:两处证据齐;收件方转发的报文里也永不出现密送地址。

挑战:收发全链路工具

组合:ComposeWrite 流式发带 10 MB 附件(第 117 章 DATA 流式)→ POP3 流式收(第 118 章)→ 增量解析还原(第 116 章)→ 附件落盘校验哈希。验收标准:全链内存恒定(MB 级);附件哈希一致;编码词主题往返无损。

小结

知识点速查
定位常见场景便利层:结构化描述→自动 MIME——原语全在第 116 章可达
描述借用message/attachment 零复制;无隐藏连接/字典/owned 树
Valid纯静态校验——首次网络写入前错误全发现
两出口ComposeWrite(流式 sink、分块编码)/Compose(完整报文)
结构自动mixed/alternative/related 三层按内容;boundary 互异层保证
确定性键Date/Message-ID/boundary 可注入;留空安全随机
编码文本 QP;附件小块 Base64——零随大小增长的副本
Bccenvelope 有、报文无——密送的协议级正确
地址默认 ASCII;SMTPUTF8 显式;语法≠可送达
Submit一行闭环;与 CommandWrite 之间的每层可下钻

📖 本章涉及的 API 参考手册:xmail-mail_compose(4 个条目)xsmtp-smtp_submit(2 个条目)xmail-mail(3 个条目)

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