Chapter Orientation

xmail's concluding chapter. The previous four gave the data layer (115) and three protocols (116–118); this chapter fuses them: mail_compose (high-level composition — xmailmessage/xmailattachment borrow all caller data; xrtMailCompose produces a complete message in one line: three-layer automatic multipart selection (mixed/alternative/related combined per content, boundaries distinct from each other), QP/Base64 automatic encoding, Bcc goes only to the submission layer, never into the message); smtp_submit (the highest-level send — one struct submission, the envelope derived from the message); the address syntax layer (mail_address: addr-spec validation, ASCII default with SMTPUTF8 explicitly enabled); the loop (Compose → Valid static validation → Submit session → attachments streamed and encoded with zero whole-message copies throughout). The layer map: from xrtSmtpSubmit (one line) down to CommandWrite (bytes), every level free to drill into.

122.1引入

"Send an HTML mail with attachments" in the hand-made MIME era was twenty lines of glue: pick the multipart structure (mixed wrapping related wrapping alternative?), generate non-colliding boundaries, QP the body, Base64 the attachments with line folding, encoded-word the Chinese Subject, compute Content-Type… Chapter 116 gave primitives for every one — but should common combinations be hand-spliced every time? mail_compose's answer is structured input + automatic selection: you declare "this is the body, this is the HTML version, these are two inline images, these are three attachments" — the structure is automatically derived from the content (three-layer nesting assembled automatically, boundaries auto-generated and distinct), encodings automatic by type (text QP / attachments Base64 in chunks), and you inject Date/Message-ID/boundary only when you need deterministic output (testing/archiving).

