From d615c73c51801cd7950d82729bb69687970a9c00 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 1 Sep 2026 12:45:26 -0400 Subject: [PATCH 1/6] Simplify message sorting API --- src/Enums/SortDirection.php | 9 +++ src/MessageQuery.php | 7 +- src/MessageQueryInterface.php | 56 +++------------- src/QueriesMessages.php | 109 ++++++-------------------------- tests/Unit/MessageQueryTest.php | 51 ++++++++++++--- 5 files changed, 84 insertions(+), 148 deletions(-) create mode 100644 src/Enums/SortDirection.php diff --git a/src/Enums/SortDirection.php b/src/Enums/SortDirection.php new file mode 100644 index 0000000..09a08cc --- /dev/null +++ b/src/Enums/SortDirection.php @@ -0,0 +1,9 @@ +sortKey) { $messages = match ($this->fetchOrder) { - 'asc' => $messages->sort(SORT_NUMERIC), - 'desc' => $messages->sortDesc(SORT_NUMERIC), + SortDirection::Ascending => $messages->sort(SORT_NUMERIC), + SortDirection::Descending => $messages->sortDesc(SORT_NUMERIC), }; } @@ -404,7 +405,7 @@ protected function sort(): Collection $response = $this->connection()->sort( $this->sortKey, - $this->sortDirection, + $this->sortDirection->value, [$this->query->toImap()] ); diff --git a/src/MessageQueryInterface.php b/src/MessageQueryInterface.php index 21ec393..5223224 100644 --- a/src/MessageQueryInterface.php +++ b/src/MessageQueryInterface.php @@ -8,6 +8,7 @@ use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; use DirectoryTree\ImapEngine\Enums\ImapSortKey; +use DirectoryTree\ImapEngine\Enums\SortDirection; use DirectoryTree\ImapEngine\MessageData\FetchItem; use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator; @@ -57,64 +58,27 @@ public function without(FetchItem ...$items): static; public function only(FetchItem ...$items): static; /** - * Set the fetch order. + * Fetch messages in ascending UID order. */ - public function setFetchOrder(string $fetchOrder): MessageQueryInterface; + public function oldest(): static; /** - * Get the fetch order. + * Fetch messages in descending UID order. */ - public function getFetchOrder(): string; - - /** - * Set the fetch order to 'ascending'. - */ - public function setFetchOrderAsc(): MessageQueryInterface; - - /** - * Set the fetch order to 'descending'. - */ - public function setFetchOrderDesc(): MessageQueryInterface; - - /** - * Set the fetch order to show oldest messages first (ascending). - */ - public function oldest(): MessageQueryInterface; - - /** - * Set the fetch order to show newest messages first (descending). - */ - public function newest(): MessageQueryInterface; - - /** - * Set the sort key for server-side sorting (RFC 5256). - */ - public function setSortKey(ImapSortKey|string|null $key): MessageQueryInterface; - - /** - * Get the sort key for server-side sorting. - */ - public function getSortKey(): ?ImapSortKey; - - /** - * Set the sort direction for server-side sorting. - */ - public function setSortDirection(string $direction): MessageQueryInterface; - - /** - * Get the sort direction for server-side sorting. - */ - public function getSortDirection(): string; + public function newest(): static; /** * Sort messages by a field using server-side sorting (RFC 5256). */ - public function sortBy(ImapSortKey|string $key, string $direction = 'asc'): MessageQueryInterface; + public function sortBy( + ImapSortKey|string $key, + SortDirection|string $direction = SortDirection::Ascending, + ): static; /** * Sort messages by a field in descending order using server-side sorting. */ - public function sortByDesc(ImapSortKey|string $key): MessageQueryInterface; + public function sortByDesc(ImapSortKey|string $key): static; /** * Count all available messages matching the current search criteria. diff --git a/src/QueriesMessages.php b/src/QueriesMessages.php index c245a6c..fce3c25 100644 --- a/src/QueriesMessages.php +++ b/src/QueriesMessages.php @@ -4,6 +4,7 @@ use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\Enums\ImapSortKey; +use DirectoryTree\ImapEngine\Enums\SortDirection; use DirectoryTree\ImapEngine\MessageData\FetchItem; use DirectoryTree\ImapEngine\Support\ForwardsCalls; use Illuminate\Support\Traits\Conditionable; @@ -36,10 +37,8 @@ trait QueriesMessages /** * The fetch order. - * - * @var 'asc'|'desc' */ - protected string $fetchOrder = 'desc'; + protected SortDirection $fetchOrder = SortDirection::Descending; /** * The methods that should be returned from query builder. @@ -53,10 +52,8 @@ trait QueriesMessages /** * The sort direction for server-side sorting. - * - * @var 'asc'|'desc' */ - protected string $sortDirection = 'asc'; + protected SortDirection $sortDirection = SortDirection::Ascending; /** * Handle dynamic method calls into the query builder. @@ -156,68 +153,22 @@ public function only(FetchItem ...$items): static return $this->with(...$items); } - /** {@inheritDoc} */ - public function setFetchOrder(string $fetchOrder): MessageQueryInterface - { - $fetchOrder = strtolower($fetchOrder); - - if (in_array($fetchOrder, ['asc', 'desc'])) { - $this->fetchOrder = $fetchOrder; - } - - return $this; - } - - /** - * {@inheritDoc} - */ - public function getFetchOrder(): string - { - return $this->fetchOrder; - } - - /** - * {@inheritDoc} - */ - public function setFetchOrderAsc(): MessageQueryInterface - { - return $this->setFetchOrder('asc'); - } - /** * {@inheritDoc} */ - public function setFetchOrderDesc(): MessageQueryInterface + public function oldest(): static { - return $this->setFetchOrder('desc'); - } - - /** - * {@inheritDoc} - */ - public function oldest(): MessageQueryInterface - { - return $this->setFetchOrder('asc'); - } + $this->fetchOrder = SortDirection::Ascending; - /** - * {@inheritDoc} - */ - public function newest(): MessageQueryInterface - { - return $this->setFetchOrder('desc'); + return $this; } /** * {@inheritDoc} */ - public function setSortKey(ImapSortKey|string|null $key): MessageQueryInterface + public function newest(): static { - if (is_string($key)) { - $key = ImapSortKey::from(strtoupper($key)); - } - - $this->sortKey = $key; + $this->fetchOrder = SortDirection::Descending; return $this; } @@ -225,21 +176,17 @@ public function setSortKey(ImapSortKey|string|null $key): MessageQueryInterface /** * {@inheritDoc} */ - public function getSortKey(): ?ImapSortKey - { - return $this->sortKey; - } + public function sortBy( + ImapSortKey|string $key, + SortDirection|string $direction = SortDirection::Ascending, + ): static { + $this->sortKey = is_string($key) + ? ImapSortKey::from(strtoupper($key)) + : $key; - /** - * {@inheritDoc} - */ - public function setSortDirection(string $direction): MessageQueryInterface - { - $direction = strtolower($direction); - - if (in_array($direction, ['asc', 'desc'])) { - $this->sortDirection = $direction; - } + $this->sortDirection = is_string($direction) + ? SortDirection::from(strtolower($direction)) + : $direction; return $this; } @@ -247,24 +194,8 @@ public function setSortDirection(string $direction): MessageQueryInterface /** * {@inheritDoc} */ - public function getSortDirection(): string - { - return $this->sortDirection; - } - - /** - * {@inheritDoc} - */ - public function sortBy(ImapSortKey|string $key, string $direction = 'asc'): MessageQueryInterface - { - return $this->setSortKey($key)->setSortDirection($direction); - } - - /** - * {@inheritDoc} - */ - public function sortByDesc(ImapSortKey|string $key): MessageQueryInterface + public function sortByDesc(ImapSortKey|string $key): static { - return $this->sortBy($key, 'desc'); + return $this->sortBy($key, SortDirection::Descending); } } diff --git a/tests/Unit/MessageQueryTest.php b/tests/Unit/MessageQueryTest.php index 433ac3d..2f3db0b 100644 --- a/tests/Unit/MessageQueryTest.php +++ b/tests/Unit/MessageQueryTest.php @@ -5,6 +5,7 @@ use DirectoryTree\ImapEngine\Connection\Streams\FakeStream; use DirectoryTree\ImapEngine\Enums\ImapFlag; use DirectoryTree\ImapEngine\Enums\ImapSortKey; +use DirectoryTree\ImapEngine\Enums\SortDirection; use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException; use DirectoryTree\ImapEngine\Folder; use DirectoryTree\ImapEngine\Mailbox; @@ -152,20 +153,46 @@ function query(?Mailbox $mailbox = null): MessageQuery $stream->assertWritten('TAG3 UID EXPUNGE 1:3'); }); -test('oldest sets fetch order to asc', function () { - $query = query(); +test('oldest returns messages in ascending UID order', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 3 1 2', + 'TAG2 OK UID SEARCH completed', + ]); - $query->oldest(); + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); - expect($query->getFetchOrder())->toBe('asc'); + $uids = query($mailbox)->oldest()->get()->map( + fn ($message) => $message->uid() + )->all(); + + expect($uids)->toBe([1, 2, 3]); }); -test('newest sets fetch order to desc', function () { - $query = query(); +test('newest returns messages in descending UID order', function () { + $stream = new FakeStream; + $stream->open(); - $query->newest(); + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 2 3 1', + 'TAG2 OK UID SEARCH completed', + ]); - expect($query->getFetchOrder())->toBe('desc'); + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $uids = query($mailbox)->newest()->get()->map( + fn ($message) => $message->uid() + )->all(); + + expect($uids)->toBe([3, 2, 1]); }); test('oldest and newest return query instance for chaining', function () { @@ -585,6 +612,10 @@ function query(?Mailbox $mailbox = null): MessageQuery query()->sortBy('invalid'); })->throws(ValueError::class); +test('sortBy fails with incorrect string direction', function () { + query()->sortBy('date', 'invalid'); +})->throws(ValueError::class); + test('sortBy sends correct sort command with ascending order', function () { $stream = new FakeStream; $stream->open(); @@ -622,7 +653,7 @@ function query(?Mailbox $mailbox = null): MessageQuery $mailbox = Mailbox::make(); $mailbox->connect(new ImapConnection($stream)); - query($mailbox)->sortBy('date', 'desc')->get(); + query($mailbox)->sortByDesc('date')->get(); $stream->assertWritten('TAG3 UID SORT (REVERSE DATE) UTF-8 ALL'); }); @@ -664,7 +695,7 @@ function query(?Mailbox $mailbox = null): MessageQuery $mailbox = Mailbox::make(); $mailbox->connect(new ImapConnection($stream)); - query($mailbox)->unseen()->sortBy('arrival', 'desc')->get(); + query($mailbox)->unseen()->sortBy('arrival', SortDirection::Descending)->get(); $stream->assertWritten('TAG3 UID SORT (REVERSE ARRIVAL) UTF-8 UNSEEN'); }); From 66911e965dafa4073920cd3141c5616ced76140d Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 1 Sep 2026 15:16:21 -0400 Subject: [PATCH 2/6] Separate UID ordering from server sorting --- src/Connection/ConnectionInterface.php | 6 +- src/Connection/ImapConnection.php | 9 ++- src/MessageQuery.php | 12 ++-- src/MessageQueryInterface.php | 20 +++---- src/QueriesMessages.php | 49 ++++++----------- src/SortCriterion.php | 25 +++++++++ tests/Unit/MessageQueryTest.php | 76 +++++++++++++++++++++++--- 7 files changed, 131 insertions(+), 66 deletions(-) create mode 100644 src/SortCriterion.php diff --git a/src/Connection/ConnectionInterface.php b/src/Connection/ConnectionInterface.php index 2b64114..ee32bec 100644 --- a/src/Connection/ConnectionInterface.php +++ b/src/Connection/ConnectionInterface.php @@ -8,7 +8,7 @@ use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse; use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; -use DirectoryTree\ImapEngine\Enums\ImapSortKey; +use DirectoryTree\ImapEngine\SortCriterion; use Generator; interface ConnectionInterface @@ -119,9 +119,11 @@ public function search(array $params): UntaggedResponse; * * Execute a sort request using RFC 5256. * + * @param array $criteria + * * @see https://datatracker.ietf.org/doc/html/rfc5256 */ - public function sort(ImapSortKey $key, string $direction, array $params): UntaggedResponse; + public function sort(array $criteria, array $params): UntaggedResponse; /** * Send a "FETCH" command. diff --git a/src/Connection/ImapConnection.php b/src/Connection/ImapConnection.php index 381398c..3b5b23c 100644 --- a/src/Connection/ImapConnection.php +++ b/src/Connection/ImapConnection.php @@ -16,13 +16,13 @@ use DirectoryTree\ImapEngine\Connection\Streams\StreamInterface; use DirectoryTree\ImapEngine\Connection\Tokens\Token; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; -use DirectoryTree\ImapEngine\Enums\ImapSortKey; use DirectoryTree\ImapEngine\Exceptions\ImapCommandException; use DirectoryTree\ImapEngine\Exceptions\ImapConnectionClosedException; use DirectoryTree\ImapEngine\Exceptions\ImapConnectionFailedException; use DirectoryTree\ImapEngine\Exceptions\ImapConnectionTimedOutException; use DirectoryTree\ImapEngine\Exceptions\ImapResponseException; use DirectoryTree\ImapEngine\Exceptions\ImapStreamException; +use DirectoryTree\ImapEngine\SortCriterion; use DirectoryTree\ImapEngine\Support\Str; use Exception; use Generator; @@ -509,9 +509,12 @@ public function search(array $params): UntaggedResponse /** * {@inheritDoc} */ - public function sort(ImapSortKey $key, string $direction, array $params): UntaggedResponse + public function sort(array $criteria, array $params): UntaggedResponse { - $sortCriteria = $direction === 'desc' ? "REVERSE {$key->value}" : $key->value; + $sortCriteria = implode(' ', array_map( + fn (SortCriterion $criterion) => $criterion->toImap(), + $criteria, + )); $this->send('UID SORT', ["({$sortCriteria})", 'UTF-8', ...$params], tag: $tag); diff --git a/src/MessageQuery.php b/src/MessageQuery.php index 489d88f..ef916d4 100644 --- a/src/MessageQuery.php +++ b/src/MessageQuery.php @@ -69,7 +69,7 @@ public function firstOrFail(): MessageInterface */ public function get(): MessageCollection { - return $this->process($this->sortKey ? $this->sort() : $this->search()); + return $this->process($this->sortCriteria ? $this->sort() : $this->search()); } /** @@ -338,11 +338,8 @@ protected function populate(Collection $uids): MessageCollection */ protected function fetch(Collection $messages): array { - // Only apply client-side sorting when not using server-side sorting. - // When sortKey is set, the IMAP SORT command already returns UIDs - // in the correct order, so we should preserve that order. - if (! $this->sortKey) { - $messages = match ($this->fetchOrder) { + if ($this->uidOrder) { + $messages = match ($this->uidOrder) { SortDirection::Ascending => $messages->sort(SORT_NUMERIC), SortDirection::Descending => $messages->sortDesc(SORT_NUMERIC), }; @@ -404,8 +401,7 @@ protected function sort(): Collection } $response = $this->connection()->sort( - $this->sortKey, - $this->sortDirection->value, + $this->sortCriteria, [$this->query->toImap()] ); diff --git a/src/MessageQueryInterface.php b/src/MessageQueryInterface.php index 5223224..ccf5cad 100644 --- a/src/MessageQueryInterface.php +++ b/src/MessageQueryInterface.php @@ -58,28 +58,22 @@ public function without(FetchItem ...$items): static; public function only(FetchItem ...$items): static; /** - * Fetch messages in ascending UID order. + * Order messages locally by UID, replacing any server-side sort criteria. */ - public function oldest(): static; - - /** - * Fetch messages in descending UID order. - */ - public function newest(): static; + public function orderByUid( + SortDirection|string $direction = SortDirection::Ascending, + ): static; /** - * Sort messages by a field using server-side sorting (RFC 5256). + * Add a server-side sort criterion using RFC 5256. + * + * Subsequent calls are used as tie-breakers in the order they are added. */ public function sortBy( ImapSortKey|string $key, SortDirection|string $direction = SortDirection::Ascending, ): static; - /** - * Sort messages by a field in descending order using server-side sorting. - */ - public function sortByDesc(ImapSortKey|string $key): static; - /** * Count all available messages matching the current search criteria. */ diff --git a/src/QueriesMessages.php b/src/QueriesMessages.php index fce3c25..8d49ffd 100644 --- a/src/QueriesMessages.php +++ b/src/QueriesMessages.php @@ -36,9 +36,9 @@ trait QueriesMessages protected array $fetchItems = []; /** - * The fetch order. + * The UID order. */ - protected SortDirection $fetchOrder = SortDirection::Descending; + protected ?SortDirection $uidOrder = SortDirection::Descending; /** * The methods that should be returned from query builder. @@ -46,14 +46,11 @@ trait QueriesMessages protected array $passthru = ['toimap', 'isempty']; /** - * The sort key for server-side sorting (RFC 5256). - */ - protected ?ImapSortKey $sortKey = null; - - /** - * The sort direction for server-side sorting. + * The criteria for server-side sorting (RFC 5256). + * + * @var array */ - protected SortDirection $sortDirection = SortDirection::Ascending; + protected array $sortCriteria = []; /** * Handle dynamic method calls into the query builder. @@ -156,19 +153,14 @@ public function only(FetchItem ...$items): static /** * {@inheritDoc} */ - public function oldest(): static - { - $this->fetchOrder = SortDirection::Ascending; + public function orderByUid( + SortDirection|string $direction = SortDirection::Ascending, + ): static { + $this->uidOrder = is_string($direction) + ? SortDirection::from(strtolower($direction)) + : $direction; - return $this; - } - - /** - * {@inheritDoc} - */ - public function newest(): static - { - $this->fetchOrder = SortDirection::Descending; + $this->sortCriteria = []; return $this; } @@ -180,22 +172,17 @@ public function sortBy( ImapSortKey|string $key, SortDirection|string $direction = SortDirection::Ascending, ): static { - $this->sortKey = is_string($key) + $key = is_string($key) ? ImapSortKey::from(strtoupper($key)) : $key; - $this->sortDirection = is_string($direction) + $direction = is_string($direction) ? SortDirection::from(strtolower($direction)) : $direction; - return $this; - } + $this->uidOrder = null; + $this->sortCriteria[] = new SortCriterion($key, $direction); - /** - * {@inheritDoc} - */ - public function sortByDesc(ImapSortKey|string $key): static - { - return $this->sortBy($key, SortDirection::Descending); + return $this; } } diff --git a/src/SortCriterion.php b/src/SortCriterion.php new file mode 100644 index 0000000..10047a9 --- /dev/null +++ b/src/SortCriterion.php @@ -0,0 +1,25 @@ +direction) { + SortDirection::Ascending => $this->key->value, + SortDirection::Descending => "REVERSE {$this->key->value}", + }; + } +} diff --git a/tests/Unit/MessageQueryTest.php b/tests/Unit/MessageQueryTest.php index 2f3db0b..5d0078f 100644 --- a/tests/Unit/MessageQueryTest.php +++ b/tests/Unit/MessageQueryTest.php @@ -153,7 +153,7 @@ function query(?Mailbox $mailbox = null): MessageQuery $stream->assertWritten('TAG3 UID EXPUNGE 1:3'); }); -test('oldest returns messages in ascending UID order', function () { +test('orderByUid returns messages in ascending UID order', function () { $stream = new FakeStream; $stream->open(); @@ -167,14 +167,14 @@ function query(?Mailbox $mailbox = null): MessageQuery $mailbox = Mailbox::make(); $mailbox->connect(new ImapConnection($stream)); - $uids = query($mailbox)->oldest()->get()->map( + $uids = query($mailbox)->orderByUid()->get()->map( fn ($message) => $message->uid() )->all(); expect($uids)->toBe([1, 2, 3]); }); -test('newest returns messages in descending UID order', function () { +test('orderByUid returns messages in descending UID order', function () { $stream = new FakeStream; $stream->open(); @@ -188,20 +188,23 @@ function query(?Mailbox $mailbox = null): MessageQuery $mailbox = Mailbox::make(); $mailbox->connect(new ImapConnection($stream)); - $uids = query($mailbox)->newest()->get()->map( + $uids = query($mailbox)->orderByUid(SortDirection::Descending)->get()->map( fn ($message) => $message->uid() )->all(); expect($uids)->toBe([3, 2, 1]); }); -test('oldest and newest return query instance for chaining', function () { +test('orderByUid returns query instance for chaining', function () { $query = query(); - expect($query->oldest())->toBe($query); - expect($query->newest())->toBe($query); + expect($query->orderByUid())->toBe($query); }); +test('orderByUid fails with incorrect string direction', function () { + query()->orderByUid('invalid'); +})->throws(ValueError::class); + test('each breaks when callback returns false', function () { $stream = new FakeStream; $stream->open(); @@ -632,9 +635,16 @@ function query(?Mailbox $mailbox = null): MessageQuery $mailbox = Mailbox::make(); $mailbox->connect(new ImapConnection($stream)); - query($mailbox)->sortBy('date')->get(); + $uids = query($mailbox) + ->orderByUid(SortDirection::Descending) + ->sortBy('date') + ->get() + ->map(fn ($message) => $message->uid()) + ->all(); $stream->assertWritten('TAG3 UID SORT (DATE) UTF-8 ALL'); + + expect($uids)->toBe([3, 1, 2]); }); test('sortBy sends correct sort command with descending order', function () { @@ -653,11 +663,59 @@ function query(?Mailbox $mailbox = null): MessageQuery $mailbox = Mailbox::make(); $mailbox->connect(new ImapConnection($stream)); - query($mailbox)->sortByDesc('date')->get(); + query($mailbox)->sortBy('date', SortDirection::Descending)->get(); $stream->assertWritten('TAG3 UID SORT (REVERSE DATE) UTF-8 ALL'); }); +test('sortBy sends multiple sort criteria in priority order', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* CAPABILITY IMAP4rev1 SORT', + 'TAG2 OK CAPABILITY completed', + '* SORT 2 1 3', + 'TAG3 OK SORT completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + query($mailbox) + ->sortBy('subject') + ->sortBy('date', SortDirection::Descending) + ->get(); + + $stream->assertWritten('TAG3 UID SORT (SUBJECT REVERSE DATE) UTF-8 ALL'); +}); + +test('orderByUid replaces server sorting', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* SEARCH 3 1 2', + 'TAG2 OK UID SEARCH completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + $uids = query($mailbox) + ->sortBy('date') + ->orderByUid() + ->get() + ->map(fn ($message) => $message->uid()) + ->all(); + + expect($uids)->toBe([1, 2, 3]); +}); + test('sortBy works with ImapSortKey enum', function () { $stream = new FakeStream; $stream->open(); From a59a97e725482ace221f809937d98c8c1fefc010 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 1 Sep 2026 15:33:05 -0400 Subject: [PATCH 3/6] Model message ordering as a single strategy --- src/Connection/ConnectionInterface.php | 6 ++--- src/Connection/ImapConnection.php | 11 +++------ src/ImapSort.php | 34 ++++++++++++++++++++++++++ src/MessageQuery.php | 29 ++++++++++++++++------ src/QueriesMessages.php | 28 +++++++++------------ src/Testing/FakeMessageQuery.php | 6 ++++- src/UidOrder.php | 12 +++++++++ 7 files changed, 89 insertions(+), 37 deletions(-) create mode 100644 src/ImapSort.php create mode 100644 src/UidOrder.php diff --git a/src/Connection/ConnectionInterface.php b/src/Connection/ConnectionInterface.php index ee32bec..2cf5dd4 100644 --- a/src/Connection/ConnectionInterface.php +++ b/src/Connection/ConnectionInterface.php @@ -8,7 +8,7 @@ use DirectoryTree\ImapEngine\Connection\Responses\TaggedResponse; use DirectoryTree\ImapEngine\Connection\Responses\UntaggedResponse; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; -use DirectoryTree\ImapEngine\SortCriterion; +use DirectoryTree\ImapEngine\ImapSort; use Generator; interface ConnectionInterface @@ -119,11 +119,9 @@ public function search(array $params): UntaggedResponse; * * Execute a sort request using RFC 5256. * - * @param array $criteria - * * @see https://datatracker.ietf.org/doc/html/rfc5256 */ - public function sort(array $criteria, array $params): UntaggedResponse; + public function sort(ImapSort $sort, array $params): UntaggedResponse; /** * Send a "FETCH" command. diff --git a/src/Connection/ImapConnection.php b/src/Connection/ImapConnection.php index 3b5b23c..826dc42 100644 --- a/src/Connection/ImapConnection.php +++ b/src/Connection/ImapConnection.php @@ -22,7 +22,7 @@ use DirectoryTree\ImapEngine\Exceptions\ImapConnectionTimedOutException; use DirectoryTree\ImapEngine\Exceptions\ImapResponseException; use DirectoryTree\ImapEngine\Exceptions\ImapStreamException; -use DirectoryTree\ImapEngine\SortCriterion; +use DirectoryTree\ImapEngine\ImapSort; use DirectoryTree\ImapEngine\Support\Str; use Exception; use Generator; @@ -509,14 +509,9 @@ public function search(array $params): UntaggedResponse /** * {@inheritDoc} */ - public function sort(array $criteria, array $params): UntaggedResponse + public function sort(ImapSort $sort, array $params): UntaggedResponse { - $sortCriteria = implode(' ', array_map( - fn (SortCriterion $criterion) => $criterion->toImap(), - $criteria, - )); - - $this->send('UID SORT', ["({$sortCriteria})", 'UTF-8', ...$params], tag: $tag); + $this->send('UID SORT', ["({$sort->toImap()})", 'UTF-8', ...$params], tag: $tag); $this->assertTaggedResponse($tag); diff --git a/src/ImapSort.php b/src/ImapSort.php new file mode 100644 index 0000000..6104957 --- /dev/null +++ b/src/ImapSort.php @@ -0,0 +1,34 @@ + $criteria + */ + public function __construct( + public array $criteria = [], + ) {} + + /** + * Add a sort criterion. + */ + public function add(SortCriterion $criterion): static + { + $this->criteria[] = $criterion; + + return $this; + } + + /** + * Get the IMAP SORT criteria. + */ + public function toImap(): string + { + return implode(' ', array_map( + fn (SortCriterion $criterion) => $criterion->toImap(), + $this->criteria, + )); + } +} diff --git a/src/MessageQuery.php b/src/MessageQuery.php index ef916d4..d504ef5 100644 --- a/src/MessageQuery.php +++ b/src/MessageQuery.php @@ -34,7 +34,9 @@ class MessageQuery implements MessageQueryInterface public function __construct( protected FolderInterface $folder, protected ImapQueryBuilder $query, - ) {} + ) { + $this->ordering = new UidOrder(SortDirection::Descending); + } /** * Count all available messages matching the current search criteria. @@ -69,7 +71,7 @@ public function firstOrFail(): MessageInterface */ public function get(): MessageCollection { - return $this->process($this->sortCriteria ? $this->sort() : $this->search()); + return $this->process($this->orderedUids()); } /** @@ -104,8 +106,8 @@ public function chunk(callable $callback, int $chunkSize = 10, int $startChunk = $startChunk = max($startChunk, 1); $chunkSize = max($chunkSize, 1); - // Get all search result tokens once. - $messages = $this->search(); + // Get all ordered result tokens once. + $messages = $this->orderedUids(); // Calculate how many chunks there are $totalChunks = (int) ceil($messages->count() / $chunkSize); @@ -338,8 +340,8 @@ protected function populate(Collection $uids): MessageCollection */ protected function fetch(Collection $messages): array { - if ($this->uidOrder) { - $messages = match ($this->uidOrder) { + if ($this->ordering instanceof UidOrder) { + $messages = match ($this->ordering->direction) { SortDirection::Ascending => $messages->sort(SORT_NUMERIC), SortDirection::Descending => $messages->sortDesc(SORT_NUMERIC), }; @@ -365,6 +367,17 @@ protected function fetch(Collection $messages): array })->all(); } + /** + * Get the ordered message UIDs. + */ + protected function orderedUids(): Collection + { + return match (true) { + $this->ordering instanceof UidOrder => $this->search(), + $this->ordering instanceof ImapSort => $this->sort($this->ordering), + }; + } + /** * Execute an IMAP search request. */ @@ -388,7 +401,7 @@ protected function search(): Collection /** * Execute an IMAP UID SORT request using RFC 5256. */ - protected function sort(): Collection + protected function sort(ImapSort $sort): Collection { if (! in_array('SORT', $this->folder->mailbox()->capabilities())) { throw new ImapCapabilityException( @@ -401,7 +414,7 @@ protected function sort(): Collection } $response = $this->connection()->sort( - $this->sortCriteria, + $sort, [$this->query->toImap()] ); diff --git a/src/QueriesMessages.php b/src/QueriesMessages.php index 8d49ffd..c8c05d2 100644 --- a/src/QueriesMessages.php +++ b/src/QueriesMessages.php @@ -36,22 +36,15 @@ trait QueriesMessages protected array $fetchItems = []; /** - * The UID order. + * The message ordering strategy. */ - protected ?SortDirection $uidOrder = SortDirection::Descending; + protected UidOrder|ImapSort $ordering; /** * The methods that should be returned from query builder. */ protected array $passthru = ['toimap', 'isempty']; - /** - * The criteria for server-side sorting (RFC 5256). - * - * @var array - */ - protected array $sortCriteria = []; - /** * Handle dynamic method calls into the query builder. */ @@ -156,11 +149,11 @@ public function only(FetchItem ...$items): static public function orderByUid( SortDirection|string $direction = SortDirection::Ascending, ): static { - $this->uidOrder = is_string($direction) - ? SortDirection::from(strtolower($direction)) - : $direction; - - $this->sortCriteria = []; + $this->ordering = new UidOrder( + is_string($direction) + ? SortDirection::from(strtolower($direction)) + : $direction, + ); return $this; } @@ -180,8 +173,11 @@ public function sortBy( ? SortDirection::from(strtolower($direction)) : $direction; - $this->uidOrder = null; - $this->sortCriteria[] = new SortCriterion($key, $direction); + if (! $this->ordering instanceof ImapSort) { + $this->ordering = new ImapSort; + } + + $this->ordering->add(new SortCriterion($key, $direction)); return $this; } diff --git a/src/Testing/FakeMessageQuery.php b/src/Testing/FakeMessageQuery.php index 34e8cc4..c0168a4 100644 --- a/src/Testing/FakeMessageQuery.php +++ b/src/Testing/FakeMessageQuery.php @@ -8,10 +8,12 @@ use DirectoryTree\ImapEngine\Collections\MessageCollection; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; +use DirectoryTree\ImapEngine\Enums\SortDirection; use DirectoryTree\ImapEngine\MessageInterface; use DirectoryTree\ImapEngine\MessageQueryInterface; use DirectoryTree\ImapEngine\Pagination\LengthAwarePaginator; use DirectoryTree\ImapEngine\QueriesMessages; +use DirectoryTree\ImapEngine\UidOrder; class FakeMessageQuery implements MessageQueryInterface { @@ -23,7 +25,9 @@ class FakeMessageQuery implements MessageQueryInterface public function __construct( protected FakeFolder $folder, protected ImapQueryBuilder $query = new ImapQueryBuilder - ) {} + ) { + $this->ordering = new UidOrder(SortDirection::Descending); + } /** * {@inheritDoc} diff --git a/src/UidOrder.php b/src/UidOrder.php new file mode 100644 index 0000000..07f3d1d --- /dev/null +++ b/src/UidOrder.php @@ -0,0 +1,12 @@ + Date: Tue, 1 Sep 2026 15:34:56 -0400 Subject: [PATCH 4/6] Document ordering constructors --- src/ImapSort.php | 2 ++ src/SortCriterion.php | 3 +++ src/UidOrder.php | 3 +++ 3 files changed, 8 insertions(+) diff --git a/src/ImapSort.php b/src/ImapSort.php index 6104957..12d341d 100644 --- a/src/ImapSort.php +++ b/src/ImapSort.php @@ -5,6 +5,8 @@ class ImapSort { /** + * Constructor. + * * @param array $criteria */ public function __construct( diff --git a/src/SortCriterion.php b/src/SortCriterion.php index 10047a9..d0de925 100644 --- a/src/SortCriterion.php +++ b/src/SortCriterion.php @@ -7,6 +7,9 @@ class SortCriterion { + /** + * Constructor. + */ public function __construct( public ImapSortKey $key, public SortDirection $direction = SortDirection::Ascending, diff --git a/src/UidOrder.php b/src/UidOrder.php index 07f3d1d..b27cfa1 100644 --- a/src/UidOrder.php +++ b/src/UidOrder.php @@ -6,6 +6,9 @@ class UidOrder { + /** + * Constructor. + */ public function __construct( public SortDirection $direction, ) {} From 0a794d6d36595a2058c97aeb8ff1a4127bf0aa5b Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 1 Sep 2026 15:52:02 -0400 Subject: [PATCH 5/6] Preserve message sorting semantics --- src/ImapSort.php | 16 +++++-- src/MessageQuery.php | 16 +++++-- src/QueriesMessages.php | 10 ++-- src/Testing/FakeMessageQuery.php | 51 +++++++++++++++++--- tests/Unit/MessageQueryTest.php | 26 +++++++++++ tests/Unit/Testing/FakeMessageQueryTest.php | 52 +++++++++++++++++++-- 6 files changed, 148 insertions(+), 23 deletions(-) diff --git a/src/ImapSort.php b/src/ImapSort.php index 12d341d..a2701ce 100644 --- a/src/ImapSort.php +++ b/src/ImapSort.php @@ -5,13 +5,19 @@ class ImapSort { /** - * Constructor. + * The sort criteria. * - * @param array $criteria + * @var array */ - public function __construct( - public array $criteria = [], - ) {} + public array $criteria; + + /** + * Constructor. + */ + public function __construct(SortCriterion $criterion, SortCriterion ...$criteria) + { + $this->criteria = [$criterion, ...$criteria]; + } /** * Add a sort criterion. diff --git a/src/MessageQuery.php b/src/MessageQuery.php index d504ef5..6e455a9 100644 --- a/src/MessageQuery.php +++ b/src/MessageQuery.php @@ -360,11 +360,17 @@ protected function fetch(Collection $messages): array ])->all(); } - return $this->connection()->fetch($fetch, $uids->all())->mapWithKeys(function (UntaggedResponse $response) { + $fetched = $this->connection()->fetch($fetch, $uids->all())->mapWithKeys(function (UntaggedResponse $response) { $data = FetchedMessageData::fromResponse($response); return [$data->uid() => $data]; - })->all(); + }); + + return $uids + ->map(fn (string|int $uid) => $fetched->get($uid)) + ->filter() + ->mapWithKeys(fn (FetchedMessageData $data) => [$data->uid() => $data]) + ->all(); } /** @@ -403,7 +409,11 @@ protected function search(): Collection */ protected function sort(ImapSort $sort): Collection { - if (! in_array('SORT', $this->folder->mailbox()->capabilities())) { + $supportsSort = collect($this->folder->mailbox()->capabilities())->contains( + fn (string $capability) => str_starts_with(strtoupper($capability), 'SORT') + ); + + if (! $supportsSort) { throw new ImapCapabilityException( 'Unable to sort messages. IMAP server does not support SORT capability.' ); diff --git a/src/QueriesMessages.php b/src/QueriesMessages.php index c8c05d2..15ca026 100644 --- a/src/QueriesMessages.php +++ b/src/QueriesMessages.php @@ -173,11 +173,13 @@ public function sortBy( ? SortDirection::from(strtolower($direction)) : $direction; - if (! $this->ordering instanceof ImapSort) { - $this->ordering = new ImapSort; - } + $criterion = new SortCriterion($key, $direction); - $this->ordering->add(new SortCriterion($key, $direction)); + if ($this->ordering instanceof ImapSort) { + $this->ordering->add($criterion); + } else { + $this->ordering = new ImapSort($criterion); + } return $this; } diff --git a/src/Testing/FakeMessageQuery.php b/src/Testing/FakeMessageQuery.php index c0168a4..8c99c72 100644 --- a/src/Testing/FakeMessageQuery.php +++ b/src/Testing/FakeMessageQuery.php @@ -8,6 +8,7 @@ use DirectoryTree\ImapEngine\Collections\MessageCollection; use DirectoryTree\ImapEngine\Connection\ImapQueryBuilder; use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier; +use DirectoryTree\ImapEngine\Enums\ImapSortKey; use DirectoryTree\ImapEngine\Enums\SortDirection; use DirectoryTree\ImapEngine\MessageInterface; use DirectoryTree\ImapEngine\MessageQueryInterface; @@ -34,9 +35,9 @@ public function __construct( */ public function get(): MessageCollection { - return new MessageCollection( + return $this->applyOrdering(new MessageCollection( $this->folder->getMessages() - ); + )); } /** @@ -70,11 +71,9 @@ public function firstOrFail(): MessageInterface */ public function append(string $message, mixed $flags = null, ?DateTimeInterface $date = null): AppendResult { - $uid = 1; - - if ($lastMessage = $this->get()->last()) { - $uid = $lastMessage->uid() + 1; - } + $uid = (int) collect($this->folder->getMessages())->max( + fn (FakeMessage $message) => $message->uid() + ) + 1; $this->folder->addMessage( new FakeMessage($uid, $flags === null ? [] : $flags, $message) @@ -83,6 +82,44 @@ public function append(string $message, mixed $flags = null, ?DateTimeInterface return new AppendResult(uid: $uid); } + /** + * Apply the selected ordering strategy. + */ + protected function applyOrdering(MessageCollection $messages): MessageCollection + { + if ($this->ordering instanceof UidOrder) { + return $messages->sortBy( + fn (MessageInterface $message) => $message->uid(), + descending: $this->ordering->direction === SortDirection::Descending, + )->values(); + } + + foreach (array_reverse($this->ordering->criteria) as $criterion) { + $messages = $messages->sortBy( + fn (MessageInterface $message) => $this->sortValue($message, $criterion->key), + descending: $criterion->direction === SortDirection::Descending, + ); + } + + return $messages->values(); + } + + /** + * Get a message's value for the given sort key. + */ + protected function sortValue(MessageInterface $message, ImapSortKey $key): mixed + { + return match ($key) { + ImapSortKey::Cc => head($message->cc())?->email() ?? '', + ImapSortKey::To => head($message->to())?->email() ?? '', + ImapSortKey::Date => $message->date()?->getTimestamp() ?? 0, + ImapSortKey::From => $message->from()?->email() ?? '', + ImapSortKey::Size => $message->size(), + ImapSortKey::Arrival => $message->uid(), + ImapSortKey::Subject => $message->subject() ?? '', + }; + } + /** * {@inheritDoc} */ diff --git a/tests/Unit/MessageQueryTest.php b/tests/Unit/MessageQueryTest.php index 5d0078f..bf0f3c1 100644 --- a/tests/Unit/MessageQueryTest.php +++ b/tests/Unit/MessageQueryTest.php @@ -630,6 +630,10 @@ function query(?Mailbox $mailbox = null): MessageQuery 'TAG2 OK CAPABILITY completed', '* SORT 3 1 2', 'TAG3 OK SORT completed', + '* 1 FETCH (UID 1 FLAGS ())', + '* 2 FETCH (UID 2 FLAGS ())', + '* 3 FETCH (UID 3 FLAGS ())', + 'TAG4 OK UID FETCH completed', ]); $mailbox = Mailbox::make(); @@ -638,6 +642,7 @@ function query(?Mailbox $mailbox = null): MessageQuery $uids = query($mailbox) ->orderByUid(SortDirection::Descending) ->sortBy('date') + ->with(MessageData::flags()) ->get() ->map(fn ($message) => $message->uid()) ->all(); @@ -647,6 +652,27 @@ function query(?Mailbox $mailbox = null): MessageQuery expect($uids)->toBe([3, 1, 2]); }); +test('sortBy recognizes extended SORT capabilities', function () { + $stream = new FakeStream; + $stream->open(); + + $stream->feed([ + '* OK Welcome to IMAP', + 'TAG1 OK Logged in', + '* CAPABILITY IMAP4rev1 SORT=DISPLAY', + 'TAG2 OK CAPABILITY completed', + '* SORT 1', + 'TAG3 OK SORT completed', + ]); + + $mailbox = Mailbox::make(); + $mailbox->connect(new ImapConnection($stream)); + + query($mailbox)->sortBy('date')->get(); + + $stream->assertWritten('TAG3 UID SORT (DATE) UTF-8 ALL'); +}); + test('sortBy sends correct sort command with descending order', function () { $stream = new FakeStream; $stream->open(); diff --git a/tests/Unit/Testing/FakeMessageQueryTest.php b/tests/Unit/Testing/FakeMessageQueryTest.php index 697173d..a223756 100644 --- a/tests/Unit/Testing/FakeMessageQueryTest.php +++ b/tests/Unit/Testing/FakeMessageQueryTest.php @@ -1,6 +1,8 @@ toHaveCount(2); }); +test('it orders messages by uid', function () { + $folder = new FakeFolder('INBOX', messages: [ + new FakeMessage(2), + new FakeMessage(1), + new FakeMessage(3), + ]); + + $query = new FakeMessageQuery($folder); + + $ascending = $query + ->orderByUid() + ->get() + ->map(fn (FakeMessage $message) => $message->uid()) + ->all(); + + $descending = $query + ->orderByUid(SortDirection::Descending) + ->get() + ->map(fn (FakeMessage $message) => $message->uid()) + ->all(); + + expect($ascending)->toBe([1, 2, 3]) + ->and($descending)->toBe([3, 2, 1]); +}); + +test('it applies server sort criteria', function () { + $folder = new FakeFolder('INBOX', messages: [ + new FakeMessage(1, contents: "Subject: Zebra\r\n\r\n"), + new FakeMessage(2, contents: "Subject: Apple\r\n\r\n"), + ]); + + $query = new FakeMessageQuery($folder); + + $uids = $query + ->sortBy(ImapSortKey::Subject) + ->get() + ->map(fn (FakeMessage $message) => $message->uid()) + ->all(); + + expect($uids)->toBe([2, 1]); +}); + test('it counts messages correctly', function () { $folder = new FakeFolder('INBOX', messages: [ new FakeMessage(1), @@ -53,7 +97,7 @@ $first = $query->first(); expect($first)->toBeInstanceOf(FakeMessage::class); - expect($first->uid())->toBe(1); + expect($first->uid())->toBe(2); }); test('it returns null when no messages exist for first()', function () { @@ -195,8 +239,8 @@ } }, 2); // Use chunk size of 2 - // Should process messages 1, 2, and 3, then break - expect($processedUids)->toBe([1, 2, 3]); + // Should process messages 5, 4, and 3, then break + expect($processedUids)->toBe([5, 4, 3]); }); test('chunk breaks when callback returns false', function () { @@ -240,7 +284,7 @@ }); // Should process all messages - expect($processedUids)->toBe([1, 2, 3]); + expect($processedUids)->toBe([3, 2, 1]); }); test('chunk processes all chunks when callback never returns false', function () { From de8379c6a64362845b8adfb35e114ef375282fd0 Mon Sep 17 00:00:00 2001 From: Steve Bauman Date: Tue, 1 Sep 2026 16:21:02 -0400 Subject: [PATCH 6/6] Add mailbox capability checks --- src/Folder.php | 4 ++-- src/HasCapabilities.php | 24 ++++++++++++++++++++++++ src/Mailbox.php | 2 ++ src/MailboxInterface.php | 5 +++++ src/Message.php | 10 +++------- src/MessageQuery.php | 6 +----- src/Testing/FakeMailbox.php | 3 +++ tests/Unit/MailboxTest.php | 6 ++++++ tests/Unit/Testing/FakeMailboxTest.php | 2 ++ 9 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 src/HasCapabilities.php diff --git a/src/Folder.php b/src/Folder.php index b73aecf..0646fe1 100644 --- a/src/Folder.php +++ b/src/Folder.php @@ -95,7 +95,7 @@ public function messages(): MessageQuery */ public function idle(callable $callback, ?callable $query = null, callable|int $timeout = 300): void { - if (! in_array('IDLE', $this->mailbox->capabilities())) { + if (! $this->mailbox->hasCapability('IDLE')) { throw new ImapCapabilityException('Unable to IDLE. IMAP server does not support IDLE capability.'); } @@ -183,7 +183,7 @@ public function select(bool $force = false): void */ public function quota(): array { - if (! in_array('QUOTA', $this->mailbox->capabilities())) { + if (! $this->mailbox->hasCapability('QUOTA')) { throw new ImapCapabilityException( 'Unable to fetch mailbox quotas. IMAP server does not support QUOTA capability.' ); diff --git a/src/HasCapabilities.php b/src/HasCapabilities.php new file mode 100644 index 0000000..c398009 --- /dev/null +++ b/src/HasCapabilities.php @@ -0,0 +1,24 @@ +capabilities() as $supported) { + $supported = strtoupper($supported); + + if ($supported === $capability || str_starts_with($supported, "{$capability}=")) { + return true; + } + } + + return false; + } +} diff --git a/src/Mailbox.php b/src/Mailbox.php index 3661686..ef4c206 100644 --- a/src/Mailbox.php +++ b/src/Mailbox.php @@ -12,6 +12,8 @@ class Mailbox implements MailboxInterface { + use HasCapabilities; + /** * The mailbox configuration. */ diff --git a/src/MailboxInterface.php b/src/MailboxInterface.php index fba71c1..0c0792b 100644 --- a/src/MailboxInterface.php +++ b/src/MailboxInterface.php @@ -51,6 +51,11 @@ public function folders(): FolderRepositoryInterface; */ public function capabilities(): array; + /** + * Determine if the mailbox supports the given capability. + */ + public function hasCapability(string $capability): bool; + /** * Select the given folder. */ diff --git a/src/Message.php b/src/Message.php index 16647c4..db78a17 100644 --- a/src/Message.php +++ b/src/Message.php @@ -187,9 +187,7 @@ public function copy(string $folder): ?int { $mailbox = $this->folder->mailbox(); - $capabilities = $mailbox->capabilities(); - - if (! in_array('UIDPLUS', $capabilities)) { + if (! $mailbox->hasCapability('UIDPLUS')) { throw new ImapCapabilityException( 'Unable to copy message. IMAP server does not support UIDPLUS capability' ); @@ -209,15 +207,13 @@ public function move(string $folder, bool $expunge = false): ?int { $mailbox = $this->folder->mailbox(); - $capabilities = $mailbox->capabilities(); - switch (true) { - case in_array('MOVE', $capabilities): + case $mailbox->hasCapability('MOVE'): $response = $mailbox->connection()->move($folder, $this->uid); return MessageResponseParser::getUidFromCopy($response); - case in_array('UIDPLUS', $capabilities): + case $mailbox->hasCapability('UIDPLUS'): $uid = $this->copy($folder); $this->delete($expunge); diff --git a/src/MessageQuery.php b/src/MessageQuery.php index 6e455a9..0327b66 100644 --- a/src/MessageQuery.php +++ b/src/MessageQuery.php @@ -409,11 +409,7 @@ protected function search(): Collection */ protected function sort(ImapSort $sort): Collection { - $supportsSort = collect($this->folder->mailbox()->capabilities())->contains( - fn (string $capability) => str_starts_with(strtoupper($capability), 'SORT') - ); - - if (! $supportsSort) { + if (! $this->folder->mailbox()->hasCapability('SORT')) { throw new ImapCapabilityException( 'Unable to sort messages. IMAP server does not support SORT capability.' ); diff --git a/src/Testing/FakeMailbox.php b/src/Testing/FakeMailbox.php index e1ce41c..f7d2275 100644 --- a/src/Testing/FakeMailbox.php +++ b/src/Testing/FakeMailbox.php @@ -6,10 +6,13 @@ use DirectoryTree\ImapEngine\Exceptions\Exception; use DirectoryTree\ImapEngine\FolderInterface; use DirectoryTree\ImapEngine\FolderRepositoryInterface; +use DirectoryTree\ImapEngine\HasCapabilities; use DirectoryTree\ImapEngine\MailboxInterface; class FakeMailbox implements MailboxInterface { + use HasCapabilities; + /** * The currently selected folder. */ diff --git a/tests/Unit/MailboxTest.php b/tests/Unit/MailboxTest.php index 8e5f873..09b6448 100644 --- a/tests/Unit/MailboxTest.php +++ b/tests/Unit/MailboxTest.php @@ -152,4 +152,10 @@ 'STARTTLS', 'AUTH=PLAIN', ]); + + expect($mailbox->hasCapability('imap4rev1'))->toBeTrue(); + expect($mailbox->hasCapability('AUTH'))->toBeTrue(); + expect($mailbox->hasCapability('AUTH=PLAIN'))->toBeTrue(); + expect($mailbox->hasCapability('AUTH=LOGIN'))->toBeFalse(); + expect($mailbox->hasCapability('START'))->toBeFalse(); }); diff --git a/tests/Unit/Testing/FakeMailboxTest.php b/tests/Unit/Testing/FakeMailboxTest.php index c257505..126ac87 100644 --- a/tests/Unit/Testing/FakeMailboxTest.php +++ b/tests/Unit/Testing/FakeMailboxTest.php @@ -15,6 +15,8 @@ expect($mailbox->config('host'))->toBe('imap.example.com'); expect($mailbox->config('username'))->toBe('user1'); expect($mailbox->capabilities())->toBe(['IMAP4rev1', 'STARTTLS']); + expect($mailbox->hasCapability('imap4rev1'))->toBeTrue(); + expect($mailbox->hasCapability('START'))->toBeFalse(); }); test('it returns config values correctly', function () {