generated from Nodarx/template
feat: initial version
Signed-off-by: Sebastian Krupinski <root@LAPTOP-7DVOR6NC>
This commit was merged in pull request #1.
This commit is contained in:
515
lib/Client/Client.php
Normal file
515
lib/Client/Client.php
Normal file
@@ -0,0 +1,515 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Gricob\IMAP;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Exception;
|
||||
use Generator;
|
||||
use Gricob\IMAP\Mime\LazyMessage;
|
||||
use Gricob\IMAP\Mime\Message;
|
||||
use Gricob\IMAP\Mime\Part\Disposition;
|
||||
use Gricob\IMAP\Mime\Part\LazyBody;
|
||||
use Gricob\IMAP\Mime\Part\MultiPart;
|
||||
use Gricob\IMAP\Mime\Part\Part;
|
||||
use Gricob\IMAP\Mime\Part\SinglePart;
|
||||
use Gricob\IMAP\Protocol\Command\AppendCommand;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Criteria;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\SequenceSet;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Store\Flags;
|
||||
use Gricob\IMAP\Protocol\Command\Authenticate\SASLMechanism;
|
||||
use Gricob\IMAP\Protocol\Command\AuthenticateCommand;
|
||||
use Gricob\IMAP\Protocol\Command\Command;
|
||||
use Gricob\IMAP\Protocol\Command\CreateCommand;
|
||||
use Gricob\IMAP\Protocol\Command\ExpungeCommand;
|
||||
use Gricob\IMAP\Protocol\Command\FetchCommand;
|
||||
use Gricob\IMAP\Protocol\Command\ListCommand;
|
||||
use Gricob\IMAP\Protocol\Command\LogInCommand;
|
||||
use Gricob\IMAP\Protocol\Command\SelectCommand;
|
||||
use Gricob\IMAP\Protocol\Command\StoreCommand;
|
||||
use Gricob\IMAP\Protocol\Imap;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\FetchData;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\Fetch\BodyStructure;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\FlagsData;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\ExistsData;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\RecentData;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\ListData;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Data\SearchData;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Code\AppendUidCode;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Code\PermanentFlagsCode;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Code\UidNextCode;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Code\UidValidityCode;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Code\UnseenCode;
|
||||
use Gricob\IMAP\Protocol\Response\Line\Status\Status;
|
||||
use Gricob\IMAP\Protocol\Response\Response;
|
||||
use Gricob\IMAP\Transport\Socket\SocketConnection;
|
||||
use Gricob\IMAP\Transport\Traceable\TraceableConnection;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class Client
|
||||
{
|
||||
public Configuration $configuration;
|
||||
private Imap $imap;
|
||||
|
||||
private Mailbox $selectedMailbox;
|
||||
|
||||
private function __construct(
|
||||
Configuration $configuration,
|
||||
?LoggerInterface $logger,
|
||||
) {
|
||||
$connection = new SocketConnection(
|
||||
$configuration->transport,
|
||||
$configuration->host,
|
||||
$configuration->port,
|
||||
$configuration->timeout,
|
||||
$configuration->verifyPeer,
|
||||
$configuration->verifyPeerName,
|
||||
$configuration->allowSelfSigned,
|
||||
);
|
||||
|
||||
if (null !== $logger) {
|
||||
$connection = new TraceableConnection($connection, $logger);
|
||||
}
|
||||
|
||||
$this->configuration = $configuration;
|
||||
$this->imap = new Imap($connection);
|
||||
$this->selectedMailbox = new Mailbox([], '', '');
|
||||
}
|
||||
|
||||
public static function create(Configuration $configuration, ?LoggerInterface $logger = null): self
|
||||
{
|
||||
return new self($configuration, $logger);
|
||||
}
|
||||
|
||||
public function connect(): void
|
||||
{
|
||||
$this->imap->connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform STARTTLS negotiation (patch).
|
||||
*
|
||||
* Call after connect() but before logIn(). The underlying Imap protocol
|
||||
* layer sends the STARTTLS command and upgrades the socket to TLS.
|
||||
*/
|
||||
public function startTls(): void
|
||||
{
|
||||
$this->imap->startTls();
|
||||
}
|
||||
|
||||
public function disconnect(): void
|
||||
{
|
||||
$this->imap->disconnect();
|
||||
}
|
||||
|
||||
public function logIn(string $username, string $password): void
|
||||
{
|
||||
$this->send(new LogInCommand($username, $password));
|
||||
}
|
||||
|
||||
public function authenticate(SASLMechanism $mechanism): void
|
||||
{
|
||||
$this->send(new AuthenticateCommand($mechanism));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Mailbox>
|
||||
*/
|
||||
public function mailboxes(string $referenceName = '', string $pattern = '*'): array
|
||||
{
|
||||
$response = $this->send(new ListCommand($referenceName, $pattern));
|
||||
|
||||
return array_map(
|
||||
fn (ListData $data) => new Mailbox($data->nameAttributes, $data->hierarchyDelimiter, $data->name),
|
||||
$response->getData(ListData::class),
|
||||
);
|
||||
}
|
||||
|
||||
public function select(Mailbox|string $mailbox): Mailbox
|
||||
{
|
||||
if (is_string($mailbox)) {
|
||||
$mailbox = new Mailbox([], '', $mailbox);
|
||||
}
|
||||
|
||||
$response = $this->send(new SelectCommand($mailbox->name));
|
||||
|
||||
if ($flagsData = $response->getData(FlagsData::class)[0] ?? null) {
|
||||
$mailbox->flags = $flagsData->flags;
|
||||
}
|
||||
|
||||
if ($existsData = $response->getData(ExistsData::class)[0] ?? null) {
|
||||
$mailbox->exists = $existsData->numberOfMessages;
|
||||
}
|
||||
|
||||
if ($recentData = $response->getData(RecentData::class)[0] ?? null) {
|
||||
$mailbox->recent = $recentData->numberOfMessages;
|
||||
}
|
||||
|
||||
foreach ($response->getData(Status::class) as $status) {
|
||||
if ($status->code instanceof UnseenCode) {
|
||||
$mailbox->unseen = $status->code->seq;
|
||||
} elseif ($status->code instanceof UidValidityCode) {
|
||||
$mailbox->uidValidity = $status->code->value;
|
||||
} elseif ($status->code instanceof UidNextCode) {
|
||||
$mailbox->uidNext = $status->code->value;
|
||||
} elseif ($status->code instanceof PermanentFlagsCode) {
|
||||
$mailbox->permanentFlags = $status->code->flags;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->selectedMailbox = $mailbox;
|
||||
}
|
||||
|
||||
public function search(): Search
|
||||
{
|
||||
return new Search($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MessageNotFound
|
||||
*/
|
||||
public function fetch(int $id): Message
|
||||
{
|
||||
$response = $this->imap->send(
|
||||
new FetchCommand(
|
||||
$this->configuration->useUid,
|
||||
new SequenceSet($id),
|
||||
['INTERNALDATE', 'BODY[HEADER]', 'BODYSTRUCTURE']
|
||||
)
|
||||
);
|
||||
|
||||
$data = $response->getData(FetchData::class)[0] ?? throw new MessageNotFound();
|
||||
|
||||
if (null === $internalDate = $data->internalDate) {
|
||||
throw new Exception('Unable to fetch internal date from message '.$id);
|
||||
}
|
||||
|
||||
if (null === $part = $data->bodyStructure?->part) {
|
||||
throw new Exception('Unable to fetch body structure from message '.$id);
|
||||
}
|
||||
|
||||
return new Message(
|
||||
$id,
|
||||
$this->createHeaders($data) ?? [],
|
||||
$this->createMessagePart($id, '0', $part),
|
||||
$internalDate,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream FetchData for a specific set of UIDs, one response line at a time.
|
||||
*
|
||||
* Uses the same sendStreaming path as fetchMultiple() so responses are
|
||||
* processed as they arrive off the socket without buffering the entire
|
||||
* server reply. Items can be tailored per call-site; defaults to a rich
|
||||
* set that populates EntityResource fully (flags, envelope, body structure,
|
||||
* size, arrival date).
|
||||
*
|
||||
* @param int[] $uids
|
||||
* @param string[] $items IMAP fetch data items
|
||||
* @return Generator<int, FetchData> Yields uid => FetchData
|
||||
*/
|
||||
public function streamByUids(
|
||||
array $uids,
|
||||
array $items = ['FLAGS', 'ENVELOPE', 'INTERNALDATE', 'RFC822.SIZE', 'BODYSTRUCTURE', 'UID'],
|
||||
): Generator {
|
||||
$gen = $this->imap->sendStreaming(
|
||||
new FetchCommand(
|
||||
$this->configuration->useUid,
|
||||
new SequenceSet(...$uids),
|
||||
$items,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($gen as $line) {
|
||||
if (!$line instanceof FetchData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = $line->id;
|
||||
if ($this->configuration->useUid) {
|
||||
$id = $line->uid ?? throw new RuntimeException('Unable to get uid from message ' . $line->id);
|
||||
}
|
||||
|
||||
yield $id => $line;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream messages from a sequence range as a Generator, yielding each
|
||||
* LazyMessage as soon as its FETCH response line arrives off the socket —
|
||||
* without waiting for the entire batch to complete.
|
||||
*
|
||||
* Usage with an NDJSON HTTP response:
|
||||
*
|
||||
* foreach ($client->fetchMultiple(1, 50) as $message) {
|
||||
* echo json_encode($message) . "\n";
|
||||
* flush();
|
||||
* }
|
||||
*
|
||||
* @param int $from First sequence number (inclusive)
|
||||
* @param int $to Last sequence number (inclusive)
|
||||
* @return Generator<int, LazyMessage>
|
||||
*/
|
||||
public function fetchMultiple(int $from, int $to): Generator
|
||||
{
|
||||
$items = ['FLAGS', 'INTERNALDATE', 'BODY[HEADER]'];
|
||||
|
||||
$gen = $this->imap->sendStreaming(
|
||||
new FetchCommand(
|
||||
$this->configuration->useUid,
|
||||
SequenceSet::range($from, $to),
|
||||
$items,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ($gen as $line) {
|
||||
if (!$line instanceof FetchData) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$id = $line->id;
|
||||
if ($this->configuration->useUid) {
|
||||
$id = $line->uid ?? throw new RuntimeException('Unable to get uid from message ' . $line->id);
|
||||
}
|
||||
|
||||
yield new LazyMessage(
|
||||
$this,
|
||||
$id,
|
||||
$this->createHeaders($line),
|
||||
$line->internalDate,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
* @throws MessageNotFound
|
||||
*/
|
||||
public function fetchHeaders(int $id): array
|
||||
{
|
||||
$response = $this->imap->send(
|
||||
new FetchCommand(
|
||||
$this->configuration->useUid,
|
||||
new SequenceSet($id),
|
||||
['BODY[HEADER]']
|
||||
)
|
||||
);
|
||||
|
||||
/** @var FetchData $data */
|
||||
$data = $response->getData(FetchData::class)[0] ?? throw new MessageNotFound();
|
||||
|
||||
return $this->createHeaders($data) ?? [];
|
||||
}
|
||||
|
||||
public function fetchBody(int $id): Part
|
||||
{
|
||||
$response = $this->send(
|
||||
new FetchCommand(
|
||||
$this->configuration->useUid,
|
||||
new SequenceSet($id),
|
||||
['BODYSTRUCTURE']
|
||||
)
|
||||
);
|
||||
|
||||
$data = $response->getData(FetchData::class)[0];
|
||||
|
||||
if (null === $part = $data->bodyStructure?->part) {
|
||||
throw new Exception('Unable to fetch body from message '.$id);
|
||||
}
|
||||
|
||||
return $this->createMessagePart($id, '0', $part);
|
||||
}
|
||||
|
||||
public function fetchInternalDate(int $id): DateTimeImmutable
|
||||
{
|
||||
$response = $this->send(
|
||||
new FetchCommand(
|
||||
$this->configuration->useUid,
|
||||
new SequenceSet($id),
|
||||
['INTERNALDATE']
|
||||
)
|
||||
);
|
||||
|
||||
$data = $response->getData(FetchData::class)[0];
|
||||
|
||||
if (null === $internalDate = $data->internalDate) {
|
||||
throw new Exception('Unable to fetch internal date from message '.$id);
|
||||
}
|
||||
|
||||
return $internalDate;
|
||||
}
|
||||
|
||||
public function fetchSectionBody(int $id, string $section): string
|
||||
{
|
||||
$response = $this->send(
|
||||
new FetchCommand(
|
||||
$this->configuration->useUid,
|
||||
new SequenceSet($id),
|
||||
["BODY[$section]"]
|
||||
)
|
||||
);
|
||||
|
||||
$data = $response->getData(FetchData::class)[0];
|
||||
|
||||
return $data->getBodySection($section)?->text ?? '';
|
||||
}
|
||||
|
||||
public function deleteMessage(Message|int $message): void
|
||||
{
|
||||
$id = $message instanceof Message ? $message->id() : $message;
|
||||
|
||||
$this->send(
|
||||
new StoreCommand(
|
||||
$this->configuration->useUid,
|
||||
new SequenceSet($id),
|
||||
new Flags(['\Deleted'], '+')
|
||||
)
|
||||
);
|
||||
|
||||
$this->send(new ExpungeCommand());
|
||||
}
|
||||
|
||||
public function createMailbox(string $name): void
|
||||
{
|
||||
$this->send(new CreateCommand($name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string>|null $flags
|
||||
*/
|
||||
public function append(
|
||||
string $message,
|
||||
string $mailbox = 'INBOX',
|
||||
?array $flags = null,
|
||||
?DateTimeInterface $internalDate = null
|
||||
): int
|
||||
{
|
||||
$response = $this->send(new AppendCommand($mailbox, $message, $flags, $internalDate));
|
||||
|
||||
$code = $response->status->code;
|
||||
if ($code instanceof AppendUidCode) {
|
||||
return $code->uid;
|
||||
}
|
||||
|
||||
throw new RuntimeException('Unable to retrieve uid from append response');
|
||||
}
|
||||
|
||||
public function send(Command $command): Response
|
||||
{
|
||||
$this->imap->connect();
|
||||
|
||||
return $this->imap->send($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Criteria> $criteria
|
||||
* @return array<Message>
|
||||
*/
|
||||
public function doSearch(array $criteria, ?PreFetchOptions $preFetchOptions = null): array
|
||||
{
|
||||
$response = $this->send(
|
||||
new Protocol\Command\SearchCommand(
|
||||
$this->configuration->useUid,
|
||||
...$criteria
|
||||
)
|
||||
);
|
||||
|
||||
$ids = [];
|
||||
foreach ($response->data as $data) {
|
||||
if ($data instanceof SearchData) {
|
||||
array_push($ids, ...$data->numbers);
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (null !== $preFetchOptions) {
|
||||
$items = [];
|
||||
|
||||
if ($preFetchOptions->headers) {
|
||||
$items[] = 'BODY[HEADER]';
|
||||
}
|
||||
|
||||
if ($preFetchOptions->internalDate) {
|
||||
$items[] = 'INTERNALDATE';
|
||||
}
|
||||
|
||||
$preFetchResult = $this->send(new FetchCommand(
|
||||
$this->configuration->useUid,
|
||||
new SequenceSet(...$ids),
|
||||
$items,
|
||||
));
|
||||
|
||||
$messages = [];
|
||||
foreach ($preFetchResult->data as $data) {
|
||||
if ($data instanceof FetchData) {
|
||||
$id = $data->id;
|
||||
if ($this->configuration->useUid) {
|
||||
$id = $data->uid ?? throw new RuntimeException('Unable to get uid from message '.$id);
|
||||
}
|
||||
|
||||
$messages[] = new LazyMessage(
|
||||
$this,
|
||||
$id,
|
||||
$this->createHeaders($data),
|
||||
$data->internalDate,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $messages;
|
||||
}
|
||||
|
||||
return array_map(fn (int $id) => new LazyMessage($this, $id), $ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>|null
|
||||
*/
|
||||
private function createHeaders(FetchData $data): ?array
|
||||
{
|
||||
if (null === $headerSection = $data->getBodySection('HEADER')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return iconv_mime_decode_headers($headerSection->text, ICONV_MIME_DECODE_CONTINUE_ON_ERROR) ?: [];
|
||||
}
|
||||
|
||||
private function createMessagePart(int $id, string $section, BodyStructure\Part $part): Mime\Part\Part
|
||||
{
|
||||
if ($part instanceof BodyStructure\SinglePart) {
|
||||
return new SinglePart(
|
||||
$part->type,
|
||||
$part->subtype,
|
||||
$part->attributes,
|
||||
new LazyBody($this, $id, $section === '0' ? '1' : $section),
|
||||
$part->attributes['charset'] ?? 'utf-8',
|
||||
$part->encoding,
|
||||
null !== $part->disposition
|
||||
? new Disposition(
|
||||
$part->disposition->type,
|
||||
$part->disposition->attributes['filename'] ?? null
|
||||
) : null,
|
||||
);
|
||||
}
|
||||
|
||||
if (!$part instanceof BodyStructure\MultiPart) {
|
||||
throw new Exception('Unable to create message part from body structure part of class '.$part::class);
|
||||
}
|
||||
|
||||
$childParts = [];
|
||||
foreach ($part->parts as $index => $childPart) {
|
||||
$childIndex = (string) ($index + 1);
|
||||
$childSection = $section === '0' ? $childIndex : $section.'.'.$childIndex;
|
||||
$childParts[] = $this->createMessagePart($id, $childSection, $childPart);
|
||||
}
|
||||
|
||||
return new MultiPart($part->subtype, $part->attributes, $childParts);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user