* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderImapMail\Stores; use KTXC\Db\DataStore; /** * MongoDB cache for IMAP messages and mailboxes. * * Collections: * provider_imap_mail_messages — one document per (sid, mailbox, uid) * provider_imap_mail_mailboxes — one document per (sid, name) * * Documents are stored **pre-formatted** in the same internal shape used by * EntityResource / MessageProperties and CollectionResource / CollectionProperties * so the read path does zero IMAP parsing. */ class MessageStore { protected const MESSAGES_COLLECTION = 'provider_imap_mail_messages'; protected const MAILBOXES_COLLECTION = 'provider_imap_mail_mailboxes'; public function __construct( protected readonly DataStore $store, ) { $this->ensureIndexes(); } // ── Index creation ─────────────────────────────────────────────────────── private function ensureIndexes(): void { $messages = $this->store->selectCollection(self::MESSAGES_COLLECTION); $mailboxes = $this->store->selectCollection(self::MAILBOXES_COLLECTION); $messages->createIndex(['sid' => 1, 'mailbox' => 1, 'uid' => 1], ['unique' => true]); $mailboxes->createIndex(['sid' => 1, 'name' => 1], ['unique' => true]); } // ── Messages ───────────────────────────────────────────────────────────── /** * Insert or replace a cached message document. * * @param array $data Result of EntityResource::toStore() — must include * sid, mailbox, uid at the top level. */ public function upsertMessage(array $data): void { $filter = [ 'sid' => $data['sid'], 'mailbox' => $data['mailbox'], 'uid' => (int)$data['uid'], ]; $data['syncedAt'] = (new \DateTime())->format(\DateTimeInterface::ATOM); $this->store->selectCollection(self::MESSAGES_COLLECTION)->updateOne( $filter, ['$set' => $data], ['upsert' => true], ); } /** * Retrieve a single cached message by service ID, mailbox and UID. */ public function fetchMessage(string $serviceId, string $mailbox, int $uid): ?array { $doc = $this->store->selectCollection(self::MESSAGES_COLLECTION)->findOne([ 'sid' => $serviceId, 'mailbox' => $mailbox, 'uid' => $uid, ]); return $doc ? (array)$doc : null; } /** * Return all cached UIDs for a mailbox/service combination. * * @return int[] */ public function listUids(string $serviceId, string $mailbox): array { $cursor = $this->store->selectCollection(self::MESSAGES_COLLECTION)->find( ['sid' => $serviceId, 'mailbox' => $mailbox], ['projection' => ['uid' => 1]], ); $uids = []; foreach ($cursor as $doc) { $uids[] = (int)$doc['uid']; } return $uids; } /** * Fetch multiple cached messages by UID. * * @param int[] $uids * @return array[] */ public function fetchMessages(string $serviceId, string $mailbox, array $uids): array { $cursor = $this->store->selectCollection(self::MESSAGES_COLLECTION)->find([ 'sid' => $serviceId, 'mailbox' => $mailbox, 'uid' => ['$in' => $uids], ]); $result = []; foreach ($cursor as $doc) { $doc = (array)$doc; $result[$doc['uid']] = $doc; } return $result; } /** * Delete a cached message entry by UID. */ public function deleteMessage(string $serviceId, string $mailbox, int $uid): void { $this->store->selectCollection(self::MESSAGES_COLLECTION)->deleteOne([ 'sid' => $serviceId, 'mailbox' => $mailbox, 'uid' => $uid, ]); } /** * Delete multiple cached message entries. * * @param int[] $uids */ public function deleteMessages(string $serviceId, string $mailbox, array $uids): void { if (empty($uids)) { return; } $this->store->selectCollection(self::MESSAGES_COLLECTION)->deleteMany([ 'sid' => $serviceId, 'mailbox' => $mailbox, 'uid' => ['$in' => $uids], ]); } // ── Mailboxes ──────────────────────────────────────────────────────────── /** * Insert or replace a cached mailbox document. * * @param array $data Result of CollectionResource::toStore() — must include * sid and name at the top level. */ public function upsertMailbox(array $data): void { $filter = [ 'sid' => $data['sid'], 'name' => $data['name'], ]; $data['syncedAt'] = (new \DateTime())->format(\DateTimeInterface::ATOM); $this->store->selectCollection(self::MAILBOXES_COLLECTION)->updateOne( $filter, ['$set' => $data], ['upsert' => true], ); } /** * Retrieve a cached mailbox by service ID and name. */ public function fetchMailbox(string $serviceId, string $name): ?array { $doc = $this->store->selectCollection(self::MAILBOXES_COLLECTION)->findOne([ 'sid' => $serviceId, 'name' => $name, ]); return $doc ? (array)$doc : null; } /** * List all cached mailboxes for a service. * * @return array[] */ public function listMailboxes(string $serviceId): array { $cursor = $this->store->selectCollection(self::MAILBOXES_COLLECTION)->find( ['sid' => $serviceId], ); $result = []; foreach ($cursor as $doc) { $doc = (array)$doc; $result[$doc['name']] = $doc; } return $result; } /** * Delete a cached mailbox entry by service ID and name. */ public function deleteMailbox(string $serviceId, string $name): void { $this->store->selectCollection(self::MAILBOXES_COLLECTION)->deleteOne([ 'sid' => $serviceId, 'name' => $name, ]); } }