From cd724df8ec367b1bbee436e18bc9d222eef36544 Mon Sep 17 00:00:00 2001 From: Sebastian Krupinski Date: Fri, 25 Sep 2026 11:45:44 -0400 Subject: [PATCH] refactor: list command parsing Signed-off-by: Sebastian Krupinski --- lib/Client/Protocol/Command/ListCommand.php | 108 +--------------- .../Protocol/Parser/ListResponseParser.php | 116 ++++++++++++++++++ 2 files changed, 121 insertions(+), 103 deletions(-) create mode 100644 lib/Client/Protocol/Parser/ListResponseParser.php diff --git a/lib/Client/Protocol/Command/ListCommand.php b/lib/Client/Protocol/Command/ListCommand.php index 765bea9..1be8801 100644 --- a/lib/Client/Protocol/Command/ListCommand.php +++ b/lib/Client/Protocol/Command/ListCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\Parser\ListResponseParser; use KTXM\ProviderImap\Client\Protocol\Parser\StatusResponseParser; use Generator; use KTXM\ProviderImap\Client\ImapException; @@ -24,6 +25,7 @@ final class ListCommand implements CommandInterface { private readonly ListSelectionOptions $selectionOptions; private readonly ListReturnOptions $returnOptions; + private readonly ListResponseParser $listResponseParser; private readonly StatusResponseParser $statusResponseParser; public function __construct( @@ -34,6 +36,7 @@ final class ListCommand implements CommandInterface ) { $this->selectionOptions = $selectionOptions ?? ListSelectionOptions::none(); $this->returnOptions = $returnOptions ?? ListReturnOptions::none(); + $this->listResponseParser = new ListResponseParser(); $this->statusResponseParser = new StatusResponseParser(); } @@ -82,7 +85,7 @@ final class ListCommand implements CommandInterface if (!$this->returnOptions->hasStatus()) { foreach ($responses as $response) { if ($response instanceof UntaggedResponse && $response->label() === 'LIST') { - yield $this->parseMailbox($response->payload()); + yield $this->listResponseParser->parse($response->payload()); continue; } @@ -103,7 +106,7 @@ final class ListCommand implements CommandInterface foreach ($responses as $response) { if ($response instanceof UntaggedResponse && $response->label() === 'LIST') { - $mailbox = $this->parseMailbox($response->payload()); + $mailbox = $this->listResponseParser->parse($response->payload()); $mailboxes[$mailbox->name()] = $this->applyStatus( $mailbox, $statuses[$mailbox->name()] ?? [], @@ -138,30 +141,6 @@ final class ListCommand implements CommandInterface throw new ImapException('LIST did not receive a tagged completion response.'); } - private function parseMailbox(string $payload): Mailbox - { - $payload = trim($payload); - $offset = 0; - - $attributesToken = $this->readToken($payload, $offset); - $delimiterToken = $this->readToken($payload, $offset); - $nameToken = $this->readToken($payload, $offset); - - if ($attributesToken === null || $delimiterToken === null || $nameToken === null) { - throw new ImapException('Unable to parse LIST response payload: ' . $payload); - } - - $attributeString = trim($attributesToken, '() '); - $attributes = $attributeString === '' || strtoupper($attributeString) === 'NIL' - ? [] - : array_map('strtoupper', preg_split('/\s+/', $attributeString) ?: []); - - $delimiter = $this->decodeAtom($delimiterToken); - $name = $this->decodeMailboxName($nameToken); - - return new Mailbox($name, $delimiter, $attributes); - } - /** * @param array $status */ @@ -180,83 +159,6 @@ final class ListCommand implements CommandInterface ); } - private function readToken(string $payload, int &$offset): ?string - { - $length = strlen($payload); - - while ($offset < $length && ctype_space($payload[$offset])) { - $offset++; - } - - if ($offset >= $length) { - return null; - } - - if ($payload[$offset] === '(') { - $end = strpos($payload, ')', $offset); - - if ($end === false) { - throw new ImapException('Unterminated LIST attribute block: ' . $payload); - } - - $token = substr($payload, $offset, $end - $offset + 1); - $offset = $end + 1; - - return $token; - } - - if ($payload[$offset] === '"') { - $start = $offset; - $offset++; - - while ($offset < $length) { - if ($payload[$offset] === '\\') { - $offset += 2; - continue; - } - - if ($payload[$offset] === '"') { - $offset++; - return substr($payload, $start, $offset - $start); - } - - $offset++; - } - - throw new ImapException('Unterminated quoted LIST token: ' . $payload); - } - - $start = $offset; - while ($offset < $length && !ctype_space($payload[$offset])) { - $offset++; - } - - return substr($payload, $start, $offset - $start); - } - - private function decodeAtom(string $value): ?string - { - $value = trim($value); - - if (strtoupper($value) === 'NIL') { - return null; - } - - if (str_starts_with($value, '"') && str_ends_with($value, '"')) { - return stripcslashes(substr($value, 1, -1)); - } - - return $value; - } - - private function decodeMailboxName(string $value): string - { - $name = $this->decodeAtom($value); - - // LIST may advertise the root mailbox as an empty quoted string. - return $name ?? ''; - } - private function quote(string $value): string { return '"' . addcslashes($value, "\\\"") . '"'; diff --git a/lib/Client/Protocol/Parser/ListResponseParser.php b/lib/Client/Protocol/Parser/ListResponseParser.php new file mode 100644 index 0000000..22c5636 --- /dev/null +++ b/lib/Client/Protocol/Parser/ListResponseParser.php @@ -0,0 +1,116 @@ +readToken($payload, $offset); + $delimiterToken = $this->readToken($payload, $offset); + $nameToken = $this->readToken($payload, $offset); + + if ($attributesToken === null || $delimiterToken === null || $nameToken === null) { + throw new ImapException('Unable to parse LIST response payload: ' . $payload); + } + + $attributeString = trim($attributesToken, '() '); + $attributes = $attributeString === '' || strtoupper($attributeString) === 'NIL' + ? [] + : array_map('strtoupper', preg_split('/\s+/', $attributeString) ?: []); + + $delimiter = $this->decodeAtom($delimiterToken); + $name = $this->decodeMailboxName($nameToken); + + return new Mailbox($name, $delimiter, $attributes); + } + + private function readToken(string $payload, int &$offset): ?string + { + $length = strlen($payload); + + while ($offset < $length && ctype_space($payload[$offset])) { + $offset++; + } + + if ($offset >= $length) { + return null; + } + + if ($payload[$offset] === '(') { + $end = strpos($payload, ')', $offset); + + if ($end === false) { + throw new ImapException('Unterminated LIST attribute block: ' . $payload); + } + + $token = substr($payload, $offset, $end - $offset + 1); + $offset = $end + 1; + + return $token; + } + + if ($payload[$offset] === '"') { + $start = $offset; + $offset++; + + while ($offset < $length) { + if ($payload[$offset] === '\\') { + $offset += 2; + continue; + } + + if ($payload[$offset] === '"') { + $offset++; + return substr($payload, $start, $offset - $start); + } + + $offset++; + } + + throw new ImapException('Unterminated quoted LIST token: ' . $payload); + } + + $start = $offset; + while ($offset < $length && !ctype_space($payload[$offset])) { + $offset++; + } + + return substr($payload, $start, $offset - $start); + } + + private function decodeAtom(string $value): ?string + { + $value = trim($value); + + if (strtoupper($value) === 'NIL') { + return null; + } + + if (str_starts_with($value, '"') && str_ends_with($value, '"')) { + return stripcslashes(substr($value, 1, -1)); + } + + return $value; + } + + private function decodeMailboxName(string $value): string + { + $name = $this->decodeAtom($value); + + // LIST may advertise the root mailbox as an empty quoted string. + return $name ?? ''; + } + +}