Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Connection/ConnectionInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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\ImapSort;
use Generator;

interface ConnectionInterface
Expand Down Expand Up @@ -121,7 +121,7 @@ public function search(array $params): UntaggedResponse;
*
* @see https://datatracker.ietf.org/doc/html/rfc5256
*/
public function sort(ImapSortKey $key, string $direction, array $params): UntaggedResponse;
public function sort(ImapSort $sort, array $params): UntaggedResponse;

/**
* Send a "FETCH" command.
Expand Down
8 changes: 3 additions & 5 deletions src/Connection/ImapConnection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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\ImapSort;
use DirectoryTree\ImapEngine\Support\Str;
use Exception;
use Generator;
Expand Down Expand Up @@ -509,11 +509,9 @@ public function search(array $params): UntaggedResponse
/**
* {@inheritDoc}
*/
public function sort(ImapSortKey $key, string $direction, array $params): UntaggedResponse
public function sort(ImapSort $sort, array $params): UntaggedResponse
{
$sortCriteria = $direction === 'desc' ? "REVERSE {$key->value}" : $key->value;

$this->send('UID SORT', ["({$sortCriteria})", 'UTF-8', ...$params], tag: $tag);
$this->send('UID SORT', ["({$sort->toImap()})", 'UTF-8', ...$params], tag: $tag);

$this->assertTaggedResponse($tag);

Expand Down
9 changes: 9 additions & 0 deletions src/Enums/SortDirection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace DirectoryTree\ImapEngine\Enums;

enum SortDirection: string
{
case Ascending = 'asc';
case Descending = 'desc';
}
4 changes: 2 additions & 2 deletions src/Folder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}

Expand Down Expand Up @@ -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.'
);
Expand Down
24 changes: 24 additions & 0 deletions src/HasCapabilities.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace DirectoryTree\ImapEngine;

trait HasCapabilities
{
/**
* Determine if the mailbox supports the given capability.
*/
public function hasCapability(string $capability): bool
{
$capability = strtoupper($capability);

foreach ($this->capabilities() as $supported) {
$supported = strtoupper($supported);

if ($supported === $capability || str_starts_with($supported, "{$capability}=")) {
return true;
}
}

return false;
}
}
42 changes: 42 additions & 0 deletions src/ImapSort.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace DirectoryTree\ImapEngine;

class ImapSort
{
/**
* The sort criteria.
*
* @var array<int, SortCriterion>
*/
public array $criteria;

/**
* Constructor.
*/
public function __construct(SortCriterion $criterion, SortCriterion ...$criteria)
{
$this->criteria = [$criterion, ...$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,
));
}
}
2 changes: 2 additions & 0 deletions src/Mailbox.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

class Mailbox implements MailboxInterface
{
use HasCapabilities;

/**
* The mailbox configuration.
*/
Expand Down
5 changes: 5 additions & 0 deletions src/MailboxInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
10 changes: 3 additions & 7 deletions src/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -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'
);
Expand All @@ -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);
Expand Down
50 changes: 33 additions & 17 deletions src/MessageQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use DirectoryTree\ImapEngine\Connection\Tokens\Token;
use DirectoryTree\ImapEngine\Enums\ImapFetchIdentifier;
use DirectoryTree\ImapEngine\Enums\ImapFlag;
use DirectoryTree\ImapEngine\Enums\SortDirection;
use DirectoryTree\ImapEngine\Exceptions\ImapCapabilityException;
use DirectoryTree\ImapEngine\Exceptions\ImapCommandException;
use DirectoryTree\ImapEngine\MessageData\FetchItem;
Expand All @@ -33,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.
Expand Down Expand Up @@ -68,7 +71,7 @@ public function firstOrFail(): MessageInterface
*/
public function get(): MessageCollection
{
return $this->process($this->sortKey ? $this->sort() : $this->search());
return $this->process($this->orderedUids());
}

/**
Expand Down Expand Up @@ -103,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);
Expand Down Expand Up @@ -337,13 +340,10 @@ 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) {
'asc' => $messages->sort(SORT_NUMERIC),
'desc' => $messages->sortDesc(SORT_NUMERIC),
if ($this->ordering instanceof UidOrder) {
$messages = match ($this->ordering->direction) {
SortDirection::Ascending => $messages->sort(SORT_NUMERIC),
SortDirection::Descending => $messages->sortDesc(SORT_NUMERIC),
};
}

Expand All @@ -360,11 +360,28 @@ 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();
}

/**
* 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),
};
}

/**
Expand All @@ -390,9 +407,9 @@ 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())) {
if (! $this->folder->mailbox()->hasCapability('SORT')) {
throw new ImapCapabilityException(
'Unable to sort messages. IMAP server does not support SORT capability.'
);
Expand All @@ -403,8 +420,7 @@ protected function sort(): Collection
}

$response = $this->connection()->sort(
$this->sortKey,
$this->sortDirection,
$sort,
[$this->query->toImap()]
);

Expand Down
66 changes: 12 additions & 54 deletions src/MessageQueryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -57,64 +58,21 @@ public function without(FetchItem ...$items): static;
public function only(FetchItem ...$items): static;

/**
* Set the fetch order.
* Order messages locally by UID, replacing any server-side sort criteria.
*/
public function setFetchOrder(string $fetchOrder): MessageQueryInterface;
public function orderByUid(
SortDirection|string $direction = SortDirection::Ascending,
): static;

/**
* Get the fetch 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;

/**
* Sort messages by a field using server-side sorting (RFC 5256).
*/
public function sortBy(ImapSortKey|string $key, string $direction = 'asc'): MessageQueryInterface;

/**
* Sort messages by a field in descending order using server-side sorting.
* Add a server-side sort criterion using RFC 5256.
*
* Subsequent calls are used as tie-breakers in the order they are added.
*/
public function sortByDesc(ImapSortKey|string $key): MessageQueryInterface;
public function sortBy(
ImapSortKey|string $key,
SortDirection|string $direction = SortDirection::Ascending,
): static;

/**
* Count all available messages matching the current search criteria.
Expand Down
Loading
Loading