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

IMAP — «серверно-авторитетный» дуал POP3 (глава 118): почта остаётся на сервере, папки/флаги/поиск/частичное FETCH делаются на сервере — краеугольный камень синхронизации многих устройств. Сложность протокола на порядок выше: модель помеченных ответов (каждая команда несёт уникальный tag, ответы сервера связываются по tag — включая конвейеризацию); механизм literal (параметры и ответы с произвольными байтами идут через объявления длины {N} — синхронные literal ждут подтверждения продолжения, LITERAL+ без синхронизации); поток незапомненных событий (EXISTS/RECENT/FETCH-обновления врываются в любой момент). Расслоение ximap: протокольные примитивы (imap: ответы/literal/возможности/команды — объектов ящика не строит и условий поиска не конструирует, неизвестные расширения достижимы как сырой текст) → клиент (imap_client: двухуровневая модель команд — низкоуровневый конвейер с явными tag плюс последовательный слой удобства) → слой команд-удобств (imap_command: безопасная сборка SELECT/LIST/SEARCH/FETCH/IDLE) → специализированные слои (auth/body/message/append/compress).

121.1引入

Три механизма IMAP заслуживают открытия. Первый: потоковые literal. FETCH письма на 20 МБ — ответ несёт {20971520}, за которым сырые байты — клиент обязан читать сегментами по явной длине: ReadLiteral берёт сегменты в буфер вызывающего, до завершения читать последующие ответы нельзя — «размер сообщения не становится фиксированным потолком памяти» есть гарантия протокольного слоя (то же семейство, что построчный поток POP3, но по длине, а не по строкам). Второй: пуш IDLE. Команда IDLE велит серверу активно проталкивать незапомненные события при прибытии новой почты — серверная версия «длинное соединение ждёт новое» (почтовая редакция SSE). Третий: конвейеризация. Низкоуровневый Send берёт явные tag — шлёте несколько команд, затем один цикл Receive потребляет события в порядке прибытия и связывает завершения по tag — примитив конкурентности IMAP (форма пропускной способности переиспользования соединения).

Привычная защита от инъекций на месте: CommandWrite отвергает ошибочные atom/управляющие символы/инъекции линии/превышения; имена ящиков imap_command «валидируются и цитируются по правилам IMAP-строк — дополнительные команды через CRLF не инъектируются».

121.2概念

Модель ответов и literal

1Три класса ответовtagged (завершение команды - связка по tag) / untagged (начинается с * - поток событий врывается) / continuation (начинается с + - строки продолжения literal)
2Пять состояний статусаOK/NO/BAD/PREAUTH/BYE (стабильно распознаются)
3Три формы literalсинхронный {N} (ждёт подтверждения продолжения от клиента) / LITERAL+ {N+} (без синхронизации, слать сразу) / binary ~{N}

Бюджеты literal двусторонние: сторона отправки (APPEND большого письма — композиция главы 120) и сторона приёма (FETCH большого literal). Контракт приёма: на событии HasLiteral сперва проверить Event.Literal.Size — сверх прикладного бюджета немедленный Abort (тело можно не потреблять — защита памяти «сперва меню, потом решать, есть ли»); низкоуровневый потоковый путь без потолка по умолчанию (потолок — приложению); слой удобств imap_message несёт собственный бюджет 64 МиБ (они не разделяются — каждому слою своя политика).

Двухуровневая модель команд

  • Низкий уровень (Send/SendParts/Write/Continue/Receive/ReadLiteral): конвейер с явными tag — несколько команд, события по порядку, связь по tag; SendParts вставляет пробелы между кусками аргументов и пишет прямо в линию (целая временная команда не строится). Представления заимствуют линейный буфер — недействительны после следующего Receive/Next/ReadLiteral (семейная дисциплина представлений строк главы 118; здесь и чтение literal — тоже точка инвалидации).
  • Последовательный слой (Begin/BeginParts/Next): автоматические уникальные tag; Next выдаёт незапомненные события (XMAIL_NEXT_ITEM) и целевое завершение (XMAIL_NEXT_END) раздельными состояниями — единая форма цикла; пока активна одна последовательная команда, следующую начать нельзя.

Слой команд-удобств: сводка SELECT и потоковые результаты

imap_command покрывает распространённые команды, но тяжёлых объектов не строит: Select/Examine дают без-выделительную ximapmailboxinfo (дайджест фактов ящика; бит Present различает «не возвращено» и «значение ноль» — бит присутствия главы 102 в ящичной редакции; ReadOnly берёт режим доступа финального завершения; MailboxInfoUpdate вливает последующие события EXISTS/RECENT в ту же сводку). Управляющие команды (CREATE/DELETE/RENAME/SUBSCRIBE/CHECK/UNSELECT/CLOSE) полностью потребляют синхронные завершения; потоковые команды (LIST/STATUS/SEARCH/FETCH/STORE/COPY/MOVE/EXPUNGE) только безопасно строятся и запускаются — результаты потребляются потоково через Next+ReadLiteral (слой удобств поток не перерезает). IDLE: после входа Done завершает — проталкиваемые события идут тем же циклом Receive.

