Files
provider_imap/lib/Client/Protocol/Command/SearchCommand.php
T
2026-09-25 12:14:37 -04:00

125 lines
3.6 KiB
PHP

<?php
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;
use KTXM\ProviderImap\Client\Protocol\Command\Argument\SearchCriteriaBuilder;
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\Protocol\SessionContext;
use KTXM\ProviderImap\Client\Protocol\SessionState;
/**
* @implements CommandInterface<SearchResult>
*/
final class SearchCommand implements CommandInterface
{
/**
* @param SearchCriteriaBuilder|list<string> $criteria
*/
public function __construct(
private readonly SearchCriteriaBuilder|array $criteria = ['ALL'],
private readonly IdentifierMode $identifierMode = IdentifierMode::Sequence,
private readonly ?string $charset = 'UTF-8',
) {}
public function name(): string
{
return 'SEARCH';
}
public function allowedStates(): array
{
return [SessionState::Selected];
}
public function encode(string $tag, SessionContext $context): RequestFrame
{
unset($tag, $context);
$criteria = $this->normalizeCriteria(
$this->criteria instanceof SearchCriteriaBuilder
? $this->criteria->toArray()
: $this->criteria,
);
$command = $this->identifierMode === IdentifierMode::Uid ? 'UID SEARCH' : 'SEARCH';
if ($this->charset !== null && $this->charset !== '') {
$command .= ' CHARSET ' . strtoupper(trim($this->charset));
}
$command .= ' ' . implode(' ', $criteria);
return new RequestFrame($command);
}
public function handle(ResponseStream $responses, SessionContext $context): SearchResult
{
if ($context->selectedMailbox() === null) {
throw new ImapException('SEARCH requires a selected mailbox.');
}
$matches = [];
foreach ($responses as $response) {
if ($response instanceof UntaggedResponse && $response->label() === 'SEARCH') {
$matches = $this->parseMatches($response->payloadTokens());
continue;
}
if ($response instanceof TaggedResponse) {
CompletionChecker::assertSuccess($this->name(), $response);
return new SearchResult($matches, $this->identifierMode);
}
}
throw new ImapException('SEARCH 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> $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;
}
}