本章导读
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:描述、校验与两出口
确定性输出的钥匙: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(业务)——每层往下钻一行、每层往上升一代劳。
收发闭环全景
122.3示例
本章命令在仓库根目录执行,构建器按清单选择模块、公共头和平台链接库,并输出构建及依赖测试日志。终端块摘录范例自身的输出。第二个程序在进程内启动回环服务器,只使用固定演示数据;真实服务的主机、端口、CA 与运行时凭据配置见相应客户端库的 README。
第一个完整程序:一行 Compose
下面的程序来自 examples/mail/compose——UTF-8 主题文本邮件的最短路径:
#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; }
刚才发生了什么。 ① 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.
#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; }
刚才发生了什么。 ① 离线程序用与第一例相同的结构化邮件接口填写 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→会话一行;截止/取消贯穿。
常见错误
症状:"密送"变成了明送——所有收件人在头里互相可见,隐私事故。
原因:Bcc 的语义由提交层消费(envelope 收件人)实现——报文头没有它是特性。把 Bcc 地址当自定义字段加=手写抄送。
message.Bcc = &Secret; add_custom_field(&message, "Bcc", "secret@x.com"); /* 进报文:明送 */
message.Bcc = &Secret; /* 只设结构字段 */ /* Submit 推导 envelope:RCPT TO 含 secret;报文头无 Bcc——真密送 */
症状:50 MB 附件经 Compose 完整报文入口——内存里同时有原文+完整报文。
原因:两个出口的语义差:Compose 是"给我完整报文"(必然聚合);流式场景用 ComposeWrite——附件按固定小块编码直进 sink(网络流/文件),峰值=小块。
str raw = xrtMailCompose(&Msg, &n); /* 50MB 聚合:峰值翻倍 */ send_all(raw);
/* ComposeWrite + 流式 sink:分块产出直发 */ xrtMailComposeWrite(&Msg, stream_sink, pCtx, ...); /* 小块直达网络 */
症状:两次 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 的消息 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——零随大小增长的副本 |
| Bcc | envelope 有、报文无——密送的协议级正确 |
| 地址 | 默认 ASCII;SMTPUTF8 显式;语法≠可送达 |
| Submit | 一行闭环;与 CommandWrite 之间的每层可下钻 |
📖 本章涉及的 API 参考手册:xmail-mail_compose(4 个条目)xsmtp-smtp_submit(2 个条目)xmail-mail(3 个条目)
点击查看完整签名、参数约束、返回值与错误说明