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

126 lines
3.5 KiB
PHP

<?php
declare(strict_types=1);
namespace KTXM\ProviderImap\Client\Command;
use KTXM\ProviderImap\Client\Command\Result\SearchResult;
use KTXM\ProviderImap\Client\IdentifierMode;
use KTXM\ProviderImap\Client\ImapException;
use KTXM\ProviderImap\Client\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\SessionContext;
use KTXM\ProviderImap\Client\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) {
if (!$response->isOk()) {
throw new ImapException('SEARCH failed: ' . $response->text());
}
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;
}
}