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

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).

Клиент: машина состояний и многоуровневые входы

AUTHORIZATIONаутентификация успешна (USER/PASS или SASL) →TRANSACTION
TRANSACTIONмногострочная команда (RETR/TOP/LIST/UIDL полностью) →MULTILINE
MULTILINEстрока одиночной точки (Next восстанавливает автоматически) →TRANSACTION
TRANSACTIONQuit (фиксирует отметки DELE) →UPDATE
любоеошибка линии (не переиспользуется) →FAILED

Четыре уровня входов: 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 — офлайн-цикл ответов и команд:

extlibs/xpop3/examples/protocol/main.c源码 ↗
#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;
}
$ python tools/build.py --manifest extlibs/xpop3/config/modules.json --suite pop3 --no-single --jobs 4messages=12 command=RETR 1

Что только что произошло. (1) Программа разбирает фиксированный ответ STAT через xrtPop3StatParse, получает число писем, затем строит и печатает RETR через xrtPop3CommandWrite. Числа писем и байтов сохраняют 64-битную семантику; аргументы команды проходят протокольную проверку. (2) Разбор и построение команд не открывают соединение и подходят для собственных клиентов и тестовых серверов. Остальные границы ответов, UIDL и многострочных данных проверяются тестами того же набора; короткая программа показывает два входа.

Вторая полная программа: локальное получение

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

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;
}
$ python tools/build.py --manifest extlibs/xpop3/config/modules.json --suite pop3_offline_example --no-single --jobs 4offline POP3 retrieval: 3 message lines

Что только что произошло. (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 не звать; один клиент без конкурентности.

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

Ловушка 1: сохранение представления строки и продолжение приёма:Ловушка 1: сохранение представления строки и продолжение приёма

Симптом: после приёма обработка кешированных строк — содержимое уже байты следующей строки.

Причина: граница стабильности строки — «следующее чтение линии»; самое агрессивное заимствование клиента POP3. Хотите сохранить между строками — копируйте.

错误写法
while ( Next(&Line) ) {
	rows[n++] = Line;   /* сохранённые представления: все недействительны при следующем Next */
}
process(rows);   /* всё — последняя строка */
正确写法
while ( xrtPop3ClientNext(pClient, &Line) == OK ) {
	process_now(Line);          /* потреблять немедленно */
	/* или копировать: append_own_store(Line); */
}
Ловушка 2: USER/PASS по открытому соединению:Ловушка 2: USER/PASS по открытому соединению

Симптом: AllowPlaintext включили и забыли — записи прослушки интрасети полны учётных данных.

Причина: отказ по умолчанию — защита, а не помеха; правильный путь совместимости со старыми серверами — STLS (апгрейд, коль скоро CAPA объявляет), а не отказ от шифрования.

错误写法
Config.AllowPlaintext = true;   /* удобство: учётные данные открытым текстом */
Auth(pClient, User, Pass);
正确写法
if ( capa_has_stls() ) {
	Stls(pClient, ...);   /* сперва апгрейд */
}
Auth(pClient, User, Pass);   /* внутри шифрованного туннеля */
Ловушка 3: предположение, что DELE удаляет немедленно:Ловушка 3: предположение, что DELE удаляет немедленно

Симптом: после 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 записей)

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