TLS, сжатие и семантика отказов

TLS: неявный/STARTTLS (рукопожатие только после полного потребления tagged OK; повторный CAPABILITY после апгрейда — снимок открытого текста не наследуется; верификатор обязателен) — то же тройное семейство, что SMTP/POP3. COMPRESS (imap_compress): после аутентификации согласовывается DEFLATE-сжатая линия — оптимизация полосы для высоколатентных каналов. Семантика отказов троичная: линия FAILED (сеть/отмена/тайм-аут/протокольный разлад/провал разбора — «нельзя гарантировать, что следующий байт на границе команды», соединение не переиспользуется); команда NO/BAD (отрицание сервером полной команды — клиент сохраняет восстанавливаемое состояние, возвращает структурированную ошибку — следующая команда продолжается); Abort (стоп без ожидания, повторяем; FAILED и последнее завершение сохраняются до Destroy для диагностики).

121.3示例

Выполняйте команды из корня репозитория. Сборщик выбирает модули, публичные заголовки и системные библиотеки по манифесту, а также печатает журнал сборки и тестов зависимостей. Терминальные блоки показывают вывод примера. Вторая программа запускает сервер loopback внутри процесса и использует фиксированные демонстрационные данные. Параметры реального сервиса, CA и учётных данных времени выполнения описаны в README клиентской библиотеки.

Первая полная программа: протокольные примитивы

Программа ниже из examples/imap/protocol — офлайн-цикл ответов и команд:

extlibs/ximap/examples/protocol/main.c源码 ↗
#include <stdio.h>
#include <ximap.h>



/* 解析服务器响应并构建一条 IMAP 命令。 */
int main(void)
{
	ximapresponseview Response;
	char arrCommand[64];
	size_t iSize;

	if ( !xrtImapResponseParse(
		XRT_STR_LITERAL("A001 OK authenticated"),
		&Response
	) || !xrtImapCommandWrite(
		XRT_STR_LITERAL("A002"),
		XRT_STR_LITERAL("SELECT"),
		XRT_STR_LITERAL("\"INBOX\""),
		0,
		arrCommand,
		sizeof(arrCommand),
		&iSize
	) ) {
		return 1;
	}
	printf("status=%d command=%.*s",
		(int)Response.Status, (int)iSize, arrCommand);
	return 0;
}
$ python tools/build.py --manifest extlibs/ximap/config/modules.json --suite imap --no-single --jobs 4status=1 command=A002 SELECT "INBOX"

Что только что произошло. (1) Программа разбирает фиксированный tagged OK ответ через xrtImapResponseParse, затем строит SELECT с явным тегом и аргументом ящика через xrtImapCommandWrite, печатая статус и байты команды. (2) Аргумент ящика уже содержит двойные кавычки; построитель проверяет синтаксис линии и границы инъекций. Вызывающий код выбирает quoted string или literal по типу аргумента. Остальные границы ответов, literal и экранирования проверяются тестами того же набора; чтение тела остаётся задачей клиентской машины состояний.

Вторая полная программа: запрос ящика только для чтения

Вторая программа из extlibs/ximap/examples/offline/main.c.

extlibs/ximap/examples/offline/main.c源码 ↗
#include <ximap.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;
}

