Files
provider_imap/lib/Client/Protocol/ProtocolReader.php
2026-05-08 00:16:43 -04:00

121 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXM\ProviderImap\Client\Protocol;
use KTXM\ProviderImap\Client\ImapException;
use KTXM\ProviderImap\Client\Protocol\Response\ContinuationResponse;
use KTXM\ProviderImap\Client\Protocol\Response\GreetingResponse;
use KTXM\ProviderImap\Client\Protocol\Response\ResponseInterface;
use KTXM\ProviderImap\Client\Protocol\Response\TaggedResponse;
use KTXM\ProviderImap\Client\Protocol\Response\UntaggedResponse;
use KTXM\ProviderImap\Client\Transport\ConnectionInterface;
use Psr\Log\LoggerInterface;
final class ProtocolReader
{
public function __construct(
private readonly ConnectionInterface $connection,
private readonly ?LoggerInterface $logger = null,
) {}
public function readGreeting(): GreetingResponse
{
$raw = $this->trimTrailingLineEnding($this->connection->readLine());
if (!str_starts_with($raw, '* ')) {
throw new ImapException(sprintf('Expected IMAP greeting, got: %s', $raw));
}
$parts = preg_split('/\s+/', substr($raw, 2), 2) ?: [];
$status = strtoupper($parts[0] ?? '');
$text = $parts[1] ?? '';
$this->logger?->debug('IMAP greeting received: {raw}', [
'status' => $status,
'raw' => $raw,
]);
return new GreetingResponse($status, $text, $raw);
}
public function readResponse(): ResponseInterface
{
$raw = $this->readRawResponse();
if ($raw === '') {
throw new ImapException('Received empty IMAP response line.');
}
if (str_starts_with($raw, '* ')) {
$parts = preg_split('/\s+/', substr($raw, 2), 2) ?: [];
$label = strtoupper($parts[0] ?? '');
$this->logger?->debug('IMAP untagged response received: {raw}', [
'label' => $label,
'raw' => $raw,
]);
return new UntaggedResponse(
$label,
$parts[1] ?? '',
$raw,
);
}
if (str_starts_with($raw, '+')) {
$this->logger?->debug('IMAP continuation response received: {raw}', [
'raw' => $raw,
]);
return new ContinuationResponse(ltrim(substr($raw, 1)), $raw);
}
$parts = preg_split('/\s+/', $raw, 3) ?: [];
if (count($parts) < 2) {
throw new ImapException(sprintf('Malformed tagged IMAP response: %s', $raw));
}
$status = strtoupper($parts[1]);
$this->logger?->debug('IMAP tagged response received: {raw}', [
'tag' => $parts[0],
'status' => $status,
'raw' => $raw,
]);
return new TaggedResponse($parts[0], $status, $parts[2] ?? '', $raw);
}
private function readRawResponse(): string
{
$raw = $this->connection->readLine();
while (($literalLength = $this->trailingLiteralLength($raw)) !== null) {
$raw .= $this->connection->readBytes($literalLength);
$raw .= $this->connection->readLine();
}
return $this->trimTrailingLineEnding($raw);
}
private function trailingLiteralLength(string $raw): ?int
{
if (preg_match('/\{(\d+)\}\r?\n$/', $raw, $matches) !== 1) {
return null;
}
return (int) $matches[1];
}
private function trimTrailingLineEnding(string $raw): string
{
if (str_ends_with($raw, "\r\n")) {
return substr($raw, 0, -2);
}
if (str_ends_with($raw, "\n")) {
return substr($raw, 0, -1);
}
return $raw;
}
}