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:
115
lib/Service/Cache/CacheMailService.php
Normal file
115
lib/Service/Cache/CacheMailService.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?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\Stores\MessageStore;
|
||||
|
||||
/**
|
||||
* Cache Mail Service
|
||||
*
|
||||
* Provides read-only access to locally cached IMAP data stored in MongoDB by
|
||||
* CacheService. Call CacheService::syncMailboxes() / syncMessages() to keep
|
||||
* the cache up to date before reading from here.
|
||||
*/
|
||||
class CacheMailService
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MessageStore $messageStore,
|
||||
private readonly string $provider,
|
||||
private readonly string|int $service,
|
||||
) {}
|
||||
|
||||
// ── Collection (mailbox) reads ────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return all cached mailboxes for this service.
|
||||
*
|
||||
* @return CollectionResource[] keyed by mailbox name
|
||||
*/
|
||||
public function collectionList(): array
|
||||
{
|
||||
$docs = $this->messageStore->listMailboxes((string) $this->service);
|
||||
$result = [];
|
||||
foreach ($docs as $doc) {
|
||||
$resource = new CollectionResource($this->provider, $this->service);
|
||||
$resource->fromStore($doc);
|
||||
$result[$resource->identifier()] = $resource;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single cached mailbox by name.
|
||||
*/
|
||||
public function collectionFetch(string $name): ?CollectionResource
|
||||
{
|
||||
$doc = $this->messageStore->fetchMailbox((string) $this->service, $name);
|
||||
if ($doc === null) {
|
||||
return null;
|
||||
}
|
||||
$resource = new CollectionResource($this->provider, $this->service);
|
||||
$resource->fromStore($doc);
|
||||
return $resource;
|
||||
}
|
||||
|
||||
// ── Entity (message) reads ────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Return all cached UIDs for a mailbox.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function entityList(string $collection): array
|
||||
{
|
||||
return $this->messageStore->listUids((string) $this->service, $collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch one or more cached messages by UID.
|
||||
*
|
||||
* @param int ...$uids
|
||||
* @return EntityResource[] keyed by UID
|
||||
*/
|
||||
public function entityFetch(string $collection, int ...$uids): array
|
||||
{
|
||||
if (empty($uids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$docs = $this->messageStore->fetchMessages(
|
||||
(string) $this->service,
|
||||
$collection,
|
||||
array_values($uids),
|
||||
);
|
||||
$result = [];
|
||||
foreach ($docs as $uid => $doc) {
|
||||
$resource = new EntityResource($this->provider, $this->service);
|
||||
$resource->fromStore($doc);
|
||||
$result[$uid] = $resource;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single cached message.
|
||||
*/
|
||||
public function entityFetchOne(string $collection, int $uid): ?EntityResource
|
||||
{
|
||||
$doc = $this->messageStore->fetchMessage((string) $this->service, $collection, $uid);
|
||||
if ($doc === null) {
|
||||
return null;
|
||||
}
|
||||
$resource = new EntityResource($this->provider, $this->service);
|
||||
$resource->fromStore($doc);
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
193
lib/Service/Cache/CacheService.php
Normal file
193
lib/Service/Cache/CacheService.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user