diff --git a/lib/Client/Protocol/Command/AppendCommand.php b/lib/Client/Protocol/Command/AppendCommand.php index e0d5430..392eee3 100644 --- a/lib/Client/Protocol/Command/AppendCommand.php +++ b/lib/Client/Protocol/Command/AppendCommand.php @@ -4,10 +4,10 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\Parser\ResponseCodeParser; use KTXM\ProviderImap\Client\Protocol\StringEncoder; use KTXM\ProviderImap\Client\ImapException; -use KTXM\ProviderImap\Client\CommandFailedException; use KTXM\ProviderImap\Client\Protocol\RequestFrame; use KTXM\ProviderImap\Client\Protocol\Response\ContinuationResponse; use KTXM\ProviderImap\Client\Protocol\Response\TaggedResponse; @@ -81,14 +81,7 @@ final class AppendCommand implements CommandInterface } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new CommandFailedException( - 'APPEND', - $response->status(), - $response->text(), - $this->responseCodeParser->parse($response->text()), - ); - } + CompletionChecker::assertSuccess($this->name(), $response); return $this->parseAppendUid($response->text()); } diff --git a/lib/Client/Protocol/Command/CapabilityCommand.php b/lib/Client/Protocol/Command/CapabilityCommand.php index 14b86a9..7225ae2 100644 --- a/lib/Client/Protocol/Command/CapabilityCommand.php +++ b/lib/Client/Protocol/Command/CapabilityCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Result\CapabilityResult; use KTXM\ProviderImap\Client\ImapException; use KTXM\ProviderImap\Client\Protocol\RequestFrame; @@ -49,9 +50,7 @@ final class CapabilityCommand implements CommandInterface } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('CAPABILITY failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); } } diff --git a/lib/Client/Protocol/Command/CreateCommand.php b/lib/Client/Protocol/Command/CreateCommand.php index 08b4f1f..0ccdedc 100644 --- a/lib/Client/Protocol/Command/CreateCommand.php +++ b/lib/Client/Protocol/Command/CreateCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\StringEncoder; use KTXM\ProviderImap\Client\Result\CommandCompletion; use KTXM\ProviderImap\Client\ImapException; @@ -48,9 +49,7 @@ final class CreateCommand implements CommandInterface foreach ($responses as $response) { if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('CREATE failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); return new CommandCompletion($response->status(), $response->text()); } diff --git a/lib/Client/Protocol/Command/DeleteCommand.php b/lib/Client/Protocol/Command/DeleteCommand.php index 66a669b..ec8fbc4 100644 --- a/lib/Client/Protocol/Command/DeleteCommand.php +++ b/lib/Client/Protocol/Command/DeleteCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\StringEncoder; use KTXM\ProviderImap\Client\Result\CommandCompletion; use KTXM\ProviderImap\Client\ImapException; @@ -51,9 +52,7 @@ final class DeleteCommand implements CommandInterface foreach ($responses as $response) { if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('DELETE failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); return new CommandCompletion($response->status(), $response->text()); } diff --git a/lib/Client/Protocol/Command/ExpungeCommand.php b/lib/Client/Protocol/Command/ExpungeCommand.php index 6e67a43..85de2e4 100644 --- a/lib/Client/Protocol/Command/ExpungeCommand.php +++ b/lib/Client/Protocol/Command/ExpungeCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\Command\Argument\MessageTarget; use KTXM\ProviderImap\Client\Protocol\IdentifierMode; use KTXM\ProviderImap\Client\ImapException; @@ -89,11 +90,7 @@ final class ExpungeCommand implements CommandInterface } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException($this->sequenceSet === null - ? 'EXPUNGE failed: ' . $response->text() - : 'UID EXPUNGE failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->sequenceSet === null ? 'EXPUNGE' : 'UID EXPUNGE', $response); return $expunged; } diff --git a/lib/Client/Protocol/Command/ListCommand.php b/lib/Client/Protocol/Command/ListCommand.php index 29e570b..95a0ca4 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\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\StringEncoder; use KTXM\ProviderImap\Client\Protocol\Parser\ListResponseParser; use KTXM\ProviderImap\Client\Protocol\Parser\StatusResponseParser; @@ -91,9 +92,7 @@ final class ListCommand implements CommandInterface } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('LIST failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); return; } @@ -127,9 +126,7 @@ final class ListCommand implements CommandInterface } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('LIST failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); foreach ($mailboxes as $mailbox) { yield $mailbox; diff --git a/lib/Client/Protocol/Command/LoginCommand.php b/lib/Client/Protocol/Command/LoginCommand.php index 610f6ec..44f9d1c 100644 --- a/lib/Client/Protocol/Command/LoginCommand.php +++ b/lib/Client/Protocol/Command/LoginCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\StringEncoder; use KTXM\ProviderImap\Client\Result\CommandCompletion; use KTXM\ProviderImap\Client\ImapException; @@ -48,9 +49,7 @@ final class LoginCommand implements CommandInterface { foreach ($responses as $response) { if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('LOGIN failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); $context->setState(SessionState::Authenticated); diff --git a/lib/Client/Protocol/Command/LogoutCommand.php b/lib/Client/Protocol/Command/LogoutCommand.php index c3fc6b9..ac1f1d9 100644 --- a/lib/Client/Protocol/Command/LogoutCommand.php +++ b/lib/Client/Protocol/Command/LogoutCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Result\CommandCompletion; use KTXM\ProviderImap\Client\ImapException; use KTXM\ProviderImap\Client\Protocol\RequestFrame; @@ -42,9 +43,7 @@ final class LogoutCommand implements CommandInterface { foreach ($responses as $response) { if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('LOGOUT failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); $context->setSelectedMailbox(null); $context->setState(SessionState::Logout); diff --git a/lib/Client/Protocol/Command/MessageTransferCommand.php b/lib/Client/Protocol/Command/MessageTransferCommand.php index eeb6735..b489df1 100644 --- a/lib/Client/Protocol/Command/MessageTransferCommand.php +++ b/lib/Client/Protocol/Command/MessageTransferCommand.php @@ -4,13 +4,13 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\Parser\ResponseCodeParser; use KTXM\ProviderImap\Client\Protocol\StringEncoder; use KTXM\ProviderImap\Client\Result\MessageTransferResult; use KTXM\ProviderImap\Client\Protocol\Command\Argument\MessageTarget; use KTXM\ProviderImap\Client\Protocol\IdentifierMode; use KTXM\ProviderImap\Client\ImapException; -use KTXM\ProviderImap\Client\CommandFailedException; use KTXM\ProviderImap\Client\Protocol\RequestFrame; use KTXM\ProviderImap\Client\Protocol\Response\TaggedResponse; use KTXM\ProviderImap\Client\Protocol\Response\UntaggedResponse; @@ -114,14 +114,7 @@ final class MessageTransferCommand implements CommandInterface $highestModSeq, ); - if (!$response->isOk()) { - throw new CommandFailedException( - $this->operation, - $response->status(), - $response->text(), - $this->responseCodeParser->parse($response->text()), - ); - } + CompletionChecker::assertSuccess($this->name(), $response); return new MessageTransferResult( $response->status(), diff --git a/lib/Client/Protocol/Command/NoopCommand.php b/lib/Client/Protocol/Command/NoopCommand.php index 3bc06d7..f7cdcf2 100644 --- a/lib/Client/Protocol/Command/NoopCommand.php +++ b/lib/Client/Protocol/Command/NoopCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Result\CommandCompletion; use KTXM\ProviderImap\Client\ImapException; use KTXM\ProviderImap\Client\Protocol\RequestFrame; @@ -44,9 +45,7 @@ final class NoopCommand implements CommandInterface foreach ($responses as $response) { if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('NOOP failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); return new CommandCompletion($response->status(), $response->text()); } diff --git a/lib/Client/Protocol/Command/RenameCommand.php b/lib/Client/Protocol/Command/RenameCommand.php index badd315..1733a48 100644 --- a/lib/Client/Protocol/Command/RenameCommand.php +++ b/lib/Client/Protocol/Command/RenameCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\StringEncoder; use KTXM\ProviderImap\Client\Result\CommandCompletion; use KTXM\ProviderImap\Client\ImapException; @@ -51,9 +52,7 @@ final class RenameCommand implements CommandInterface { foreach ($responses as $response) { if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('RENAME failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); if ($context->selectedMailbox() === $this->fromMailbox) { $context->setSelectedMailbox($this->toMailbox); diff --git a/lib/Client/Protocol/Command/SearchCommand.php b/lib/Client/Protocol/Command/SearchCommand.php index 54f15bf..af6a6a9 100644 --- a/lib/Client/Protocol/Command/SearchCommand.php +++ b/lib/Client/Protocol/Command/SearchCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Result\SearchResult; use KTXM\ProviderImap\Client\Protocol\IdentifierMode; use KTXM\ProviderImap\Client\ImapException; @@ -74,9 +75,7 @@ final class SearchCommand implements CommandInterface } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('SEARCH failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); return new SearchResult($matches, $this->identifierMode); } diff --git a/lib/Client/Protocol/Command/SelectCommand.php b/lib/Client/Protocol/Command/SelectCommand.php index 13f014d..f6d718a 100644 --- a/lib/Client/Protocol/Command/SelectCommand.php +++ b/lib/Client/Protocol/Command/SelectCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\StringEncoder; use KTXM\ProviderImap\Client\ImapException; use KTXM\ProviderImap\Client\Mailbox; @@ -76,9 +77,7 @@ final class SelectCommand implements CommandInterface } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException($this->name() . ' failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); if (str_contains(strtoupper($response->text()), 'READ-ONLY')) { $readOnly = true; diff --git a/lib/Client/Protocol/Command/SortCommand.php b/lib/Client/Protocol/Command/SortCommand.php index 3d66ddd..6333127 100644 --- a/lib/Client/Protocol/Command/SortCommand.php +++ b/lib/Client/Protocol/Command/SortCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Result\SortResult; use KTXM\ProviderImap\Client\Protocol\IdentifierMode; use KTXM\ProviderImap\Client\ImapException; @@ -82,9 +83,7 @@ final class SortCommand implements CommandInterface } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('SORT failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); return new SortResult($matches, $this->identifierMode); } diff --git a/lib/Client/Protocol/Command/StartTlsCommand.php b/lib/Client/Protocol/Command/StartTlsCommand.php index d351d2d..ead9075 100644 --- a/lib/Client/Protocol/Command/StartTlsCommand.php +++ b/lib/Client/Protocol/Command/StartTlsCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Result\CommandCompletion; use KTXM\ProviderImap\Client\ImapException; use KTXM\ProviderImap\Client\Protocol\RequestFrame; @@ -38,9 +39,7 @@ final class StartTlsCommand implements CommandInterface { foreach ($responses as $response) { if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('STARTTLS failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); $context->connection()->upgradeToTls(); $context->replaceCapabilities(); diff --git a/lib/Client/Protocol/Command/StatusCommand.php b/lib/Client/Protocol/Command/StatusCommand.php index 43cef7e..be10e60 100644 --- a/lib/Client/Protocol/Command/StatusCommand.php +++ b/lib/Client/Protocol/Command/StatusCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Protocol\StringEncoder; use KTXM\ProviderImap\Client\Protocol\Parser\StatusResponseParser; use KTXM\ProviderImap\Client\Result\MailboxStatusResult; @@ -71,9 +72,7 @@ final class StatusCommand implements CommandInterface } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('STATUS failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); return new MailboxStatusResult($mailbox, $items); } diff --git a/lib/Client/Protocol/Command/StoreCommand.php b/lib/Client/Protocol/Command/StoreCommand.php index 2157e5a..76acd99 100644 --- a/lib/Client/Protocol/Command/StoreCommand.php +++ b/lib/Client/Protocol/Command/StoreCommand.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace KTXM\ProviderImap\Client\Protocol\Command; +use KTXM\ProviderImap\Client\Protocol\CompletionChecker; use KTXM\ProviderImap\Client\Result\CommandCompletion; use KTXM\ProviderImap\Client\Protocol\Command\Argument\MessageTarget; use KTXM\ProviderImap\Client\Protocol\IdentifierMode; @@ -90,9 +91,7 @@ final class StoreCommand implements CommandInterface foreach ($responses as $response) { if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('STORE failed: ' . $response->text()); - } + CompletionChecker::assertSuccess($this->name(), $response); return new CommandCompletion($response->status(), $response->text()); } diff --git a/lib/Client/Protocol/CompletionChecker.php b/lib/Client/Protocol/CompletionChecker.php new file mode 100644 index 0000000..056469c --- /dev/null +++ b/lib/Client/Protocol/CompletionChecker.php @@ -0,0 +1,31 @@ +isOk()) { + return; + } + + if (!in_array($response->status(), ['NO', 'BAD'], true)) { + throw new ImapException($command . ' received an invalid completion status: ' . $response->status()); + } + + throw new CommandFailedException( + $command, + $response->status(), + $response->text(), + (new ResponseCodeParser())->parse($response->text()), + ); + } +} diff --git a/lib/Client/Protocol/FetchResultReader.php b/lib/Client/Protocol/FetchResultReader.php index 4aca8c5..2326ad8 100644 --- a/lib/Client/Protocol/FetchResultReader.php +++ b/lib/Client/Protocol/FetchResultReader.php @@ -49,9 +49,7 @@ final class FetchResultReader } if ($response instanceof TaggedResponse) { - if (!$response->isOk()) { - throw new ImapException('FETCH failed: ' . $response->text()); - } + CompletionChecker::assertSuccess('FETCH', $response); return; } diff --git a/lib/Client/Protocol/ProtocolReader.php b/lib/Client/Protocol/ProtocolReader.php index ac03d25..e5c820e 100644 --- a/lib/Client/Protocol/ProtocolReader.php +++ b/lib/Client/Protocol/ProtocolReader.php @@ -129,9 +129,12 @@ final class ProtocolReader if (str_starts_with($trimmed, $tag . ' ')) { $parts = preg_split('/\s+/', $trimmed, 3) ?: []; $status = strtoupper($parts[1] ?? ''); - if ($status === 'NO' || $status === 'BAD') { - throw new ImapException(sprintf('FETCH command failed: %s', $trimmed)); - } + CompletionChecker::assertSuccess('FETCH', new TaggedResponse( + $tag, + $status, + $parts[2] ?? '', + $trimmed, + )); return null; // Tagged OK without ever finding a literal → UID not found } @@ -145,9 +148,7 @@ final class ProtocolReader $response = $this->readResponse(); if ($response instanceof TaggedResponse && $response->tag() === $tag) { - if (!$response->isOk()) { - throw new ImapException(sprintf('FETCH failed: %s', $response->text())); - } + CompletionChecker::assertSuccess('FETCH', $response); return $response; } } diff --git a/tests/php/Unit/CommandFailedExceptionTest.php b/tests/php/Unit/CommandFailedExceptionTest.php index 423920f..283cb4b 100644 --- a/tests/php/Unit/CommandFailedExceptionTest.php +++ b/tests/php/Unit/CommandFailedExceptionTest.php @@ -7,6 +7,9 @@ namespace KTXT\ProviderImap\Tests\Unit; use KTXM\ProviderImap\Client\{CommandFailedException, ConnectionConfig, ImapException}; use KTXM\ProviderImap\Client\Protocol\Command\{AppendCommand, CopyCommand, MoveCommand, CommandInterface}; use KTXM\ProviderImap\Client\Protocol\Command\Argument\MessageTarget; +use KTXM\ProviderImap\Client\Protocol\Command\Argument\ListReturnOptions; +use KTXM\ProviderImap\Client\Protocol\Command as Commands; +use KTXM\ProviderImap\Client\Protocol\{CompletionChecker, ProtocolReader}; use KTXM\ProviderImap\Client\Protocol\{ResponseStream, SessionContext}; use KTXM\ProviderImap\Client\Protocol\Response\TaggedResponse; use KTXM\ProviderImap\Client\Transport\ConnectionInterface; @@ -28,12 +31,32 @@ final class CommandFailedExceptionTest extends TestCase 'COPY' => new CopyCommand(MessageTarget::uid(42), 'Archive'), 'MOVE' => new MoveCommand(MessageTarget::uid(42), 'Archive'), 'APPEND' => new AppendCommand('Archive', 'body'), + 'CAPABILITY' => new Commands\CapabilityCommand(), + 'CREATE' => new Commands\CreateCommand('Archive'), + 'DELETE' => new Commands\DeleteCommand('Archive'), + 'RENAME' => new Commands\RenameCommand('INBOX', 'Archive'), + 'LOGIN' => new Commands\LoginCommand('user', 'password'), + 'LOGOUT' => new Commands\LogoutCommand(), + 'NOOP' => new Commands\NoopCommand(), + 'STARTTLS' => new Commands\StartTlsCommand(), + 'SELECT' => new Commands\SelectCommand('INBOX', false), + 'EXAMINE' => new Commands\SelectCommand('INBOX', true), + 'STATUS' => new Commands\StatusCommand('INBOX'), + 'SEARCH' => new Commands\SearchCommand(), + 'SORT' => new Commands\SortCommand(['DATE']), + 'STORE' => new Commands\StoreCommand(MessageTarget::uid(42), ['\\Seen']), + 'EXPUNGE' => new Commands\ExpungeCommand(), + 'UID EXPUNGE' => new Commands\ExpungeCommand(MessageTarget::uid(42)), + 'LIST' => new Commands\ListCommand(), + 'LIST-STATUS' => new Commands\ListCommand(returnOptions: ListReturnOptions::status('MESSAGES')), + 'FETCH' => new Commands\FetchOneCommand(42), + 'FETCH-MANY' => new Commands\FetchManyCommand(), }; } public static function failures(): iterable { - foreach (['COPY', 'MOVE', 'APPEND'] as $command) { + foreach (['COPY', 'MOVE', 'APPEND', 'CAPABILITY', 'CREATE', 'DELETE', 'RENAME', 'LOGIN', 'LOGOUT', 'NOOP', 'STARTTLS', 'SELECT', 'EXAMINE', 'STATUS', 'SEARCH', 'SORT', 'STORE', 'EXPUNGE', 'UID EXPUNGE', 'LIST', 'LIST-STATUS', 'FETCH', 'FETCH-MANY'] as $command) { foreach (['NO', 'BAD'] as $status) { foreach ([ ['[trycreate] Create destination first', ['name' => 'TRYCREATE', 'arguments' => [], 'text' => 'Create destination first']], @@ -41,7 +64,7 @@ final class CommandFailedExceptionTest extends TestCase ['Denied', null], ['[Malformed', null], ] as [$text, $code]) { - yield [$command, $status, $text, $code]; + yield $command . ' ' . $status . ' ' . $text => [$command, $status, $text, $code]; } } } @@ -55,9 +78,13 @@ final class CommandFailedExceptionTest extends TestCase }); try { - $this->command($command)->handle($responses, $this->context()); + $result = $this->command($command)->handle($responses, $this->context()); + if ($result instanceof \Generator) { + iterator_to_array($result); + } $this->fail('Expected server failure'); } catch (ImapException $exception) { + $command = match ($command) { 'LIST-STATUS' => 'LIST', 'FETCH-MANY' => 'FETCH', default => $command }; $this->assertInstanceOf(CommandFailedException::class, $exception); $this->assertSame($command . ' failed: ' . $text, $exception->getMessage()); $this->assertSame($command, $exception->command()); @@ -88,4 +115,32 @@ final class CommandFailedExceptionTest extends TestCase }); $this->assertSame(42, $this->command('APPEND')->handle($responses, $this->context())); } + public function testDownloadFailuresUseStructuredExceptions(): void + { + foreach (['readUntilFetchLiteral', 'readToEnd'] as $method) { + foreach (['NO', 'BAD'] as $status) { + $connection = $this->createStub(ConnectionInterface::class); + $connection->method('readLine')->willReturn("A1 $status [LIMIT 10] Denied\r\n"); + try { + (new ProtocolReader($connection))->$method('A1'); + $this->fail('Expected download failure'); + } catch (CommandFailedException $exception) { + $this->assertSame('FETCH', $exception->command()); + $this->assertSame($status, $exception->status()); + $this->assertSame(['name' => 'LIMIT', 'arguments' => ['10'], 'text' => 'Denied'], $exception->responseCode()); + } + } + } + } + + public function testCheckerAcceptsOkAndRejectsMalformedStatusAsProtocolError(): void + { + CompletionChecker::assertSuccess('NOOP', new TaggedResponse('A1', 'OK', 'Done', 'A1 OK Done')); + try { + CompletionChecker::assertSuccess('NOOP', new TaggedResponse('A1', 'INVALID', 'Bad status', 'A1 INVALID Bad status')); + $this->fail('Expected malformed status error'); + } catch (ImapException $exception) { + $this->assertNotInstanceOf(CommandFailedException::class, $exception); + } + } }