$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 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 $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; } } } }