generated from Nodarx/template
120 lines
4.0 KiB
PHP
120 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace KTXM\ProviderImap\Client\Protocol;
|
|
|
|
use Generator;
|
|
use KTXM\ProviderImap\Client\Command\CommandInterface;
|
|
use KTXM\ProviderImap\Client\FetchTarget;
|
|
use KTXM\ProviderImap\Client\ImapException;
|
|
use KTXM\ProviderImap\Client\Protocol\Response\TaggedResponse;
|
|
use KTXM\ProviderImap\Client\Protocol\Response\UntaggedResponse;
|
|
use KTXM\ProviderImap\Client\Protocol\RequestFrame;
|
|
use KTXM\ProviderImap\Client\SessionContext;
|
|
use KTXM\ProviderImap\Client\SessionState;
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
final class CommandExecutor
|
|
{
|
|
public function __construct(
|
|
private readonly ProtocolReader $reader,
|
|
private readonly ProtocolWriter $writer,
|
|
private readonly TagGenerator $tags = new TagGenerator(),
|
|
private readonly ?LoggerInterface $logger = null,
|
|
) {}
|
|
|
|
/**
|
|
* @template TResult
|
|
* @param CommandInterface<TResult> $command
|
|
* @return TResult
|
|
*/
|
|
public function perform(CommandInterface $command, SessionContext $context): mixed
|
|
{
|
|
$this->assertState($command->allowedStates(), $context->state(), $command->name());
|
|
|
|
$this->logger?->debug('IMAP command execution started: {command} (state={state})', [
|
|
'command' => $command->name(),
|
|
'state' => $context->state()->value,
|
|
]);
|
|
|
|
$tag = $this->tags->next();
|
|
$frame = $command->encode($tag, $context);
|
|
$this->writer->write($tag, $frame);
|
|
|
|
return $command->handle(new ResponseStream(function () use ($tag, $context): Generator {
|
|
yield from $this->processPerform($tag, $context);
|
|
}), $context);
|
|
}
|
|
|
|
/**
|
|
* Stream the raw bytes of a single IMAP BODY section without buffering.
|
|
*
|
|
* Sends a UID FETCH for the given section and yields the literal bytes
|
|
* directly from the socket in chunks, never assembling a full string.
|
|
* The caller MUST fully exhaust the returned Generator before issuing
|
|
* any further IMAP commands.
|
|
*
|
|
* @return \Generator<string> raw (transfer-encoded) bytes from the socket
|
|
*/
|
|
public function download(FetchTarget $target, string $section, int $chunkSize, SessionContext $context): \Generator
|
|
{
|
|
$this->assertState([SessionState::Selected], $context->state(), 'FETCH (download)');
|
|
|
|
$tag = $this->tags->next();
|
|
$this->writer->write($tag, new RequestFrame(sprintf(
|
|
'UID FETCH %s (UID BODY[%s])',
|
|
$target->sequenceSet()->toCommand(),
|
|
$section,
|
|
)));
|
|
|
|
$result = $this->reader->readUntilFetchLiteral($tag);
|
|
|
|
if ($result === null) {
|
|
return; // UID not found or empty FETCH result
|
|
}
|
|
|
|
yield from $this->reader->streamLiteral($result['literalLength'], $chunkSize);
|
|
|
|
$this->reader->readToEnd($tag);
|
|
}
|
|
|
|
/**
|
|
* @param list<SessionState> $allowedStates
|
|
*/
|
|
private function assertState(array $allowedStates, SessionState $currentState, string $commandName): void
|
|
{
|
|
foreach ($allowedStates as $allowedState) {
|
|
if ($allowedState === $currentState) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new ImapException(sprintf(
|
|
'Command %s is not allowed while session is in state %s.',
|
|
$commandName,
|
|
$currentState->value,
|
|
));
|
|
}
|
|
|
|
private function processPerform(string $tag, SessionContext $context): Generator
|
|
{
|
|
while (true) {
|
|
$response = $this->reader->readResponse();
|
|
|
|
if ($response instanceof UntaggedResponse && $response->label() === 'CAPABILITY') {
|
|
$context->replaceCapabilities(...$response->payloadTokens());
|
|
}
|
|
|
|
yield $response;
|
|
|
|
if ($response instanceof TaggedResponse && $response->tag() === $tag) {
|
|
$this->logger?->debug('IMAP command execution completed: tag={tag} status={status}', [
|
|
'tag' => $tag,
|
|
'status' => $response->status(),
|
|
]);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
} |