Ориентация главы

Серия 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).

Представление сообщения: разбор и декодирование тела

1РазборMailMessageParse(сообщение, бюджет байтов заголовков, бюджет числа заголовков, &представление)
2ПоляMailMessageHeader(имя, номер) - ASCII без учёта регистра; дубликаты полей сохраняются, не сливаются (Received дублируется легально)
3Транспортная кодировкаMailMessageTransfer - единственный Content-Transfer-Encoding;
4ТелоMailMessageBody(Write) - 7bit/8bit/binary как есть; QP/Base64 декодируются (переиспользование mail_codec)

Бюджеты по умолчанию (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 — минимальный цикл чтения письма:

extlibs/xmail/examples/mail/message/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXMAIL_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xmail.h impl.c extlibs/xmail/examples/mail/message/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapiheaders=2 body=hello

Что только что произошло. (1) Ввод — полное сообщение из трёх строк заголовков + пустой строки + base64-тела — MailMessageParse разбирает одним проходом: HeaderCount=2, представление заимствует ввод (ноль копий, ноль выделений). (2) MailMessageTransfer читает единственное объявление кодировки BASE64; MailMessageBody декодирует aGVsbG8= в hello — декодирование по объявлению сообщения, не по догадке. (3) Две строки вывода — минимальный цикл слоя данных: структура (число заголовков) + содержимое (декодированное тело). Продолжить? MailMessageHeader ищет поля, курсор multipart разбирает части, кодированные слова декодируют Subject — каждый шаг явный вызов, без скрытого «разобрать всё автоматически».

Вторая полная программа: курсор multipart

Вторая программа из examples/mail/multipart — обход частей:

extlibs/xmail/examples/mail/multipart/main.c源码 ↗
#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;
}
$ gcc -O1 -DXRT_MODULE_ALL -DXMAIL_MODULE_ALL -I single -I single/extlibs -include xrt.h -include xmail.h impl.c extlibs/xmail/examples/mail/multipart/main.c -lws2_32 -lbcrypt -ladvapi32 -liphlpapibody=hello

Что только что произошло. (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 чистый синтаксис; случайная генерация — отдельный модуль.

Типичные ошибки

Ловушка 1: представление вынесено из области жизни ввода:Ловушка 1: представление вынесено из области жизни ввода

Симптом: декодированное тело части изредка портится или падает — буфер сообщения был локальной переменной, а представление из него ушло.

Причина: все представления заимствуют ввод (третье напоминание в этой главе) — жизнь сообщения диктует жизнь представления. Чтобы сохранить надолго, копируйте (выделяющий вход 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);
}
Ловушка 2: сборка письма без CRLF (отправка голого LF):Ловушка 2: сборка письма без CRLF (отправка голого LF)

Симптом: локальные тесты проходят; реальные серверы отвергают или поле заголовка поглощается телом — строгие серверы считают голый 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);
Ловушка 3: граница встретилась внутри тела (коллизия разделителя):Ловушка 3: граница встретилась внутри тела (коллизия разделителя)

Симптом: содержимое вложения случайно содержит строку --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/Base64MIME-ширина строк; группы по 57 байтов зовут ядро; строгое декодирование с поддержкой на месте
Распространение ошибокнижележащие ошибки Base64/сети как есть — самая конкретная причина доходит до вызывающего
Рекурсивная структураглубина дерева частей — явный цикл, а не автоматическое дерево

📖 Справочники API в этой главе:xmail-mail (3 записей)xmail-mail_message (6 записей)xmail-mail_multipart (3 записей)

Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки