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
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 combination | Structure |
|---|---|
| plain attachments present | multipart/mixed |
| plain text + HTML coexisting | multipart/alternative |
| HTML with inline resources | multipart/related |
| all three together | three-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
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:
#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; }
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.
#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; }
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
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 */
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 */
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
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.
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.
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
| Topic | Quick reference |
|---|---|
| Positioning | the convenience layer for common scenarios: structured description → automatic MIME — every primitive reachable in Chapter 116 |
| Description borrowing | message/attachment zero copy; no hidden links/dictionaries/owned trees |
| Valid | pure static validation — every error found before the first network write |
| Two outlets | ComposeWrite (streaming sink, chunked encoding) / Compose (complete message) |
| Structure automatic | mixed/alternative/related three layers per content; boundary distinctness guaranteed by the layer |
| Determinism keys | Date/Message-ID/boundaries injectable; empty = secure random |
| Encoding | text QP; attachments chunked Base64 — zero copies growing with size |
| Bcc | in the envelope, not the message — protocol-level correct blind copy |
| Addresses | ASCII by default; SMTPUTF8 explicit; syntax ≠ deliverable |
| Submit | a 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