generated from Nodarx/template
194 lines
6.8 KiB
PHP
194 lines
6.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
*/
|
|
|
|
namespace KTXM\ProviderImap\Service\Cache;
|
|
|
|
use KTXM\ProviderImap\Providers\CollectionResource;
|
|
use KTXM\ProviderImap\Providers\EntityResource;
|
|
use KTXM\ProviderImap\Service\Remote\RemoteMailService;
|
|
use KTXM\ProviderImap\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);
|
|
}
|
|
}
|
|
}
|