Ориентация главы
Серия xmail открывается слоем данных — что такое почтовое сообщение и как его читать. Проектная позиция проходит через все модули: никаких деревьев объектов, никакого копирования сообщения, никакого принудительного builder — xrtMailMessageParse даёт заимствованные представления (массив полей + граница тела), multipart — потоковый курсор, кодированные слова — «разбор границ + подключаемые кодировки». Четыре блока: ядро (mail_core: домен ошибок xrt.mail, нормализация CRLF, предикат синтаксиса границ); представление сообщения (mail_message: строгий разбор CRLF + декодирование тела по транспортной кодировке); multipart (mail_multipart: MIME-курсор без выделений + сборка по трём меткам); кодированные слова и кодировки (mail_word: слова RFC 2047 =?utf-8?B?...?=; mail_codec: QP/Base64 в строчно-широкой MIME-форме). Три протокольные главы (SMTP/POP3/IMAP) и глава композиции строятся на этом слое.
118.1引入
Электронная почта — старейший «формат документов» человеческой сети: родословная RFC 822 1982 года плюс MIME-расширения 1996 года; сорок лет багажа совместимости живут в зоне полей: CRLF — железный закон (сообщения с голым LF отвергаются напрочь многими серверами), кодированные слова (Subject: =?utf-8?B?...?= — не-ASCII, экранированный внутри ASCII-заголовков), multipart (разделяемое границами дерево частей — структура вложений), транспортные кодировки (QP/Base64 — форма двоичного в ASCII-канале). У каждого пункта исторический разброс «снисходительно/строго», и снисходительность — инкубатор инъекций (инъекция полей заголовка — классический путь подделки почты).
Ответ xmail — строгий разбор + представления без деревьев: CRLF строго (MailCrlfWrite нормализует; парсер требует строгого CRLF), кодированные слова строго (пустые кодировки/нелегальные маркеры/управляющие байты/сверх 75 байтов отвергаются — инъекционные байты отвергаются на месте), multipart строго (принимается только граница, полностью совпадающая в начале физической строки, — подделка голым переводом строки отвергается), представления без копий (заимствуют ввод; «высокоуровневые деревья объектов не загораживают путь к исходному сообщению» — хотите дерево, соберите сами; исходные байты всегда напрямую достижимы).
118.2概念
Ядро: домен ошибок, CRLF и границы
mail_core даёт три инфраструктурные вещи: стабильный домен ошибок xrt.mail (конфигурация/строка/поле/кодировка/charset/адрес/MIME/протокол/лимиты — у каждого своё стабильное кодовое значение; нижележащие ошибки XRT Base64/сети распространяются как есть — самая конкретная причина доходит до вызывающего); xrtMailCrlfWrite/Crlf (нормализация CRLF — двухчастная/выделяющая двойная форма; явная длина сохраняет ноль байтов; перекрытие отвергается); xrtMailBoundaryValid (чисто синтаксический предикат без зависимостей от ГСЧ/контейнеров/парсеров — легальность границы до 70 байтов; случайная генерация живёт в отдельном модуле mail_id).
Представление сообщения: разбор и декодирование тела
Бюджеты по умолчанию (HEADER_BYTES_DEFAULT/HEADERS_DEFAULT) защищают от злобно раздутых зон заголовков; SIZE_MAX отменяет явно — бюджет есть явное решение, а не бесконечное умолчание. Никакой автоматической интерпретации charset/Content-Type/multipart — эти три продолжаются в mail_word/mail_param/mail_multipart (свобода композиции: нужно только тело — остановитесь здесь; нужна MIME-структура — пройдите ещё слой).
multipart: потоковый курсор и три метки
Сторона чтения: xmailmultipartcursor держит заимствованные Source/Boundary/Preamble/Epilogue и позицию продвижения; MultipartNext выдаёт на каждую часть xmailmultipartview (Headers/Body заимствуют исходный ввод). Строгость: принимается только граница, полностью совпадающая в начале физической строки (--b\r\n); голые переводы строк, нелегальные поля части, отсутствующая закрывающая линия, превышение бюджета — всё отвергается. Тела частей не декодируются — транспортная кодировка каждой части объявляется и декодируется своя (представление сообщения переиспользуется по частям). Сторона записи: три метки xmailmultipartmark (FIRST/NEXT/CLOSE) через MultipartMarkWrite дают готовые к отправке фрагменты-разделители — вызывающий пишет поля и тело прямо в сетевой поток, избегая склейки целого сообщения (большие вложения не живут в памяти — потоковые тела главы 108 в почтовой редакции).
Кодированные слова: границы и политика кодировок RFC 2047
Форма =?charset?B/Q?text?= — канал не-ASCII внутри ASCII-заголовков. Трёхслойный дизайн: слой представлений (MailWordParse: чтение границ без выделений — представления Charset/Language/Encoding/текста; charset*language RFC 2231 разъединяется) — гнездо для кодировок, которые xmail не встраивает (GB18030/Big5/Shift_JIS и другие большие таблицы соответствий не входят в сборку по умолчанию: берёте границу + исходный текст, применяете собственный конвертер); кодирование (WordEncodeWrite: ввод UTF-8 — чистый ASCII проходит как есть, не-ASCII или способное ошибочно читаться =? кодируется в слова ≤75 байтов, разбиение только по границам UTF-8-скаляров; Base64 по умолчанию / Q для phrase-безопасного набора); декодирование (WordDecodeWrite: смешанный текст + кодированные слова; пробелы/фолдинг между соседними декодируемыми словами игнорируются, прочий фолдинг нормализуется в один пробел; строгий режим встроенно знает UTF-8/ASCII/Latin-1/CP1252-алиасы и отвергает инъекционно-опасные управляющие байты; режим RELAXED сохраняет неизвестные кодировки дословно, не гадая).
Транспортные кодировки: QP и Base64 в MIME-версии
mail_codec — MIME-специализация поверх ядра XRT (глава 28): QP (QpWrite — ширина строки по умолчанию 76, = резервируется перед мягкими переносами, хвостовые пробелы/табы всегда кодируются; режим BINARY считает CR/LF обычными байтами, режим TEXT нормализует к CRLF; декодирование строго отвергает битые escape и поддерживает усадку на месте); Base64 (Base64Write — группы максимум по 57 байтов зовут ядро напрямую без построения полной копии, каждая строка завершается CRLF, ширина строки кратна 4 в пределах 4..76; декодирование допускает только дополнительный MIME-пробел, отвергая неканонический паддинг и ненулевые хвостовые биты).
118.3示例
Первая полная программа: разбор и декодирование тела
Программа ниже из examples/mail/message — минимальный цикл чтения письма:
#include <stdio.h> #include <xmail.h> /* 解析原始邮件并按报文声明解码正文。 */ int main(void) { xmailmessageview Message; xmailtransfer Transfer; bytes pBody; size_t iSize; if ( !xrtMailMessageParse( XRT_STR_LITERAL( "Subject: xmail message\r\n" "Content-Transfer-Encoding: base64\r\n" "\r\n" "aGVsbG8=\r\n" ), 0, 0, &Message ) || !xrtMailMessageTransfer(&Message, &Transfer) ) { return 1; } pBody = xrtMailMessageBody(&Message, Transfer, 0, &iSize); if ( pBody == NULL ) { return 2; } printf("headers=%zu body=%.*s\n", Message.HeaderCount, (int)iSize, pBody); xrtFree(pBody); return 0; }
Что только что произошло. (1) Ввод — полное сообщение из трёх строк заголовков + пустой строки + base64-тела — MailMessageParse разбирает одним проходом: HeaderCount=2, представление заимствует ввод (ноль копий, ноль выделений). (2) MailMessageTransfer читает единственное объявление кодировки BASE64; MailMessageBody декодирует aGVsbG8= в hello — декодирование по объявлению сообщения, не по догадке. (3) Две строки вывода — минимальный цикл слоя данных: структура (число заголовков) + содержимое (декодированное тело). Продолжить? MailMessageHeader ищет поля, курсор multipart разбирает части, кодированные слова декодируют Subject — каждый шаг явный вызов, без скрытого «разобрать всё автоматически».
Вторая полная программа: курсор multipart
Вторая программа из examples/mail/multipart — обход частей:
#include <stdio.h> #include <xmail.h> /* 逐段读取 multipart,不复制正文。 */ int main(void) { static const char sBody[] = "--b\r\nContent-Type: text/plain\r\n\r\nhello\r\n--b--\r\n"; xmailmultipartcursor Cursor; xmailmultipartview Part; if ( !xrtMailMultipartCursorInit( &Cursor, XRT_STR_LITERAL(sBody), XRT_STR_LITERAL("b"), 0 ) || (xrtMailMultipartNext(&Cursor, &Part) != XMAIL_NEXT_ITEM) ) { return 1; } printf("body=%.*s\n", (int)Part.Body.Size, Part.Body.Data); return 0; }
Что только что произошло. (1) Ввод --b\r\nContent-Type: text/plain\r\n\r\nhello\r\n--b--\r\n: граница b, одна часть (заголовок + тело), закрывающая линия-разделитель. (2) MultipartCursorInit привязывает источник и границу (нулевой бюджет берёт потолок частей по умолчанию) → MultipartNext выдаёт первую часть — Part.Body заимствует hello исходного ввода. (3) Форма цикла полностью изоморфна курсорам DER/HTTP-кадров/query-строк (Init→Next→ITEM/END) — N-я встреча паттерна «итерация по смещению, заимствованные представления». Настоящее дерево частей = внешнее представление сообщения + курсор multipart + рекурсия по частям (multipart внутри multipart — форма вложенного в письмо вложения) — глубина рекурсии есть ваш явный цикл, а не автоматическое дерево библиотеки.
118.4契约
- Строгий CRLF: разбор требует строгого CRLF;
CrlfWriteдвухчастная форма (перекрытие отвергается/атомарность ёмкости); явная длина сохраняет ноль байтов. - Явные бюджеты: байты заголовков/число заголовков/число частей по умолчанию ограничены;
SIZE_MAX— явная отмена, не бесконечное умолчание. - Представления заимствуют: представления сообщения/частей/кодированных слов заимствуют ввод — ограничение жизни ввода (общее правило главы 15).
- Дубликаты сохраняются: поиск по порядку без слияния — легально дублируемые поля вроде Received остаются видимыми.
- Транспортная кодировка единственна: отсутствует = 7BIT; дубликат/неизвестная = ошибка (без догадок).
- multipart строго: полное совпадение в начале физической строки; голый перевод/нелегальные поля/нет закрывающей линии/превышение бюджета отвергаются; тела частей не декодируются.
- Потоковая сборка: три метки дают фрагменты-разделители — поля и тело пишутся прямо в сетевой поток, без склейки целого сообщения.
- Три слоя кодированных слов: Parse (границы без выделений + гнездо кодировок) / Encode (ввод UTF-8, разбиение по скалярным границам, ≤75Б) / Decode (строго отвергает инъекционные байты; RELAXED сохраняет без догадок).
- Большие кодировки не встраиваются: GB18030/Big5/Shift_JIS подключают конвертеры приложения на слое представлений — без багажа таблиц.
- QP/Base64: MIME-форма ширины строк; группы по 57 байтов зовут ядро напрямую; строгое декодирование (с поддержкой на месте); ошибки распространяются как есть.
- Предикат границ:
BoundaryValidчистый синтаксис; случайная генерация — отдельный модуль.
Типичные ошибки
Симптом: декодированное тело части изредка портится или падает — буфер сообщения был локальной переменной, а представление из него ушло.
Причина: все представления заимствуют ввод (третье напоминание в этой главе) — жизнь сообщения диктует жизнь представления. Чтобы сохранить надолго, копируйте (выделяющий вход MailMessageBody — уже копирующая форма).
bytes load_and_parse(cstr sRaw) { xmailmessageview V; xrtMailMessageParse(sRaw, 0, 0, &V); return V 的某个视图; /* сообщение на стеке: висячее при возврате */ }
/* кто держит сообщение, держит и представление - связать оба в одной структуре */ typedef struct { str sRaw; xmailmessageview View; } loadedmail; bool load(cstr sPath, loadedmail* pOut) { pOut->sRaw = read_file(sPath); return xrtMailMessageParse(pOut->sRaw, 0, 0, &pOut->View); }
Симптом: локальные тесты проходят; реальные серверы отвергают или поле заголовка поглощается телом — строгие серверы считают голый LF нелегальным.
Причина: почтовая линия — железный закон CRLF; ваш "\n" «случайно» проходит при снисходительном локальном разборе. Перед отправкой нормализуйте через MailCrlfWrite.
snprintf(Buf, "Subject: hi\n\nbody\n"); /* голый LF */ send(Buf);
/* нормализовать после генерации (или писать всё сразу через \r\n) */ xrtMailCrlfWrite(Normalized, sizeof(Normalized), Raw, RawSize, &n); send(Normalized, n);
Симптом: содержимое вложения случайно содержит строку --boundary — части «разрезаются», разбор превращается в кашу.
Причина: уникальность границы — ответственность composing-стороны (лимит 70 байтов в RFC существует, чтобы случайность не сталкивалась). Фиксированная короткая граница (как b образца) — только для тестов; продакшн берёт случайную генерацию mail_id + проверку BoundaryValid.
#define BOUNDARY "b" /* фиксированное короткое значение: любое "--b" во вложении сталкивается */char Boundary[XMAIL_BOUNDARY_MAX + 1]; mail_boundary_random(Boundary); /* случайная генерация mail_id */ assert(xrtMailBoundaryValid(view(Boundary)));
Практические задания
Постройте сообщения во всех четырёх транспортных кодировках (7bit/8bit/QP/Base64); разберите и декодируйте каждую, затем сравните. Критерии приёмки: все четыре тела совпадают при круговом прогоне; дублирующееся поле кодировки отвергается.
Сообщение multipart из text/plain + text/html: внешнее представление → курсор multipart → поле типа (mail_param) и тело каждой части декодируются отдельно. Критерии приёмки: типы и тела обеих частей верны; вариант без закрывающего разделителя отвергается.
Письмо с китайским Subject + именем отправителя: WordEncode (ввод UTF-8) → сборка → разбор → WordDecode туда-обратно. Многословный длинный Subject (разбиение за 75 байтов). Критерии приёмки: круг байт-в-байт; точки разбиения на границах UTF-8-скаляров (без разрезанных символов); RELAXED сохраняет слова с неизвестными кодировками дословно.
Итоги
| Тема | Кратко |
|---|---|
| Проектная позиция | ноль деревьев, ноль копий, ноль builder — заимствованные представления, деревья собирайте сами, исходное сообщение всегда достижимо |
| Ядро | домен ошибок xrt.mail / нормализация CRLF (сохраняет ноль байтов) / чисто синтаксический предикат границ |
| Представление сообщения | строгий CRLF + разделение пустой строкой; заголовки ищутся по порядку, не сливаются; бюджеты явные |
| Транспортная кодировка | единственное объявление; отсутствует = 7BIT; QP/Base64 переиспользуют codec |
| multipart | курсор заимствует представления; полное совпадение в начале физической строки; три метки потоковой сборки |
| Кодированные слова | Parse (гнездо кодировок) / Encode (скалярные границы, ≤75Б) / Decode (отвергает инъекции) |
| Большие кодировки | слой представлений подключает конвертеры приложения — таблицы не входят в сборку по умолчанию |
| QP/Base64 | MIME-ширина строк; группы по 57 байтов зовут ядро; строгое декодирование с поддержкой на месте |
| Распространение ошибок | нижележащие ошибки Base64/сети как есть — самая конкретная причина доходит до вызывающего |
| Рекурсивная структура | глубина дерева частей — явный цикл, а не автоматическое дерево |
📖 Справочники API в этой главе:xmail-mail (3 записей)xmail-mail_message (6 записей)xmail-mail_multipart (3 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки