refactor: split service into ServiceBase and LiveService

Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
This commit is contained in:
2026-10-07 21:49:31 -04:00
parent eea9b5630a
commit 4e12d41ee9
18 changed files with 649 additions and 516 deletions
+6 -10
View File
@@ -10,10 +10,9 @@ declare(strict_types=1);
namespace KTXM\ProviderImap\Service\Cache;
use KTXM\ProviderImap\Client\Protocol\Command\Argument\FetchOptions;
use KTXM\ProviderImap\Providers\CollectionResource;
use KTXM\ProviderImap\Providers\EntityResource;
use KTXM\ProviderImap\Providers\MessageProperties;
use KTXM\ProviderImap\Providers\Service;
use KTXM\ProviderImap\Providers\ServiceBase;
use KTXM\ProviderImap\Service\Live\LiveMailService;
use KTXM\ProviderImap\Stores\MailboxStore;
use KTXM\ProviderImap\Stores\MessageFileStore;
@@ -35,7 +34,7 @@ class HarmonizationService
/** Lifetime of a mailbox harmonization lock in seconds; long enough for a large first run */
public const LOCK_TTL = 900;
private ?Service $service = null;
private ?ServiceBase $service = null;
private ?LiveMailService $live = null;
public function __construct(
@@ -48,7 +47,7 @@ class HarmonizationService
/**
* Bind a service; returns a new instance so the shared one stays unbound.
*/
public function for(Service $service, ?LiveMailService $live = null): static
public function for(ServiceBase $service, ?LiveMailService $live = null): static
{
$bound = clone $this;
$bound->service = $service;
@@ -72,10 +71,7 @@ class HarmonizationService
$tenantId = (string) $service->tenantIdentifier();
$serviceId = (string) $service->identifier();
$remote = [];
foreach ($live->collectionList() as $name => $mailbox) {
$remote[(string) $name] = (new CollectionResource($service->provider(), $serviceId))->fromImap($mailbox);
}
$remote = $live->collectionList();
$cached = $this->mailboxStore->list($serviceId);
$result = ['added' => [], 'updated' => [], 'removed' => []];
@@ -158,7 +154,7 @@ class HarmonizationService
try {
$state = $this->mailboxStore->state($serviceId, $mailbox);
$selected = $live->collectionFetch($mailbox)
$selected = $live->mailboxFetch($mailbox)
?? throw new RuntimeException("Mailbox not found on server: {$mailbox}");
$uidValidity = $selected->uidValidity()
?? throw new RuntimeException("Server did not report UIDVALIDITY for mailbox: {$mailbox}");
@@ -239,7 +235,7 @@ class HarmonizationService
$options = FetchOptions::message()->withBodyText(MessageIngestor::BODY_TEXT_LIMIT);
$ingested = [];
foreach ($live->entityFetch($mailbox, $options, ...$uids) as $message) {
foreach ($live->messageFetch($mailbox, $options, ...$uids) as $message) {
$entity = (new EntityResource($service->provider(), $serviceId))->fromImap($message, $mailbox);
array_push($ingested, ...$this->ingestor->ingest($tenantId, $serviceId, $uidValidity, $entity));
}
+98 -22
View File
@@ -33,6 +33,8 @@ use KTXM\ProviderImap\Client\Protocol\IdentifierMode;
use KTXM\ProviderImap\Client\ImapException;
use KTXM\ProviderImap\Client\Protocol\Command\Argument\ListReturnOptions;
use KTXM\ProviderImap\Client\Mailbox;
use KTXM\ProviderImap\Providers\CollectionResource;
use KTXM\ProviderImap\Providers\EntityResource;
use KTXM\ProviderImap\Client\Message;
use KTXM\ProviderImap\Client\MessageAddress;
use KTXM\ProviderImap\Client\MessagePart;
@@ -49,7 +51,7 @@ use KTXF\Resource\Range\RangeAnchorType;
use KTXF\Resource\Range\RangeTally;
use KTXF\Resource\Sort\ISort;
use KTXF\Resource\BinaryResource;
use KTXM\ProviderImap\Providers\Service;
use KTXM\ProviderImap\Providers\ServiceBase;
use KTXM\ProviderImap\Smtp\Client as SmtpClient;
use RuntimeException;
@@ -69,7 +71,7 @@ class LiveMailService
private ?Mailbox $selected = null;
public function __construct(
private readonly Service $service,
private readonly ServiceBase $service,
) {}
/**
@@ -140,11 +142,48 @@ class LiveMailService
}
/**
* list of collections in remote storage
*
* @since Release 1.0.0
*/
public function collectionList(?string $location = null, IFilter|null $filter = null, ISort|null $sort = null, string $depth = '*'): Generator
* List the collections of the account, keyed by mailbox name.
*
* @return array<string, CollectionResource>
*/
public function collectionList(?string $location = null, ?IFilter $filter = null, ?ISort $sort = null): array
{
$list = [];
foreach ($this->mailboxList($location, $filter, $sort) as $name => $mailbox) {
$list[(string) $name] = $this->collectionResource($mailbox);
}
return $list;
}
/**
* Fetch a single collection by its mailbox name; null when it does not exist.
*/
public function collectionFetch(string $identifier): ?CollectionResource
{
$mailbox = $this->mailboxFetch($identifier);
return $mailbox === null ? null : $this->collectionResource($mailbox);
}
/**
* Hierarchy delimiter of the account, from `LIST "" ""` (no mailboxes are listed); "/" when unknown.
*/
public function collectionDelimiter(): string
{
foreach ($this->imapClient()->perform(new ListCommand('', '')) as $mailbox) {
return $mailbox->delimiter() ?: '/';
}
return '/';
}
/**
* List the IMAP mailboxes of the account (with status), keyed by name.
*
* @return Generator<string, Mailbox>
*/
public function mailboxList(?string $location = null, IFilter|null $filter = null, ISort|null $sort = null, string $depth = '*'): Generator
{
// Prepare location filter
if (!empty($location)) {
@@ -196,11 +235,11 @@ class LiveMailService
}
/**
* Fetch a single mailbox by its full name.
* Fetch a single IMAP mailbox (with status) by its full name.
*
* Returns null when no mailbox matching $identifier is found.
*/
public function collectionFetch(string $identifier): ?Mailbox
public function mailboxFetch(string $identifier): ?Mailbox
{
// LIST-STATUS (RFC 5819) returns the status with the LIST response in one round trip
$listStatus = $this->imapClient()->hasCapability('LIST-STATUS');
@@ -225,38 +264,38 @@ class LiveMailService
}
/**
* Create a new IMAP mailbox and return it.
* Create a new IMAP mailbox and return it as a collection resource.
*
* If the server-side LIST cannot confirm the new mailbox (e.g., immediate
* consistency), a lightweight stub resource is returned instead.
*/
public function collectionCreate(string $name): Mailbox
public function collectionCreate(string $name, ?string $delimiter = null): CollectionResource
{
$this->imapClient()->perform(new CreateCommand($name));
// Attempt to refetch the new mailbox from the server
$mailbox = $this->collectionFetch($name);
$mailbox = $this->mailboxFetch($name);
if ($mailbox === null) {
throw new ImapException('Failed to create mailbox: ' . $name);
}
return $mailbox;
return $this->collectionResource($mailbox, $delimiter);
}
/**
* Rename a mailbox and return the updated resource.
*/
public function collectionRename(string $oldName, string $newName): Mailbox
public function collectionRename(string $oldName, string $newName, ?string $delimiter = null): CollectionResource
{
$this->imapClient()->perform(new RenameCommand($oldName, $newName));
$mailbox = $this->collectionFetch($newName);
$mailbox = $this->mailboxFetch($newName);
if ($mailbox === null) {
throw new ImapException('Failed to rename mailbox: ' . $oldName . ' to ' . $newName);
}
return $mailbox;
return $this->collectionResource($mailbox, $delimiter);
}
/**
@@ -354,11 +393,36 @@ class LiveMailService
}
/**
* Retrieve a list of messages in a mailbox matching the given filter, sorted and paginated as requested.
* List the entities of a mailbox matching the given filter, sorted and paginated as requested.
*
* @return Message[] list of messages matching the filter, sorted and paginated as requested
* @return Generator<int, EntityResource> keyed by UID
*/
public function entityList(string $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): Generator
{
foreach ($this->messageList($collection, $filter, $sort, $range) as $message) {
$entity = $this->entityResource($message, $collection);
yield (int) $entity->identifier() => $entity;
}
}
/**
* Fetch entities by UID (full TEXT).
*
* @return Generator<int, EntityResource> keyed by UID
*/
public function entityFetch(string $collection, int ...$uids): Generator
{
foreach ($this->messageFetch($collection, null, ...$uids) as $uid => $message) {
yield $uid => $this->entityResource($message, $collection);
}
}
/**
* List IMAP messages of a mailbox matching the given filter, sorted and paginated as requested.
*
* @return Generator<Message>
*/
public function messageList(string $collection, ?IFilter $filter = null, ?ISort $sort = null, ?IRange $range = null): Generator
{
// text parts normally precede attachments, so capping TEXT avoids transferring attachment bytes
$options = FetchOptions::message()->withBodyText(self::LIST_BODY_TEXT_LIMIT);
@@ -387,16 +451,16 @@ class LiveMailService
return [];
}
yield from $this->entityFetch($collection, $options, ...$uids);
yield from $this->messageFetch($collection, $options, ...$uids);
}
/**
* Fetch one or more messages by UID and return EntityResource objects.
* Fetch one or more messages by UID as IMAP messages.
*
* @param int ...$uids
* @return Message[] keyed by UID
* @return Generator<int, Message> keyed by UID
*/
public function entityFetch(string $collection, ?FetchOptions $options = null, int ...$uids): Generator
public function messageFetch(string $collection, ?FetchOptions $options = null, int ...$uids): Generator
{
if (empty($uids)) {
return [];
@@ -1181,6 +1245,18 @@ class LiveMailService
return $normalized;
}
private function collectionResource(Mailbox $mailbox, ?string $delimiter = null): CollectionResource
{
return (new CollectionResource($this->service->provider(), $this->service->identifier()))
->fromImap($mailbox, ['delimiter' => $delimiter]);
}
private function entityResource(Message $message, string $collection): EntityResource
{
return (new EntityResource($this->service->provider(), $this->service->identifier()))
->fromImap($message, $collection);
}
/**
* Select a mailbox, reusing the current selection when possible.
*