Ориентация главы
Заключительная глава xmail. Предыдущие четыре дали слой данных (115) и три протокола (116–118); эта глава сливает их: mail_compose (высокоуровневая композиция — xmailmessage/xmailattachment заимствуют все данные вызывающего; xrtMailCompose одной строкой даёт полное сообщение: трёхслойный автоматический выбор multipart (mixed/alternative/related комбинируются по содержимому, границы взаимно различны), автоматическое кодирование QP/Base64, Bcc идёт только слою отправки, в сообщение не попадает); smtp_submit (высшая отправка — одна структурная отправка, конверт выводится из сообщения; соединение держит вызывающий); слой синтаксиса адресов (mail_address: валидация addr-spec, ASCII по умолчанию с явным SMTPUTF8); замкнутый цикл (Compose → статическая проверка Valid → сессия Submit → вложения кодируются потоково, ноль копий целого сообщения на всём пути). Карта слоёв: от xrtSmtpSubmit (одна строка) до CommandWrite (байты) — каждый уровень свободен для спуска.
122.1引入
«Отправить HTML-письмо с вложением» в эпоху ручного MIME — двадцать строк клея: выбрать структуру multipart (mixed оборачивает related оборачивает alternative?), сгенерировать не сталкивающиеся границы, QP-кодировать тело, Base64-кодировать вложения с переносами, обработать кодированные слова китайского Subject, посчитать Content-Type… глава 116 дала примитивы для всего — но неужели распространённые комбинации каждый раз склеивать руками? Ответ mail_compose — структурированный ввод + автоматический выбор технологии: вы объявляете «вот тело, вот HTML-версия, вот две встроенные картинки, вот три вложения» — структура выводится из содержимого автоматически (трёхслойная вложенность собирается сама, границы генерируются взаимно различными), кодирование автоматично по типу (текст QP / вложения Base64 кусками), а Date/Message-ID/boundary вы вставляете, только когда нужен детерминированный вывод (тесты/архивы).
Обработка Bcc — образец проектирования безопасности: адрес скрытой копии входит только в получателей конверта SMTP-слоя отправки (в RCPT TO он есть), в RFC-сообщение — никогда (в заголовках To/Cc его нет) — протокольно корректная реализация «скрытой копии»; пропустить этот слой (вписать Bcc в заголовки) — превратить её в открытую копию, приватную аварию, где получатели видят друг друга.
122.2概念
Compose: описание, валидация и два выхода
Ключ детерминированного вывода: Date/Message-ID/три границы могут предоставляться вызывающим — пустыми остаются текущий UTC / криптостойкая случайность / криптостойкая случайность (домен Message-ID пуст — выводится из From). Тесты и архивы вставляют фиксированные значения → вывод воспроизводим байт-в-байт; продакшн оставляет пустыми → криптостойкая случайность. Свои поля не могут перекрыть структурные поля, управляемые Compose — владение структурными полями (Content-Type/boundary/объявления кодировок) принадлежит слою композиции; добавленные поля структуру не ломают.
Трёхслойный автоматический выбор multipart
| Комбинация содержимого | Структура |
|---|---|
| обычные вложения есть | multipart/mixed |
| чистый текст + HTML сосуществуют | multipart/alternative |
| HTML со встроенными ресурсами | multipart/related |
| все три вместе | трёхслойная вложенность — границы обязаны различаться (гарантия слоя композиции) |
Политика кодирования: текст QP (MIME-форма ширины строк главы 116); вложения Base64 фиксированными малыми кусками — «без кодированной копии, растущей с размером вложения» (пик кодирования вложения 10 МБ = кусок, а не 13 МБ). Это приземление философии потоковых тел главы 108 на слое кодирования.
Синтаксис адресов и граница интернационализации
xrtMailAddressValid валидирует addr-spec и отдаёт взаймы local-part/domain: по умолчанию принимает только ASCII local-part и DNS-стиль домена (поддерживаются цитируемые local-part и domain-literal); только с явно установленным XMAIL_ADDRESS_SMTPUTF8 принимаются интернационализированные local-part / UTF-8-домены — и SMTP-клиент всё равно решает отправку по возможностям сервера (синтаксис прошёл ≠ противоположная сторона примет). Граница слоёв: этот слой ведает только синтаксисом сообщения — длины пути, DNS, IDNA, лимиты конверта принадлежат верхним слоям (расслоение главы 89 в почтовой редакции: синтаксический слой без транспортной политики).
Submit: замкнутая отправка высшего уровня
xrtSmtpSubmit(客户端, 消息, 截止, 取消) (клиент, сообщение, дедлайн, отмена): внутри собирается MIME (через Compose) → выводится конверт (From→MAIL FROM; To+Cc+Bcc→RCPT TO) → отправка через DATA/BDAT — одной строкой от «структурированного описания» до «линии». Против трёх слоёв главы 117: CommandWrite (байты) ← smtp_client (сессия) ← submit (бизнес) — каждый уровень вниз — строка спуска, каждый вверх — поколение делегирования.
Панорама замкнутого цикла отправки-приёма
122.3示例
Выполняйте команды из корня репозитория. Сборщик выбирает модули, публичные заголовки и системные библиотеки по манифесту, а также печатает журнал сборки и тестов зависимостей. Терминальные блоки показывают вывод примера. Вторая программа запускает сервер loopback внутри процесса и использует фиксированные демонстрационные данные. Параметры реального сервиса, 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; }
Что только что произошло. (1) MailMessageInit + пять присваиваний полей (отправитель/получатель/Subject 示例邮件 (пример письма)/тело) — всё заимствовано (строковые литералы входят в структуру напрямую, ноль копий, ноль деревьев). (2) xrtMailCompose одной строкой даёт полное сообщение: китайский Subject автоматически в кодированные слова (=?utf-8?B?...?=), чистый текст QP-кодируется, Date/Message-ID криптостойко-случайны — все решения двадцати строк ручного MIME исполняются здесь автоматически. (3) После fwrite вывода — xrtFree; этот выход есть форма «полного сообщения» (нужен поток — переключитесь на ComposeWrite+sink: вложения идут в sink кусками). (4) Разберите этот вывод обратно MailMessageParse главы 116 — отправка и приём пользуются одним словарём разбора, цикл самосогласован.
Вторая полная программа: локальный 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; }
Что только что произошло. (1) Офлайн-программа заполняет From, To, Subject и Text тем же интерфейсом структурированного письма, что в первом примере, затем передаёт описание в xrtSmtpSubmit. Compose формирует сообщение, SMTP-клиент ведёт envelope и DATA. Отправка относится к xsmtp, композиция MIME к xmail; каждый слой использует публичный API. (2) Внутрипроцессный сервер проверяет реально полученные тему, тело и завершающую строку DATA, возвращает успех; клиент отправляет QUIT. Принятие печатается только при успехе обеих сторон, образуя исполняемый цикл отправки. (3) Демонстрационные адреса и открытый трафик остаются внутри локального стенда, без отправки во внешние ящики. Реальные сервисы используют пример отправки и настройки CA, аккаунта и учётных данных из README. Успешная отправка означает принятие сервером, а не доставку во входящие; производственный вызывающий код отдельно фиксирует отправку, диагностику закрытия и независимое подтверждение получения.
122.4契约
- Заимствованное описание: message/attachment целиком заимствованы — ноль скрытых связей / ноль словарей / ноль рекурсивных owned-деревьев.
- Valid — чистая проверка: случайных значений не генерирует, не выделяет, sink не зовёт — все статические ошибки до первой записи в сеть.
- Два выхода: ComposeWrite пишет прямо в sink (вложения кодируются кусками) / Compose даёт полное сообщение (xrtFree).
- Структура автоматична: три слоя mixed/alternative/related комбинируются по содержимому; взаимная различность границ гарантирует слой.
- Ключи детерминизма: Date/Message-ID/границы вставляемы (пусто = криптостойкая случайность); домен Message-ID выводится из From.
- Граница полей: свои поля не перекрывают структурные — владение структурой у слоя композиции.
- Политика кодирования: текст QP; вложения Base64 фиксированными кусками — ноль кодированных копий, растущих с размером.
- Семантика Bcc: только в конверт слоя отправки, никогда в RFC-сообщение — протокольно корректная скрытая копия.
- Адреса ASCII по умолчанию: SMTPUTF8 явно; синтаксис прошёл ≠ доставляемо (суждение возможностей — протокольному слою).
- Слой Submit: описание → сборка → конверт → сессия одной строкой; дедлайн/отмена насквозь.
Типичные ошибки
Симптом: «скрытая копия» становится открытой — все получатели видят друг друга в заголовках; приватная авария.
Причина: семантика Bcc реализуется потреблением слоя отправки (получатели конверта) — его отсутствие в заголовках сообщения и есть свойство. Добавить адрес Bcc как своё поле = вписать открытую копию руками.
message.Bcc = &Secret; add_custom_field(&message, "Bcc", "secret@x.com"); /* в сообщение: открытая копия */
message.Bcc = &Secret; /* только структурное поле */ /* Submit выводит конверт: RCPT TO содержит secret; в заголовках Bcc нет - настоящая скрытая копия */
Симптом: вложение 50 МБ через выход полного сообщения Compose — в памяти одновременно оригинал и полное сообщение.
Причина: у двух выходов разная семантика: Compose — «дай мне полное сообщение» (агрегация неизбежна); для потоковой передачи — ComposeWrite: вложения кодируются фиксированными малыми кусками прямо в sink (сетевой поток/файл), пик = кусок.
str raw = xrtMailCompose(&Msg, &n); /* 50МБ агрегировано: пик удвоен */ send_all(raw);
/* ComposeWrite + потоковый sink: куски выходят напрямую */ xrtMailComposeWrite(&Msg, stream_sink, pCtx, ...); /* малые куски прямо в сеть */
Симптом: сравнение двух выводов Compose проваливается — Date/Message-ID/границы каждый раз случайны.
Причина: пусто = криптостойкая случайность (правильное продакшн-умолчание); тестам для детерминизма нужно вставить фиксированные значения — это спроектированный ключ, а не дефект.
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, проверьте структуру и взаимную различность границ. Критерии приёмки: подбор структуры сходится с таблицей; порядок трёхслойной вложенности верен.
Сообщение с Bcc отправить Submit на локальный тестовый сервер (или самосбор главы 104) — снять линию: RCPT TO содержит скрытую копию, в заголовках сообщения DATA поля Bcc нет. Критерии приёмки: оба доказательства на месте; адрес скрытой копии никогда не появляется и в пересылаемых получателями сообщениях.
Композиция: ComposeWrite потоково отправляет вложение 10 МБ (DATA-поток главы 117) → потоковый приём POP3 (глава 118) → инкрементальный разбор восстанавливает (глава 116) → вложение пишется на диск с проверкой хеша. Критерии приёмки: память постоянна по всей цепочке (уровня МБ); хеши вложения совпадают; тема с кодированными словами проходит круг без потерь.
Итоги
| Тема | Кратко |
|---|---|
| Позиционирование | слой удобств распространённых сценариев: структурированное описание → автоматический MIME — все примитивы достижимы в главе 115 |
| Заимствование описания | message/attachment ноль копий; без скрытых связей/словарей/owned-деревьев |
| Valid | чистая статическая проверка — все ошибки до первой записи в сеть |
| Два выхода | ComposeWrite (потоковый sink, кодирование кусками) / Compose (полное сообщение) |
| Структура автоматична | mixed/alternative/related три слоя по содержимому; различность границ гарантирует слой |
| Ключи детерминизма | Date/Message-ID/границы вставляемы; пусто = криптостойкая случайность |
| Кодирование | текст QP; вложения Base64 кусками — ноль копий, растущих с размером |
| Bcc | в конверте есть, в сообщении нет — протокольно корректная скрытая копия |
| Адреса | ASCII по умолчанию; SMTPUTF8 явно; синтаксис ≠ доставляемо |
| Submit | однострочный цикл; каждый слой между ним и CommandWrite доступен для спуска |
📖 Справочники API в этой главе:xmail-mail_compose (4 записей)xsmtp-smtp_submit (2 записей)xmail-mail (3 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки