Files
provider_imap/lib/Client/Command/AppendCommand.php
T
Sebastian 3dd9c2f983 feat: append command
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
2026-06-23 15:13:42 -04:00

103 lines
3.1 KiB
PHP

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