Ориентация главы
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
Бюджеты 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 — офлайн-цикл ответов и команд:
#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; }
Что только что произошло. (1) Программа разбирает фиксированный tagged OK ответ через xrtImapResponseParse, затем строит SELECT с явным тегом и аргументом ящика через xrtImapCommandWrite, печатая статус и байты команды. (2) Аргумент ящика уже содержит двойные кавычки; построитель проверяет синтаксис линии и границы инъекций. Вызывающий код выбирает quoted string или literal по типу аргумента. Остальные границы ответов, literal и экранирования проверяются тестами того же набора; чтение тела остаётся задачей клиентской машины состояний.
Вторая полная программа: запрос ящика только для чтения
Вторая программа из 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; }
Что только что произошло. (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 идемпотентен с сохранённой диагностикой.
Типичные ошибки
Симптом: при 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(); } }
Симптом: мёртвое ожидание 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); } /* втиснутые события обрабатываются немедленно */ }
Симптом: сохранённые представления ответов/фрагменты читаются как мусор — линейный буфер перезаписан следующим чтением.
Причина: точек инвалидации три (Receive/Next/ReadLiteral) — тоньше, чем «следующее чтение» POP3: чтение literal тоже двигает буфер. Хотите сохранить между вызовами — копируйте.
Subject = Event.ResponseView; /* сохранено представление */ Receive(&Event2); /* буфер уже другой */ use(Subject); /* мусор */
Subject = copy_view(Event.ResponseView); /* копировать до пересечения вызовов */ Receive(&Event2); use(Subject);
Практические задания
Постройте образцы tagged/untagged/continuation и синхронных/LITERAL+/binary literal и прогоните примитивы. Критерии приёмки: пять состояний распознаются верно; длины и маркеры трёх форм literal разбираются согласованно.
SELECT берёт сводку (бит Present проверен) → вход в IDLE → имитация пуша EXISTS → MailboxInfoUpdate вливает → Done выходит. Критерии приёмки: EXISTS/RECENT сводки обновляются с событиями; обработка untagged во время IDLE разделяет состояния верно.
Конвейер явных 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 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки