generated from Nodarx/template
feat: speed improvements
Signed-off-by: Sebastian Krupinski <root@LAPTOP-7DVOR6NC>
This commit is contained in:
@@ -11,40 +11,33 @@ namespace KTXM\ProviderImapMail\Service\Remote;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Generator;
|
||||
use Gricob\IMAP\Client;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Before;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Body;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Flagged;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\From;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Larger;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Seen;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Since;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Smaller;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Subject;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\To;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Unflagged;
|
||||
use Gricob\IMAP\Protocol\Command\Argument\Search\Unseen;
|
||||
use KTXF\Resource\Filter\IFilter;
|
||||
use KTXF\Resource\Range\IRange;
|
||||
use KTXF\Resource\Range\RangeAnchorType;
|
||||
use KTXF\Resource\Range\RangeTally;
|
||||
use KTXM\ProviderImapMail\Providers\CollectionResource;
|
||||
use KTXM\ProviderImapMail\Providers\EntityResource;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\BodyCriteria;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\FlaggedCriteria;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\FromCriteria;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\LargerCriteria;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\SeenCriteria;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\SmallerCriteria;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\SubjectCriteria;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\ToCriteria;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\UnflaggedCriteria;
|
||||
use KTXM\ProviderImapMail\Service\Remote\Command\UnseenCriteria;
|
||||
|
||||
/**
|
||||
* IMAP Remote Mail Service
|
||||
*
|
||||
* Provides collection (mailbox) and entity (message) operations against a live
|
||||
* IMAP server via ImapClientWrapper. All methods are stateless — no caching or
|
||||
* local storage happens here.
|
||||
*/
|
||||
class RemoteMailService
|
||||
{
|
||||
/**
|
||||
* Default IMAP FETCH data items used for message hydration.
|
||||
*
|
||||
* RFC 822 size, flags, arrival date, envelope headers, and the BODYSTRUCTURE
|
||||
* MIME tree give us everything needed to build an EntityResource without
|
||||
* downloading the full message body.
|
||||
* Default IMAP FETCH data items used for message hydration
|
||||
*/
|
||||
private const DEFAULT_FETCH_ITEMS = [
|
||||
'FLAGS',
|
||||
@@ -57,13 +50,11 @@ class RemoteMailService
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly ImapClientWrapper $client,
|
||||
private readonly Client $client,
|
||||
private readonly string $provider,
|
||||
private readonly string|int $service,
|
||||
) {}
|
||||
|
||||
// ── Collection (mailbox) operations ──────────────────────────────────────
|
||||
|
||||
/**
|
||||
* List all selectable mailboxes on the server.
|
||||
*
|
||||
@@ -165,23 +156,22 @@ class RemoteMailService
|
||||
*/
|
||||
public function entityList(string $collection, ?IFilter $filter = null, ?IRange $range = null): array
|
||||
{
|
||||
// ── Build IMAP SEARCH criteria from filter ────────────────────────────
|
||||
$criteria = [];
|
||||
if ($filter !== null) {
|
||||
foreach ($filter->conditions() as $condition) {
|
||||
$attribute = $condition['attribute'];
|
||||
$value = $condition['value'];
|
||||
$criterion = match ($attribute) {
|
||||
'seen' => $value ? new SeenCriteria() : new UnseenCriteria(),
|
||||
'flagged' => $value ? new FlaggedCriteria() : new UnflaggedCriteria(),
|
||||
'from' => new FromCriteria($value),
|
||||
'to' => new ToCriteria($value),
|
||||
'subject' => new SubjectCriteria($value),
|
||||
'body' => new BodyCriteria($value),
|
||||
'seen' => $value ? new Seen() : new Unseen(),
|
||||
'flagged' => $value ? new Flagged() : new Unflagged(),
|
||||
'from' => new From($value),
|
||||
'to' => new To($value),
|
||||
'subject' => new Subject($value),
|
||||
'body' => new Body($value),
|
||||
'before' => new Before(new DateTimeImmutable($value)),
|
||||
'after' => new Since(new DateTimeImmutable($value)),
|
||||
'min' => new LargerCriteria($value),
|
||||
'max' => new SmallerCriteria($value),
|
||||
'min' => new Larger($value),
|
||||
'max' => new Smaller($value),
|
||||
default => null,
|
||||
};
|
||||
if ($criterion !== null) {
|
||||
@@ -190,19 +180,14 @@ class RemoteMailService
|
||||
}
|
||||
}
|
||||
|
||||
// ── Execute IMAP SEARCH (ALL when no criteria) ────────────────────────
|
||||
$uids = empty($criteria)
|
||||
? $this->client->searchAll($collection)
|
||||
: $this->client->searchMessages($collection, $criteria);
|
||||
$uids = $this->client->searchMessages($collection, $criteria);
|
||||
|
||||
if (empty($uids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// ── Sort descending: highest UID (newest) first ───────────────────────
|
||||
rsort($uids);
|
||||
|
||||
// ── Apply RangeTally pagination ───────────────────────────────────────
|
||||
if ($range instanceof RangeTally) {
|
||||
$position = (int) $range->getPosition();
|
||||
$tally = $range->getTally();
|
||||
@@ -223,11 +208,6 @@ class RemoteMailService
|
||||
/**
|
||||
* Fetch one or more messages by UID and return EntityResource objects.
|
||||
*
|
||||
* Uses client->fetchMultiple() which streams FetchData responses one at a
|
||||
* time via sendStreaming — memory-efficient even for large UID sets. Body
|
||||
* content is NOT pre-loaded; call fetchBody() on the returned resource
|
||||
* when the decoded body is needed (lazy, one extra round-trip per message).
|
||||
*
|
||||
* @param int ...$uids
|
||||
* @return EntityResource[] keyed by UID
|
||||
*/
|
||||
@@ -237,10 +217,11 @@ class RemoteMailService
|
||||
return [];
|
||||
}
|
||||
|
||||
$this->client->select($collection);
|
||||
$result = [];
|
||||
foreach ($this->client->fetchMultiple($collection, array_values($uids), self::DEFAULT_FETCH_ITEMS) as $uid => $fetchData) {
|
||||
foreach ($this->client->streamByUids(array_values($uids), self::DEFAULT_FETCH_ITEMS) as $uid => $fetchData) {
|
||||
$resource = new EntityResource($this->provider, $this->service);
|
||||
$resource->fromImap($fetchData, $collection, null);
|
||||
$resource->fromImap($fetchData, $collection);
|
||||
$result[$uid] = $resource;
|
||||
}
|
||||
return $result;
|
||||
@@ -252,9 +233,50 @@ class RemoteMailService
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->client->fetchMultiple($collection, array_values($uids), self::DEFAULT_FETCH_ITEMS) as $uid => $fetchData) {
|
||||
$this->client->select($collection);
|
||||
foreach ($this->client->streamByUids(array_values($uids), self::DEFAULT_FETCH_ITEMS) as $uid => $fetchData) {
|
||||
$resource = new EntityResource($this->provider, $this->service);
|
||||
$resource->fromImap($fetchData, $collection, null);
|
||||
$resource->fromImap($fetchData, $collection);
|
||||
yield $uid => $resource;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch every message in a mailbox using a single FETCH 1:* command and
|
||||
* return all EntityResource objects as an array keyed by UID.
|
||||
*
|
||||
* Use this for unfiltered, unpaginated listing where a two-round-trip
|
||||
* SEARCH-then-FETCH approach would be wasteful.
|
||||
*
|
||||
* @param string[] $items IMAP fetch data items
|
||||
* @return EntityResource[] keyed by UID
|
||||
*/
|
||||
public function entityFetchAll(string $collection, array $items = self::DEFAULT_FETCH_ITEMS): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($this->client->streamAll($collection, $items) as $uid => $fetchData) {
|
||||
$resource = new EntityResource($this->provider, $this->service);
|
||||
$resource->fromImap($fetchData, $collection);
|
||||
$result[$uid] = $resource;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream every message in a mailbox using FETCH 1:*, yielding
|
||||
* uid => EntityResource as each FETCH response arrives off the socket.
|
||||
*
|
||||
* Use this for unfiltered streaming where a SEARCH ALL round-trip would be
|
||||
* an unnecessary extra RTT.
|
||||
*
|
||||
* @param string[] $items IMAP fetch data items
|
||||
* @return Generator<int, EntityResource>
|
||||
*/
|
||||
public function entityFetchAllStream(string $collection, array $items = self::DEFAULT_FETCH_ITEMS): Generator
|
||||
{
|
||||
foreach ($this->client->streamAll($collection, $items) as $uid => $fetchData) {
|
||||
$resource = new EntityResource($this->provider, $this->service);
|
||||
$resource->fromImap($fetchData, $collection);
|
||||
yield $uid => $resource;
|
||||
}
|
||||
}
|
||||
@@ -272,11 +294,12 @@ class RemoteMailService
|
||||
* @param string[] $items IMAP fetch data items
|
||||
* @return \Generator<int, EntityResource>
|
||||
*/
|
||||
public function entitySyncStream(string $collection, array $uids, array $items = self::DEFAULT_FETCH_ITEMS): \Generator
|
||||
public function entitySyncStream(string $collection, array $uids, array $items = self::DEFAULT_FETCH_ITEMS): Generator
|
||||
{
|
||||
foreach ($this->client->fetchMultiple($collection, $uids, $items) as $uid => $fetchData) {
|
||||
$this->client->select($collection);
|
||||
foreach ($this->client->streamByUids($uids, $items) as $uid => $fetchData) {
|
||||
$resource = new EntityResource($this->provider, $this->service);
|
||||
$resource->fromImap($fetchData, $collection, null);
|
||||
$resource->fromImap($fetchData, $collection);
|
||||
yield $uid => $resource;
|
||||
}
|
||||
}
|
||||
@@ -288,7 +311,7 @@ class RemoteMailService
|
||||
*/
|
||||
public function entityCreate(string $collection, string $rawMessage, array $flags = []): int
|
||||
{
|
||||
return $this->client->append($rawMessage, $collection, $flags);
|
||||
return $this->client->append($rawMessage, $collection, !empty($flags) ? $flags : null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -331,45 +354,4 @@ class RemoteMailService
|
||||
$this->client->copyMessages($collection, array_values($uids), $destination);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Compact a flat array of UIDs into an IMAP sequence-set string.
|
||||
*
|
||||
* Consecutive UIDs are collapsed into n:m ranges; non-consecutive UIDs are
|
||||
* comma-separated. The input does not need to be sorted.
|
||||
*
|
||||
* Examples:
|
||||
* [1, 2, 3, 5, 6, 10] → "1:3,5:6,10"
|
||||
* [42] → "42"
|
||||
* [7, 3, 4, 5] → "3:5,7"
|
||||
*
|
||||
* @param int[] $uids
|
||||
*/
|
||||
private function uidsToRangeSet(array $uids): string
|
||||
{
|
||||
if (empty($uids)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$uids = array_unique($uids);
|
||||
sort($uids);
|
||||
|
||||
$ranges = [];
|
||||
$start = $end = $uids[0];
|
||||
|
||||
for ($i = 1, $count = count($uids); $i <= $count; $i++) {
|
||||
$current = $uids[$i] ?? null;
|
||||
if ($current !== null && $current === $end + 1) {
|
||||
$end = $current;
|
||||
} else {
|
||||
$ranges[] = $start === $end ? (string) $start : $start . ':' . $end;
|
||||
if ($current !== null) {
|
||||
$start = $end = $current;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return implode(',', $ranges);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user