/* 本地演示域名只解析到本进程的回环 IMAP 服务。 */
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, "imap.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;
}

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, "* OK imap.example.invalid ready\r\n",
		pServer->Deadline) &&
		exampleExpect(pStream, "A00000001 CAPABILITY\r\n",
			pServer->Deadline) &&
		exampleSend(pStream,
			"* CAPABILITY IMAP4rev1\r\n"
			"A00000001 OK capability complete\r\n",
			pServer->Deadline) &&
		exampleExpect(pStream,
			"A00000002 LOGIN \"demo\" \"demo\"\r\n",
			pServer->Deadline) &&
		exampleSend(pStream, "A00000002 OK login complete\r\n",
			pServer->Deadline) &&
		exampleExpect(pStream, "A00000003 EXAMINE \"INBOX\"\r\n",
			pServer->Deadline) &&
		exampleSend(pStream,
			"* 2 EXISTS\r\n"
			"* 0 RECENT\r\n"
			"* OK [UIDVALIDITY 42] valid\r\n"
			"A00000003 OK [READ-ONLY] examined\r\n",
			pServer->Deadline) &&
		exampleExpect(pStream, "A00000004 LOGOUT\r\n",
			pServer->Deadline) &&
		exampleSend(pStream,
			"* BYE signing off\r\n"
			"A00000004 OK logout complete\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;
	ximapclientconfig ClientConfig;
	ximapauthconfig Auth;
	ximapmailboxinfo Mailbox;
	example_server Server = { 0 };
	xnetengine* pEngine = NULL;
	xnetresolver* pResolver = NULL;
	xnetlistener* pListener = NULL;
	ximapclient* 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;
	xrtImapClientConfigInit(&ClientConfig);
	ClientConfig.Net.Engine = pEngine;
	ClientConfig.Net.Resolver = pResolver;
	ClientConfig.Net.Host = "imap.example.invalid";
	ClientConfig.Net.Port = ExampleAddress.Port;
	pClient = xrtImapClientOpen(&ClientConfig,exampleTimerRemaining(Deadline), NULL);
	if ( pClient == NULL ) goto Done;
	xrtImapAuthConfigInit(&Auth);
	Auth.Method = XIMAP_AUTH_LOGIN;
	Auth.Username = XRT_STR_LITERAL("demo");
	Auth.Secret = XRT_STR_LITERAL("demo");
	/* 明文凭据仅供进程内回环演示;真实服务使用 TLS 范例。 */
	Auth.AllowPlaintext = true;
	xrtImapMailboxInfoInit(&Mailbox);
	if ( !xrtImapClientAuth(pClient, &Auth,exampleTimerRemaining(Deadline), NULL) ||
		!xrtImapClientExamine(pClient, XRT_STR_LITERAL("INBOX"),
			&Mailbox,exampleTimerRemaining(Deadline), NULL) ||
		(Mailbox.Exists != 2u) || (Mailbox.Recent != 0u) ||
		(Mailbox.UidValidity != 42u) || !Mailbox.ReadOnly ||
		(xrtImapClientState(pClient) != XIMAP_CLIENT_SELECTED) ||
		!xrtImapClientLogout(pClient,exampleTimerRemaining(Deadline), NULL) ) goto Done;
	bOk = true;

Done:
	if ( (pClient != NULL) && !bOk ) (void)xrtImapClientAbort(pClient);
	xrtImapClientDestroy(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 IMAP EXAMINE failed\n", stderr);
		return 1;
	}
	puts("offline IMAP EXAMINE: INBOX has 2 messages (read-only)");
	return 0;
}
$ python tools/build.py --manifest extlibs/ximap/config/modules.json --suite imap_offline_example --no-single --jobs 4offline IMAP EXAMINE: INBOX has 2 messages (read-only)

Что только что произошло. (1) Программа создаёт IMAP-сервер loopback внутри процесса. Клиент читает приветствие и устанавливает последовательную командную сессию. LOGIN использует фиксированную демонстрационную запись; открытая аутентификация разрешена только на локальном стенде. (2) Клиент вызывает xrtImapClientExamine, получает состояние INBOX только для чтения и число двух писем, проверяет выбранное состояние и факты ящика, затем ждёт tagged completion. EXAMINE не изменяет флаги сообщений и показывает короткий запрос без записи. (3) После LOGOUT программа ждёт серверный поток, закрывает и уничтожает слушатель, резолвер и сетевой движок. Успех означает проверку и результата команды, и сценария сервера. Сжатие требует отдельного согласования после аутентификации и декодирования потока; тесты сжатия и настройки реального клиента приведены в README библиотеки.

121.4契约

  • Примитивы без модели: объекты ящика не строятся / модели поиска/FETCH не конструируются — неизвестные расширения достижимы сырым текстом.
  • Три класса, пять состояний: tagged/untagged/continuation; OK/NO/BAD/PREAUTH/BYE стабильно распознаются; все представления заимствованы.
  • Контракт literal: разбор даёт только длины и маркеры; данные читаются сегментами; до завершения последующие ответы читать нельзя — размер сообщения ≠ потолок памяти.
  • Бюджеты literal: на низком уровне потолка по умолчанию нет (приложение само проверяет Size и при превышении немедленно Abort); слой message — независимый бюджет 64 МиБ.
  • Двухуровневая модель: низкий уровень — конвейер явных tag (SendParts пишет прямо в линию) / последовательный слой — автоматический tag + раздельный по состояниям Next; последовательные команды не конкурентны.
  • Точки инвалидации представлений: любое из Receive/Next/ReadLiteral инвалидирует представления линейного буфера.
  • Слой команд-удобств: без-выделительная сводка SELECT (бит Present/слияние событий); управляющие команды завершаются синхронно; потоковые команды поток не перерезают.
  • Защита от инъекций: инъекции команд/имён ящиков отвергаются; управляющие данные идут literal.
  • TLS: неявный/STARTTLS; повторный CAPABILITY после апгрейда; верификатор обязателен.
  • Сжатие: COMPRESS после аутентификации; конфигурация сперва Valid.
  • Отказы троичны: линия FAILED не переиспользуется / команда NO-BAD восстанавливаема / Abort идемпотентен с сохранённой диагностикой.

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