Bcc's handling is a sample of security design: the blind-copy address enters only the SMTP submission layer's envelope recipients (RCPT TO has it) and never enters the RFC message (the To/Cc headers don't) — the protocol-level correct implementation of "blind copy"; skipping this layer (writing Bcc into the headers) is just carbon copy, a privacy incident where recipients see each other.

122.2概念

Compose: description, validation, and two outlets

1Describexmailmessage (From/Reply-To/To/Cc/Bcc/UTF-8 Subject/Text/Html/
2ValidatexrtMailMessageValid - checks the description only (no random values generated, no allocation, no sink called)
3Outlet onexrtMailComposeWrite(description, sink, ...) - write straight to a stream (attachments encoded into the sink in chunks)
4Outlet twoxrtMailCompose(description, &length) - the complete message (released with xrtFree)

The key to deterministic output: Date/Message-ID/the three boundaries may be provided by the caller — left empty, they use current UTC / secure randomness / secure randomness (with the Message-ID domain empty, derived from From). Testing and archiving inject fixed values → output reproducible byte for byte; production leaves them empty → secure randomness. Custom fields cannot override the structural fields Compose manages — ownership of the structural fields (Content-Type/boundary/encoding declarations) belongs to the composition layer; your added fields cannot break the structure.

Three-layer automatic multipart selection

Content combinationStructure
plain attachments presentmultipart/mixed
plain text + HTML coexistingmultipart/alternative
HTML with inline resourcesmultipart/related
all three togetherthree-layer nesting — boundaries must differ from each other (guaranteed by the composition layer)

Encoding policy: text QP (Chapter 116's MIME line-width form); attachments Base64-encoded in fixed small chunks — "no encoding copy that grows with attachment size" (a 10 MB attachment's encoding peak = the chunk, not 13 MB). This is Chapter 108's streaming-body philosophy landing at the encoding layer.

Address syntax and the internationalization boundary

xrtMailAddressValid validates the addr-spec and lends out local-part/domain: by default it accepts only ASCII local-parts and DNS-style domains (quoted local-parts and domain-literals supported); only with XMAIL_ADDRESS_SMTPUTF8 explicitly set does it accept internationalized local-parts / UTF-8 domains — and the SMTP client still decides sending by server capability (syntax passing ≠ the peer accepts). Layer boundary: this layer owns message syntax only — path length, DNS, IDNA, envelope limits belong to higher layers (Chapter 89's layering, mail edition: the syntax layer carries no transport policy).

Submit: the highest-level send loop

xrtSmtpSubmit(客户端, 消息, 截止, 取消) (client, message, deadline, cancel): internally assembles MIME (via Compose) → derives the envelope (From→MAIL FROM; To+Cc+Bcc→RCPT TO) → sends via DATA/BDAT — one line from "structured description" to "wire". Against Chapter 117's three layers: CommandWrite (bytes) ← smtp_client (session) ← submit (business) — each level down drills one line, each level up delegates one generation.

The send-receive loop panorama

1Senddescribe -> Valid (static validation) -> Compose (structure + encoding) -> Submit (envelope + session + streaming send)
2ReceivePOP3/IMAP streaming fetch -> MailMessageParse (views) -> multipart cursor -> encoded-word decode -> business
3Legacy engineeringarchive on receipt (Chapter 118's pipeline) + assemble on send (this chapter) - zero whole-message residency at both ends

122.3示例

Run these commands from the repository root. The builder selects modules, public headers and platform libraries from the manifest, and also prints build and dependency-test logs. Terminal blocks show the example output. The second program starts a loopback server within the process and uses fixed demonstration data. See the client library README for real-service host, port, CA and runtime credential configuration.

First complete program: one-line Compose

The program below is from examples/mail/compose — the shortest path to a UTF-8-subject text mail:

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 正文)

What just happened. (1) MailMessageInit + five field assignments (sender/recipient/Subject 示例邮件 (sample mail)/body) — all borrowed (string literals go straight into the struct, zero copy, zero tree). (2) xrtMailCompose produces the complete message in one line: the Chinese Subject automatically encoded-worded (=?utf-8?B?...?=), plain text QP-encoded, Date/Message-ID secure-random — every decision of hand-made MIME's twenty lines happens automatically here. (3) After fwrite of the output, xrtFree — this outlet is the "complete message" form (for streaming, switch to ComposeWrite+sink: attachments enter the sink in chunks). (4) Parse this output back with Chapter 116's MailMessageParse — sending and receiving use the same parsing vocabulary, the loop is self-consistent.

Second complete program: local Submit

The second program is from 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

What just happened. (1) The offline program fills From, To, Subject and Text using the same structured-message interface as the first example, then passes the description to xrtSmtpSubmit. Compose owns the message; the SMTP client owns envelope and DATA. Submission belongs to xsmtp, MIME composition to xmail, with each layer using its public API. (2) The in-process server checks the subject, body and DATA terminator actually received, then returns success; the client sends QUIT. Acceptance is printed only when both client and server succeed, making this an executable submission loop. (3) Demonstration addresses and plaintext traffic stay within the local fixture and send nothing to external mailboxes. Real services use the submission example and README configuration for CA, account and runtime credentials. Submission success means server acceptance, not arrival in the recipient inbox; production callers record submission, shutdown diagnostics and independent receipt evidence separately.

122.4契约

  • Borrowed description: message/attachment all borrowed — zero hidden links / zero dictionaries / zero recursive owned trees.
  • Valid is pure validation: generates no random values, allocates nothing, calls no sink — every static error found before the first network write.
  • Two outlets: ComposeWrite writes straight to a sink (attachments encoded in chunks) / Compose yields the complete message (xrtFree).
  • Structure automatic: three layers mixed/alternative/related combined per content; boundaries distinct, guaranteed by the layer.
  • Determinism keys: Date/Message-ID/boundaries injectable (empty = secure random); the Message-ID domain derived from From.
  • Field boundary: custom fields cannot override structural fields — structural ownership belongs to the composition layer.
  • Encoding policy: text QP; attachments fixed-chunk Base64 — zero encoding copies growing with size.
  • Bcc semantics: envelope only, never the RFC message — protocol-level correct blind copy.
  • Addresses ASCII by default: SMTPUTF8 explicit; syntax passing ≠ deliverable (capability judgment belongs to the protocol layer).
  • The Submit layer: description → assembly → envelope → session in one line; deadline/cancellation throughout.

Common Mistakes

Pitfall 1: writing the Bcc recipient into a custom field:Pitfall 1: writing the Bcc recipient into a custom field

Symptom: the "blind copy" becomes a public copy — all recipients see each other in the headers; a privacy incident.

Cause: Bcc's semantics are implemented by consumption at the submission layer (envelope recipients) — its absence from the message headers is a feature. Adding the Bcc address as a custom field = hand-writing a carbon copy.

错误写法
message.Bcc = &Secret;
add_custom_field(&message, "Bcc", "secret@x.com");  /* into the message: public copy */
正确写法
message.Bcc = &Secret;   /* set the structural field only */
/* Submit derives the envelope: RCPT TO includes secret; no Bcc in the headers - a true blind copy */
Pitfall 2: attachments through Compose (not Write) with memory complaints:Pitfall 2: attachments through Compose (not Write) with memory complaints

Symptom: a 50 MB attachment through Compose's complete-message outlet — memory holds the original and the complete message simultaneously.

Cause: the two outlets differ semantically: Compose is "give me the complete message" (necessarily aggregating); for streaming use ComposeWrite — attachments encode in fixed small chunks straight into the sink (network stream/file); the peak = the chunk.

错误写法
str raw = xrtMailCompose(&Msg, &n);   /* 50MB aggregated: doubled peak */
send_all(raw);
正确写法
/* ComposeWrite + a streaming sink: chunked output goes straight out */
xrtMailComposeWrite(&Msg, stream_sink, pCtx, ...);  /* small chunks straight to the network */
Pitfall 3: test assertions without injecting the determinism keys:Pitfall 3: test assertions without injecting the determinism keys

Symptom: two Compose outputs fail comparison — Date/Message-ID/boundaries are random each time.

Cause: empty = secure randomness (the correct production default); tests wanting determinism inject fixed values — this is a designed key, not a defect.

错误写法
raw1 = xrtMailCompose(&Msg, NULL);
raw2 = xrtMailCompose(&Msg, NULL);
assert(equal(raw1, raw2));   /* random Date/ID: never equal */
正确写法
Msg.Date = FixedDate; Msg.MessageId = FixedId;
provide_boundaries(&Msg, "t1", "t2", "t3");
raw1 = Compose(&Msg); raw2 = Compose(&Msg);
assert(equal(raw1, raw2));   /* deterministic: byte-identical */

Programming Exercises

Basic: three-layer structure verification

Construct three descriptions (plain text + HTML + attachments; + inline image) — Compose's output disassembled layer by layer with Chapter 116's cursor, verifying structure and boundary distinctness. Acceptance criteria: structure selection matches the table; the three-layer nesting order is correct.

Advanced: the Bcc loop experiment

Submit a message with Bcc to a local test server (or Chapter 104's self-built) — capture the wire: RCPT TO includes the blind copy, the DATA message headers carry no Bcc. Acceptance criteria: both pieces of evidence present; the blind-copy address never appears in messages forwarded by recipients either.

Challenge: a full send-receive chain tool

Compose: ComposeWrite streams a 10 MB attachment (Chapter 117's DATA streaming) → POP3 streaming receipt (Chapter 118) → incremental parsing restores (Chapter 116) → attachment written to disk and hash-checked. Acceptance criteria: memory constant across the chain (MB scale); attachment hashes match; the encoded-word subject round-trips losslessly.

Summary

TopicQuick reference
Positioningthe convenience layer for common scenarios: structured description → automatic MIME — every primitive reachable in Chapter 116
Description borrowingmessage/attachment zero copy; no hidden links/dictionaries/owned trees
Validpure static validation — every error found before the first network write
Two outletsComposeWrite (streaming sink, chunked encoding) / Compose (complete message)
Structure automaticmixed/alternative/related three layers per content; boundary distinctness guaranteed by the layer
Determinism keysDate/Message-ID/boundaries injectable; empty = secure random
Encodingtext QP; attachments chunked Base64 — zero copies growing with size
Bccin the envelope, not the message — protocol-level correct blind copy
AddressesASCII by default; SMTPUTF8 explicit; syntax ≠ deliverable
Submita one-line loop; every layer between it and CommandWrite drillable

📖 API references in this chapter:xmail-mail_compose (4 entries)xsmtp-smtp_submit (2 entries)xmail-mail (3 entries)

Click for full signatures, parameter constraints, return values and error notes