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

154 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXM\ProviderImap\Client\Command;
use KTXM\ProviderImap\Client\Command\Result\SortResult;
use KTXM\ProviderImap\Client\IdentifierMode;
use KTXM\ProviderImap\Client\ImapException;
use KTXM\ProviderImap\Client\Protocol\RequestFrame;
use KTXM\ProviderImap\Client\Protocol\Response\TaggedResponse;
use KTXM\ProviderImap\Client\Protocol\Response\UntaggedResponse;
use KTXM\ProviderImap\Client\Protocol\ResponseStream;
use KTXM\ProviderImap\Client\SearchCriteriaBuilder;
use KTXM\ProviderImap\Client\SessionContext;
use KTXM\ProviderImap\Client\SessionState;
/**
* @implements CommandInterface<SortResult>
*/
final class SortCommand implements CommandInterface
{
/**
* @param SearchCriteriaBuilder|list<string> $criteria
* @param list<string> $sortCriteria
*/
public function __construct(
private readonly array $sortCriteria,
private readonly SearchCriteriaBuilder|array $criteria = ['ALL'],
private readonly IdentifierMode $identifierMode = IdentifierMode::Sequence,
private readonly string $charset = 'UTF-8',
) {}
public function name(): string
{
return 'SORT';
}
public function allowedStates(): array
{
return [SessionState::Selected];
}
public function encode(string $tag, SessionContext $context): RequestFrame
{
unset($tag, $context);
$sortCriteria = $this->normalizeSortCriteria($this->sortCriteria);
if ($sortCriteria === []) {
throw new ImapException('SORT requires at least one sort criterion.');
}
$criteria = $this->normalizeCriteria(
$this->criteria instanceof SearchCriteriaBuilder
? $this->criteria->toArray()
: $this->criteria,
);
$command = $this->identifierMode === IdentifierMode::Uid ? 'UID SORT' : 'SORT';
$command .= sprintf(
' (%s) %s %s',
implode(' ', $sortCriteria),
strtoupper(trim($this->charset)),
implode(' ', $criteria),
);
return new RequestFrame($command);
}
public function handle(ResponseStream $responses, SessionContext $context): SortResult
{
if ($context->selectedMailbox() === null) {
throw new ImapException('SORT requires a selected mailbox.');
}
$matches = [];
foreach ($responses as $response) {
if ($response instanceof UntaggedResponse && $response->label() === 'SORT') {
$matches = $this->parseMatches($response->payloadTokens());
continue;
}
if ($response instanceof TaggedResponse) {
if (!$response->isOk()) {
throw new ImapException('SORT failed: ' . $response->text());
}
return new SortResult($matches, $this->identifierMode);
}
}
throw new ImapException('SORT did not receive a tagged completion response.');
}
/**
* @param list<string> $criteria
* @return list<string>
*/
private function normalizeCriteria(array $criteria): array
{
$normalized = [];
foreach ($criteria as $criterion) {
$criterion = trim($criterion);
if ($criterion === '') {
continue;
}
$normalized[] = $criterion;
}
return $normalized === [] ? ['ALL'] : $normalized;
}
/**
* @param list<string> $criteria
* @return list<string>
*/
private function normalizeSortCriteria(array $criteria): array
{
$normalized = [];
foreach ($criteria as $criterion) {
$criterion = strtoupper(trim($criterion));
if ($criterion === '') {
continue;
}
$normalized[] = $criterion;
}
return $normalized;
}
/**
* @param list<string> $tokens
* @return list<int>
*/
private function parseMatches(array $tokens): array
{
$matches = [];
foreach ($tokens as $token) {
if (preg_match('/^[1-9]\d*$/', $token) !== 1) {
continue;
}
$matches[] = (int) $token;
}
return $matches;
}
}