Ориентация главы
POP3 — простейший из трёх протоколов приёма (POP3/IMAP/глава 119): загрузочная модель — подключиться, аутентифицироваться, перечислить письма, забрать по одному, (опционально) удалить, попрощаться. Просто не значит небрежно: dot transparency многострочных ответов (экранирование точек в начале строк тела и завершение одиночной точкой у RETR/TOP — симметричная противоположность DATA SMTP); потоковый приём (Begin/Next выдаёт построчно, не выделяя целое письмо — каждая строка заимствует внутренний буфер, стабильна лишь до следующего чтения: три выхода — прямая запись в файл / инкрементальный MIME / собственное хранилище); семантика идентичности UIDL (уникальные ID поддерживают клиентскую логику «забирать только новое»); семейство аутентификации SASL (USER/PASS открытым текстом отвергается по умолчанию, SASL PLAIN плюс механизмы OAuth); апгрейд STLS (CAPA объявляет → +OK → апгрейд на месте → повторный CAPA). Расслоение xpop3: протокольные примитивы (офлайн) + клиент (синхронная машина состояний: AUTHORIZATION→TRANSACTION→MULTILINE→UPDATE) + опциональный слой message (ограниченная агрегация).
120.1引入
Разница моделей POP3 и IMAP решает выбор: POP3 предполагает «забрал — значит локально» — сервер есть транзитная точка доставки, клиент — авторитетное хранилище; IMAP (глава 119) предполагает «сервер — авторитет» — почта остаётся на сервере, папки/флаги/поиск серверные. Простота POP3 даёт низкие ресурсы и офлайн-дружественность; богатство IMAP — синхронизацию многих устройств. Сегодня большинство сценариев — «принял и заархивировал» (вытащить в локальную базу) — потоковый приём POP3 подходит точно.
Построчное заимствование Begin/Next заслуживает внимания: RETR письма на 20 МБ — в любой момент в памяти только текущая строка; представление строки, которое выдаёт Next, стабильно лишь до следующего чтения линии (приход следующей строки затирает её). Это радикальнее «буферизации целого письма» — «принять почту» превращается в форму «читать файл» (изоморфизм IO-потоков главы 41).
120.2概念
Протокольные примитивы: ответы, факты и команды
- Ответы:
xrtPop3ReplyParseразличает+OK/-ERRи заимствует текст статуса;StatParse(64-битные счётчики и байты — большие ящики не переполняются),ListParse/UidlParse(многострочные элементы; UIDL не копируется и не обрезается). - Переиспользование семейства точек: завершение многострочности и дезэкранирование точек напрямую используют
MailLineRead/MailDotLine/MailDotDecodeWriteизmail_net— POP3-специфичной копии не держится (два направления одного семейства примитивов с DATA SMTP). - Возможности:
CapabilityParseвозвращает имя и параметры даже для неизвестных расширений;Capabilityдаёт стабильные биты только распространённым стандартам. - Команды:
CommandWriteкак родовая подстраховка (ёмкость включает завершающий ноль; отвергает управляющие символы/инъекции CR/LF/512B — та же дисциплина, что у командного слоя SMTP).
Клиент: машина состояний и многоуровневые входы
Четыре уровня входов: Send/Line (самый низкий уровень линии — лазейка для SASL-продолжений и неизвестных расширений) → Receive (разбор +OK/-ERR) → Command (сборка + статусный ответ) → Begin/Next (многострочно, построчно). Все стандартные команды построены на публичных входах — STAT/LIST×2/UIDL×2/RETR/TOP/DELE/RSET/NOOP/QUIT суть сборки, а не частные пути: пользовательские расширения стоят на равных с официальными командами.
Потоковый приём и опциональная агрегация
Begin/Next у RETR/TOP: каждая строка дезэкранирована от dot transparency, целое письмо не выделяется, представление строки заимствует внутренний приёмный буфер (стабильно до следующего чтения). Три выхода: писать прямо в файл (глава 45), инкрементальный разбор MIME (модули главы 116 кормятся по шагам), собственное хранилище сообщений. Опциональный слой pop3_message: на той же машине состояний ограниченные RetrWrite/TopWrite (агрегация в owned-байты внутри потолка) плюс входы дерева MIME — берёте только построчный путь, код агрегации не приезжает (свобода обрезки).
Аутентификация: открытый текст отвергается по умолчанию, семейство SASL
pop3_auth: традиционные USER/PASS плюс SASL RFC 5034 (PLAIN и два механизма OAuth). Дисциплина учётных данных: конфигурация только заимствует учётные данные; Base64- и открытотекстовые временные буферы обнуляются до возврата вызова (парольная инженерия главы 77, исполненная на протокольном слое). Передача учётных данных открытым текстом отвергается по умолчанию — AllowPlaintext существует только для явно контролируемых сред совместимости (та же позиция, что базовая линия TLS у Basic главы 106). Отказ сервера в учётных данных оставляет в AUTHORIZATION (можно повторить); ошибка линии вводит FAILED.
TLS: неявный и STLS
Базовый клиент — открытый текст на 110; pop3_client_tls добавляет две формы: неявный TLS (рукопожатие при подключении); STLS (CAPA явно объявляет → отправить STLS → только после полного потребления +OK исполняется рукопожатие → повторный CAPA после рукопожатия — возможности до апгрейда не утекают в защищённую сессию; та же логика, что повторный EHLO у STARTTLS SMTP). Verifier обязателен.
120.3示例
Выполняйте команды из корня репозитория. Сборщик выбирает модули, публичные заголовки и системные библиотеки по манифесту, а также печатает журнал сборки и тестов зависимостей. Терминальные блоки показывают вывод примера. Вторая программа запускает сервер loopback внутри процесса и использует фиксированные демонстрационные данные. Параметры реального сервиса, CA и учётных данных времени выполнения описаны в README клиентской библиотеки.
Первая полная программа: протокольные примитивы
Программа ниже из examples/pop3/protocol — офлайн-цикл ответов и команд:
#include <stdio.h> #include <xpop3.h> /* 解析 STAT 并构建 RETR 命令。 */ int main(void) { xpop3stat Stat; char arrCommand[32]; size_t iSize; if ( !xrtPop3StatParse(XRT_STR_LITERAL("+OK 12 4096"), &Stat) || !xrtPop3CommandWrite( XRT_STR_LITERAL("RETR"), XRT_STR_LITERAL("1"), arrCommand, sizeof(arrCommand), &iSize ) ) { return 1; } printf("messages=%llu command=%.*s", (unsigned long long)Stat.Messages, (int)iSize, arrCommand); return 0; }
Что только что произошло. (1) Программа разбирает фиксированный ответ STAT через xrtPop3StatParse, получает число писем, затем строит и печатает RETR через xrtPop3CommandWrite. Числа писем и байтов сохраняют 64-битную семантику; аргументы команды проходят протокольную проверку. (2) Разбор и построение команд не открывают соединение и подходят для собственных клиентов и тестовых серверов. Остальные границы ответов, UIDL и многострочных данных проверяются тестами того же набора; короткая программа показывает два входа.
Вторая полная программа: локальное получение
Вторая программа из extlibs/xpop3/examples/offline/main.c.
#include <xpop3.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; } /* 只供本进程的回环 POP3 演示服务器使用。 */ 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, "pop3.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 ready\r\n", pServer->Deadline) && exampleExpect(pStream, "USER demo\r\n", pServer->Deadline) && exampleSend(pStream, "+OK user\r\n", pServer->Deadline) && exampleExpect(pStream, "PASS demo\r\n", pServer->Deadline) && exampleSend(pStream, "+OK mailbox\r\n", pServer->Deadline) && exampleExpect(pStream, "RETR 1\r\n", pServer->Deadline) && exampleSend(pStream, "+OK message\r\nSubject: offline example\r\n\r\n" "Hello from loopback.\r\n.\r\n", pServer->Deadline) && exampleExpect(pStream, "QUIT\r\n", pServer->Deadline) && exampleSend(pStream, "+OK bye\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; xpop3clientconfig ClientConfig; example_server Server = { 0 }; xnetengine* pEngine = NULL; xnetresolver* pResolver = NULL; xnetlistener* pListener = NULL; xpop3client* pClient = NULL; xthread* pThread = NULL; double Deadline = exampleTimerLimit(INT64_C(10000)); xstrview Line; xmailnext Next; size_t iLines = 0u; bool bContentOk = true; 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; xrtPop3ClientConfigInit(&ClientConfig); ClientConfig.Net.Engine = pEngine; ClientConfig.Net.Resolver = pResolver; ClientConfig.Net.Host = "pop3.example.invalid"; ClientConfig.Net.Port = ExampleAddress.Port; ClientConfig.ReadCapabilities = false; pClient = xrtPop3ClientOpen(&ClientConfig,exampleTimerRemaining(Deadline), NULL); /* 明文凭据仅用于进程内回环演示;真实服务请用 client 范例的 TLS。 */ if((pClient == NULL) || !xrtPop3ClientLogin(pClient, XRT_STR_LITERAL("demo"), XRT_STR_LITERAL("demo"), true,exampleTimerRemaining(Deadline), NULL) || !xrtPop3ClientRetr(pClient, 1u,exampleTimerRemaining(Deadline), NULL)) goto Done; for(;;) { Next = xrtPop3ClientNext(pClient, &Line,exampleTimerRemaining(Deadline), NULL); if(Next != XMAIL_NEXT_ITEM) break; if(iLines == 0u) bContentOk = bContentOk && (Line.Size == strlen("Subject: offline example")) && (memcmp(Line.Data, "Subject: offline example", Line.Size) == 0); else if(iLines == 1u) bContentOk = bContentOk && (Line.Size == 0u); else if(iLines == 2u) bContentOk = bContentOk && (Line.Size == strlen("Hello from loopback.")) && (memcmp(Line.Data, "Hello from loopback.", Line.Size) == 0); else bContentOk = false; iLines++; } if((Next != XMAIL_NEXT_END) || !bContentOk || (iLines != 3u) || !xrtPop3ClientQuit(pClient,exampleTimerRemaining(Deadline), NULL)) goto Done; bOk = true; Done: if((pClient != NULL) && !bOk) (void)xrtPop3ClientAbort(pClient); xrtPop3ClientDestroy(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 POP3 retrieval failed\n", stderr); return 1; } puts("offline POP3 retrieval: 3 message lines"); return 0; }
Что только что произошло. (1) Программа создаёт локальный слушатель и серверный поток, направляя демонстрационное имя на loopback. Стенд не реализует CAPA, поэтому автоматическое чтение возможностей отключено; настройку для реального сервера выбирают по его поведению. (2) Клиент выполняет USER/PASS и RETR, построчно потребляя xrtPop3ClientNext. Программа побайтно проверяет тему, пустую строку и тело; требуются ровно три строки и полный признак завершения многострочного ответа. (3) После QUIT программа ждёт серверный поток и уничтожает соединение, слушатель, резолвер и движок. Строка успеха появляется только после проверки содержимого и завершения работы. Фиксированные открытые учётные данные разрешены лишь для этого loopback-стенда; реальный пример использует TLS и учётные данные времени выполнения. Программа не удаляет почту: получение и удаление на сервере являются отдельными операциями.
120.4契约
- Расслоение протокол/клиент: примитивы от сети не зависят; клиент — сборка машины состояний; стандартные команды построены на публичных входах.
- 64-битные факты: счётчики и байты STAT 64-битные; UIDL не копируется и не обрезается.
- Общее семейство точек: завершение многострочности/дезэкранирование переиспользуют примитивы mail_net — без POP3-специфичной копии.
- Дисциплина инъекций: CommandWrite отвергает управляющие/CR/LF/512B — как SMTP.
- Потоковый приём: RETR/TOP заимствуют построчно (стабильно до следующего чтения); целое письмо не выделяется; три выхода (файл/инкрементальный MIME/своё хранилище).
- Опциональная агрегация: слой message с ограниченными RetrWrite/TopWrite — построчный путь кода агрегации не несёт.
- Обнуление учётных данных: конфигурация заимствуется; временные буферы обнуляются до возврата вызова; открытый текст по умолчанию отвергается.
- Семантика аутентификации: отказ в учётных данных оставляет AUTHORIZATION; ошибка линии — FAILED, не переиспользуется.
- TLS: неявный/STLS; рукопожатие только после полного потребления +OK; повторный CAPA после апгрейда; верификатор обязателен.
- Семантика UPDATE: отметки DELE вступают в силу на UPDATE у Quit — Abort бросает удаления.
- Владение: разделяемые объекты заимствуются, не уничтожаются; из обратных вызовов Worker не звать; один клиент без конкурентности.
Типичные ошибки
Симптом: после приёма обработка кешированных строк — содержимое уже байты следующей строки.
Причина: граница стабильности строки — «следующее чтение линии»; самое агрессивное заимствование клиента POP3. Хотите сохранить между строками — копируйте.
while ( Next(&Line) ) { rows[n++] = Line; /* сохранённые представления: все недействительны при следующем Next */ } process(rows); /* всё — последняя строка */
while ( xrtPop3ClientNext(pClient, &Line) == OK ) { process_now(Line); /* потреблять немедленно */ /* или копировать: append_own_store(Line); */ }
Симптом: AllowPlaintext включили и забыли — записи прослушки интрасети полны учётных данных.
Причина: отказ по умолчанию — защита, а не помеха; правильный путь совместимости со старыми серверами — STLS (апгрейд, коль скоро CAPA объявляет), а не отказ от шифрования.
Config.AllowPlaintext = true; /* удобство: учётные данные открытым текстом */ Auth(pClient, User, Pass);
if ( capa_has_stls() ) { Stls(pClient, ...); /* сперва апгрейд */ } Auth(pClient, User, Pass); /* внутри шифрованного туннеля */
Симптом: после DELE сервер упал / клиент прервался — после перезапуска письмо на месте; пользователь думает «удалилось и вернулось».
Причина: удаление в POP3 двухфазное: DELE лишь помечает, фиксирует UPDATE у Quit — протокольная защита от случайного удаления. Хотите «точно удалено» — завершите Quit; хотите «может быть удалено» (пробная обработка) — Abort откатывает.
Dele(pClient, 1); /* крах/Abort: письмо не удалено - выглядит как баг */
Dele(pClient, 1); /* отметить */ ... xrtPop3ClientQuit(...); /* UPDATE фиксирует - по-настоящему удалено */ /* при сбое обработки Abort: отметки откатываются - страховочная сетка */
Практические задания
Постройте многострочные образцы +OK/-ERR/STAT/List/Uidl и прогоните через примитивы. Критерии приёмки: 64-битные счётчики верны; UIDL сохраняется полной длиной; завершение многострочности распознаётся верно.
Список UIDL сверяется с локально виденным набором (Map главы 18) → RETR только новых UID → запись после обработки → опционально DELE+Quit. Критерии приёмки: повторные прогоны без дублей; стабильность UIDL (одно письмо — один ID); перезапуск после прерывания ничего не теряет и не дублирует.
Потоковый приём POP3 → кормление построчно в представление сообщения / инкрементальный разбор MIME главы 116 → извлечение заголовков и тела → запись в локальное хранилище mbox/JSONL. Письмо 20 МБ. Критерии приёмки: память постоянна весь путь (уровня МБ); после архивации разбор главы 116 восстанавливает целиком; возобновляемый приём (сверка UIDL).
Итоги
| Тема | Кратко |
|---|---|
| Модель | загрузочная: забрал — значит локально; UIDL — идентичность «только новое» |
| Машина состояний | AUTHORIZATION→TRANSACTION→MULTILINE→UPDATE (фиксирует Quit) |
| Ответы | +OK/-ERR два состояния; STAT 64-битный; UIDL не обрезается |
| Семейство точек | симметрично DATA SMTP; переиспользует примитивы mail_net, копии нет |
| Четыре уровня входов | Send/Line→Receive→Command→Begin/Next — стандартные команды на равных |
| Потоковый приём | построчное заимствование стабильно до следующего чтения; ноль выделений целого письма; выходы файл/инкремент/хранилище |
| Опциональная агрегация | слой message с ограниченным RetrWrite — построчный путь ничего лишнего не везёт |
| Учётные данные | открытый текст отвергается по умолчанию; временные буферы обнуляются внутри вызова; три механизма SASL |
| TLS | неявный/STLS; рукопожатие после потребления +OK; повторный CAPA; верификатор обязателен |
| Семантика удаления | DELE помечает, UPDATE у Quit фиксирует — Abort откатывает |
📖 Справочники API в этой главе:xpop3-pop3 (9 записей)xpop3-pop3_client (29 записей)xmail-mail (3 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки