Files
provider_imap/lib/Service/Cache/CacheService.php
Sebastian Krupinski e51c65bf19 feat: initial version
Signed-off-by: Sebastian Krupinski <root@LAPTOP-7DVOR6NC>
2026-02-20 21:44:49 +00:00

194 lines
6.9 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace KTXM\ProviderImapMail\Service\Cache;
use KTXM\ProviderImapMail\Providers\CollectionResource;
use KTXM\ProviderImapMail\Providers\EntityResource;
use KTXM\ProviderImapMail\Service\Remote\RemoteMailService;
use KTXM\ProviderImapMail\Stores\MessageStore;
/**
* Cache Service — IMAP Sync Orchestrator
*
* Keeps the local MongoDB cache in sync with a remote IMAP server.
*
* Strategy (UID-based):
* 1. Fetch the complete remote UID set.
* 2. Compare against cached UIDs.
* 3. Fetch and store new UIDs; remove stale UIDs from the cache.
*
* This is always a full-range UID comparison. A CONDSTORE/HIGHESTMODSEQ
* strategy can be layered on later for servers that support RFC 7162.
*/
class CacheService
{
public function __construct(
private readonly RemoteMailService $remoteMailService,
private readonly MessageStore $messageStore,
private readonly string $provider,
private readonly string|int $service,
) {}
// ── Mailbox sync ──────────────────────────────────────────────────────────
/**
* Synchronise the mailbox list for this service.
*
* Inserts/updates every selectable mailbox returned by the remote server
* and removes cached mailboxes that no longer exist remotely.
*
* @return array{added: string[], removed: string[]}
*/
public function syncMailboxes(): array
{
$remoteCollections = $this->remoteMailService->collectionList();
$added = [];
$removed = [];
// Upsert every remote mailbox
foreach ($remoteCollections as $name => $resource) {
$doc = array_merge(
$resource->toStore(),
['sid' => (string) $this->service],
);
$this->messageStore->upsertMailbox($doc);
$added[] = $name;
}
// Remove cached mailboxes that no longer exist on the server
$cached = $this->messageStore->listMailboxes((string) $this->service);
foreach ($cached as $cachedDoc) {
$name = $cachedDoc['name'] ?? ($cachedDoc['identifier'] ?? null);
if ($name === null) {
continue;
}
if (!isset($remoteCollections[$name])) {
$this->messageStore->deleteMailbox((string) $this->service, $name);
$removed[] = $name;
}
}
return ['added' => $added, 'removed' => $removed];
}
// ── Message sync ──────────────────────────────────────────────────────────
/**
* Synchronise all messages in one mailbox.
*
* Fetches new UIDs from the remote server, stores them in MongoDB, and
* removes UIDs from the cache that no longer exist on the server.
*
* @return array{added: int[], removed: int[]}
*/
public function syncMessages(string $mailbox): array
{
$remoteUids = $this->remoteMailService->entityList($mailbox);
$cachedUids = $this->messageStore->listUids((string) $this->service, $mailbox);
$remoteSet = array_fill_keys($remoteUids, true);
$cachedSet = array_fill_keys($cachedUids, true);
$newUids = array_keys(array_diff_key($remoteSet, $cachedSet));
$removedUids = array_keys(array_diff_key($cachedSet, $remoteSet));
// Stream-fetch new messages one at a time and store each
foreach ($this->remoteMailService->entitySyncStream($mailbox, $newUids) as $uid => $resource) {
/** @var EntityResource $resource */
$doc = array_merge(
$resource->toStore(),
[
'sid' => (string) $this->service,
'mailbox' => $mailbox,
'uid' => $uid,
],
);
$this->messageStore->upsertMessage($doc);
}
// Purge stale UIDs from cache
if (!empty($removedUids)) {
$this->messageStore->deleteMessages((string) $this->service, $mailbox, $removedUids);
}
return [
'added' => $newUids,
'removed' => $removedUids,
];
}
/**
* Perform a full sync: mailboxes first, then all messages in each selectable mailbox.
*
* @return array{mailboxes: array, messages: array<string, array>}
*/
public function syncAll(): array
{
$mailboxResult = $this->syncMailboxes();
$messagesResults = [];
foreach ($this->remoteMailService->collectionList() as $name => $collection) {
try {
$messagesResults[$name] = $this->syncMessages($name);
} catch (\Throwable $e) {
$messagesResults[$name] = ['error' => $e->getMessage()];
}
}
return [
'mailboxes' => $mailboxResult,
'messages' => $messagesResults,
];
}
// ── Partial helpers ───────────────────────────────────────────────────────
/**
* Sync only the flags of already-cached messages in a mailbox.
*
* This is cheaper than a full message sync: it fetches FLAGS only from
* the remote server and updates cached documents in place.
*/
public function syncFlags(string $mailbox): void
{
$cachedUids = $this->messageStore->listUids((string) $this->service, $mailbox);
if (empty($cachedUids)) {
return;
}
// Fetch only FLAGS + UID from remote
$items = ['FLAGS', 'UID'];
foreach ($this->remoteMailService->entitySyncStream($mailbox, $cachedUids, $items) as $uid => $resource) {
/** @var EntityResource $resource */
$existing = $this->messageStore->fetchMessage((string) $this->service, $mailbox, $uid);
if ($existing === null) {
continue;
}
// Merge updated flags into the cached document
$existingProperties = $existing['properties'] ?? [];
$updatedProperties = $resource->toStore()['properties'] ?? [];
// Only overwrite flag-related fields
foreach (['read', 'flagged', 'answered', 'draft', 'deleted', 'junk'] as $field) {
if (isset($updatedProperties[$field])) {
$existingProperties[$field] = $updatedProperties[$field];
}
}
$existing['properties'] = $existingProperties;
$this->messageStore->upsertMessage($existing);
}
}
}