Ловушка 1: чтение literal раньше проверки размера:Ловушка 1: чтение literal раньше проверки размера

Симптом: при HasLiteral сразу начинается ReadLiteral — следующее письмо 500 МБ (исторический архив), на середине взрывается память или бюджет.

Причина: контракт говорит прямо «сперва проверить Event.Literal.Size» — размер в событии, чтение после решения. Потолка по умолчанию на низком уровне нет = потолок — ваша ответственность.

错误写法
if ( Event.HasLiteral ) {
	while ( ReadLiteral(...) ) { consume(); }   /* размер не проверен: 500МБ вливается напрямую */
}
正确写法
if ( Event.HasLiteral ) {
	if ( Event.Literal.Size > MyBudget ) {
		xrtImapClientAbort(pClient);   /* сверх бюджета: немедленный стоп, тело не есть */
		return;
	}
	while ( ReadLiteral(...) ) { consume(); }
}
Ловушка 2: ожидание завершений конвейера в порядке отправки:Ловушка 2: ожидание завершений конвейера в порядке отправки

Симптом: мёртвое ожидание tagged-ответа команды A — сервер фактически ответил первым B (или втиснул untagged-события).

Причина: ответы IMAP приходят в порядке прибытия сервером, порядок отправки не обещан; untagged-события врываются в любой момент — связывайте по tag, потребляйте по прибытию, не ждите в порядке отправки.

错误写法
send(A); send(B);
wait_tagged(A);   /* сервер может ответить B первым: зависание */
wait_tagged(B);
正确写法
send(A); send(B);
while ( Receive(&Event) ) {
	if ( Event.kind == TAGGED ) { dispatch_by_tag(Event.Tag); }
	else { handle_untagged(Event); }   /* втиснутые события обрабатываются немедленно */
}
Ловушка 3: представления пересекают следующий Receive:Ловушка 3: представления пересекают следующий Receive

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

Причина: точек инвалидации три (Receive/Next/ReadLiteral) — тоньше, чем «следующее чтение» POP3: чтение literal тоже двигает буфер. Хотите сохранить между вызовами — копируйте.

错误写法
Subject = Event.ResponseView;   /* сохранено представление */
Receive(&Event2);               /* буфер уже другой */
use(Subject);                   /* мусор */
正确写法
Subject = copy_view(Event.ResponseView);   /* копировать до пересечения вызовов */
Receive(&Event2);
use(Subject);

Практические задания

Базовое: матрица разбора ответов и literal

Постройте образцы tagged/untagged/continuation и синхронных/LITERAL+/binary literal и прогоните примитивы. Критерии приёмки: пять состояний распознаются верно; длины и маркеры трёх форм literal разбираются согласованно.

Продвинутое: SELECT и слежение за новой почтой

SELECT берёт сводку (бит Present проверен) → вход в IDLE → имитация пуша EXISTS → MailboxInfoUpdate вливает → Done выходит. Критерии приёмки: EXISTS/RECENT сводки обновляются с событиями; обработка untagged во время IDLE разделяет состояния верно.

Вызов: конвейерный FETCH-загрузчик

Конвейер явных tag с 4 конкурентными FETCH (разные диапазоны писем) — потребление по прибытию, связка по tag, literal сегментами на диск. Критерии приёмки: неупорядоченные ответы связываются верно; письмо 20 МБ при постоянной памяти; путь превышения бюджета — чистый Abort.

Итоги

ТемаКратко
Модельсервер-авторитет: папки/флаги/поиск/частичный FETCH на сервере
Ответыtagged (связка по tag) / untagged (втиснутые события) / continuation (строки продолжения literal)
literalсинхронный {N} / LITERAL+ {N+} / binary ~{N}; разбор даёт только длины — данные читаются сегментами
Дисциплина бюджетапроверить Size в событии до решения; на низком уровне потолка нет; слой message — независимые 64 МиБ
Двухуровневая модельнизкий уровень — конвейер (явные tag) / последовательный слой (авто tag + раздельный Next)
Инвалидация представленийReceive/Next/ReadLiteral — три точки; между вызовами копировать
Слой командбез-выделительная сводка SELECT (бит Present); потоковые команды поток не режут
IDLEсерверный пуш — Done завершает; события тем же Receive
TLS/сжатиеповторный CAPABILITY после STARTTLS; COMPRESS с конфигурацией сперва Valid
Отказы троичнылиния FAILED не переиспользуется / NO-BAD восстанавливаемо и продолжается / Abort идемпотентен

📖 Справочники API в этой главе:ximap-imap (13 записей)ximap-imap_client (25 записей)ximap-imap_command (23 записей)

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