generated from Nodarx/template
feat: append command
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXM\ProviderImap\Client\Command;
|
||||
|
||||
use KTXM\ProviderImap\Client\ImapException;
|
||||
use KTXM\ProviderImap\Client\Protocol\RequestFrame;
|
||||
use KTXM\ProviderImap\Client\Protocol\Response\ContinuationResponse;
|
||||
use KTXM\ProviderImap\Client\Protocol\Response\TaggedResponse;
|
||||
use KTXM\ProviderImap\Client\Protocol\ResponseStream;
|
||||
use KTXM\ProviderImap\Client\SessionContext;
|
||||
use KTXM\ProviderImap\Client\SessionState;
|
||||
|
||||
/**
|
||||
* APPEND a raw RFC822 message to a mailbox (RFC 3501 §6.3.11).
|
||||
*
|
||||
* Uses a synchronizing literal: the command line ends with `{<len>}`, the
|
||||
* server replies with a continuation request ("+"), and only then is the
|
||||
* message streamed back via {@see ResponseStream::respond()}. Returns the
|
||||
* assigned UID when the server reports APPENDUID (RFC 4315 / UIDPLUS),
|
||||
* otherwise null.
|
||||
*
|
||||
* @implements CommandInterface<int|null>
|
||||
*/
|
||||
final class AppendCommand implements CommandInterface
|
||||
{
|
||||
private readonly string $literal;
|
||||
|
||||
/**
|
||||
* @param list<string> $flags optional initial flags, e.g. ['\\Seen']
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly string $mailbox,
|
||||
string $message,
|
||||
private readonly array $flags = [],
|
||||
) {
|
||||
// IMAP literals are octet-counted; normalise to CRLF line endings.
|
||||
$this->literal = (string) preg_replace('/\r\n|\r|\n/', "\r\n", $message);
|
||||
}
|
||||
|
||||
public function name(): string
|
||||
{
|
||||
return 'APPEND';
|
||||
}
|
||||
|
||||
public function allowedStates(): array
|
||||
{
|
||||
return [SessionState::Authenticated, SessionState::Selected];
|
||||
}
|
||||
|
||||
public function encode(string $tag, SessionContext $context): RequestFrame
|
||||
{
|
||||
unset($tag, $context);
|
||||
|
||||
$flagSegment = $this->flags === [] ? '' : '(' . implode(' ', $this->flags) . ') ';
|
||||
|
||||
return new RequestFrame(sprintf(
|
||||
'APPEND %s %s{%d}',
|
||||
$this->quote($this->mailbox),
|
||||
$flagSegment,
|
||||
strlen($this->literal),
|
||||
));
|
||||
}
|
||||
|
||||
public function handle(ResponseStream $responses, SessionContext $context): ?int
|
||||
{
|
||||
unset($context);
|
||||
|
||||
foreach ($responses as $response) {
|
||||
if ($response instanceof ContinuationResponse) {
|
||||
// Continuation granted: stream the literal, then CRLF to end the command.
|
||||
$responses->respond($this->literal . "\r\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($response instanceof TaggedResponse) {
|
||||
if (!$response->isOk()) {
|
||||
throw new ImapException('APPEND failed: ' . $response->text());
|
||||
}
|
||||
|
||||
return $this->parseAppendUid($response->text());
|
||||
}
|
||||
}
|
||||
|
||||
throw new ImapException('APPEND did not receive a tagged completion response.');
|
||||
}
|
||||
|
||||
private function quote(string $mailbox): string
|
||||
{
|
||||
return '"' . addcslashes($mailbox, "\\\"") . '"';
|
||||
}
|
||||
|
||||
private function parseAppendUid(string $text): ?int
|
||||
{
|
||||
if (preg_match('/\[APPENDUID\s+\d+\s+(\d+)\]/i', $text, $matches) === 1) {
|
||||
return (int) $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -42,9 +42,14 @@ final class CommandExecutor
|
||||
$frame = $command->encode($tag, $context);
|
||||
$this->writer->write($tag, $frame);
|
||||
|
||||
return $command->handle(new ResponseStream(function () use ($tag, $context): Generator {
|
||||
return $command->handle(new ResponseStream(
|
||||
function () use ($tag, $context): Generator {
|
||||
yield from $this->processPerform($tag, $context);
|
||||
}), $context);
|
||||
},
|
||||
function (string $payload): void {
|
||||
$this->writer->writeRaw($payload);
|
||||
},
|
||||
), $context);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -27,6 +27,21 @@ final class ProtocolWriter
|
||||
$this->connection->write($wire);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write raw bytes to the connection without tagging or logging the payload.
|
||||
*
|
||||
* Used to send literal data (e.g. an APPEND message body) after the server
|
||||
* has issued a command continuation request.
|
||||
*/
|
||||
public function writeRaw(string $payload): void
|
||||
{
|
||||
$this->logger?->debug('IMAP literal sent (bytes={bytes})', [
|
||||
'bytes' => strlen($payload),
|
||||
]);
|
||||
|
||||
$this->connection->write($payload);
|
||||
}
|
||||
|
||||
private function sanitizeWire(string $wire): string
|
||||
{
|
||||
$trimmed = rtrim($wire, "\r\n");
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace KTXM\ProviderImap\Client\Protocol;
|
||||
|
||||
use Generator;
|
||||
use IteratorAggregate;
|
||||
use KTXM\ProviderImap\Client\ImapException;
|
||||
use Traversable;
|
||||
|
||||
final class ResponseStream implements IteratorAggregate
|
||||
@@ -13,16 +14,39 @@ final class ResponseStream implements IteratorAggregate
|
||||
/** @var \Closure():Generator */
|
||||
private readonly \Closure $generatorFactory;
|
||||
|
||||
/** @var (\Closure(string):void)|null */
|
||||
private readonly ?\Closure $continuationWriter;
|
||||
|
||||
/**
|
||||
* @param \Closure():Generator $generatorFactory
|
||||
* @param (\Closure(string):void)|null $continuationWriter writes raw bytes
|
||||
* back to the server in response to a command continuation request
|
||||
*/
|
||||
public function __construct(\Closure $generatorFactory)
|
||||
public function __construct(\Closure $generatorFactory, ?\Closure $continuationWriter = null)
|
||||
{
|
||||
$this->generatorFactory = $generatorFactory;
|
||||
$this->continuationWriter = $continuationWriter;
|
||||
}
|
||||
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return ($this->generatorFactory)();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send raw bytes to the server after a command continuation request (a "+"
|
||||
* response) — e.g. the literal payload of an APPEND.
|
||||
*
|
||||
* Must only be called while iterating this stream, in response to a
|
||||
* {@see \KTXM\ProviderImap\Client\Protocol\Response\ContinuationResponse};
|
||||
* the next pulled response then reflects the server's reaction to the data.
|
||||
*/
|
||||
public function respond(string $payload): void
|
||||
{
|
||||
if ($this->continuationWriter === null) {
|
||||
throw new ImapException('This response stream does not support continuation replies.');
|
||||
}
|
||||
|
||||
($this->continuationWriter)($payload);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ namespace KTXM\ProviderImap\Service\Remote;
|
||||
use DateTimeImmutable;
|
||||
use Generator;
|
||||
use KTXM\ProviderImap\Client\Client;
|
||||
use KTXM\ProviderImap\Client\Command\AppendCommand;
|
||||
use KTXM\ProviderImap\Client\Command\FetchManyCommand;
|
||||
use KTXM\ProviderImap\Client\Command\FetchOneCommand;
|
||||
use KTXM\ProviderImap\Client\Command\ExpungeCommand;
|
||||
@@ -410,9 +411,9 @@ class RemoteMailService
|
||||
*
|
||||
* @param string[] $flags optional initial flags, e.g. ['\\Seen']
|
||||
*/
|
||||
public function entityCreate(string $collection, string $rawMessage, array $flags = []): int
|
||||
public function entityCreate(string $collection, string $rawMessage, array $flags = []): ?int
|
||||
{
|
||||
return $this->client->append($rawMessage, $collection, !empty($flags) ? $flags : null);
|
||||
return $this->client->perform(new AppendCommand($collection, $rawMessage, $flags));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user