* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderImap\Stores; use KTXC\Db\DataStore; use KTXM\ProviderImap\Providers\EntityResource; /** * IMAP Message Meta Store * * One MongoDB document per cached message in `provider_imap_mail_messages`, * keyed by (sid, mailbox, uidValidity, uid). Holds only what list, filter and * sort need; message content lives in the content store (message.json). */ class MessageStore { protected const COLLECTION_NAME = 'provider_imap_mail_messages'; public function __construct( protected readonly DataStore $dataStore, ) {} /** * Create the collection indexes. * * MongoDB createIndex is idempotent when the name and specification match. * * @return string[] */ public function ensureIndexes(): array { $collection = $this->dataStore->selectCollection(self::COLLECTION_NAME); return [ $collection->createIndex( ['sid' => 1, 'mailbox' => 1, 'uidValidity' => 1, 'uid' => 1], ['name' => 'messages_key', 'unique' => true] ), $collection->createIndex( ['sid' => 1, 'mailbox' => 1, 'received' => -1], ['name' => 'messages_by_received'] ), $collection->createIndex( ['sid' => 1, 'mailbox' => 1, 'sent' => -1], ['name' => 'messages_by_sent'] ), $collection->createIndex( ['sid' => 1, 'mailbox' => 1, 'flags' => 1], ['name' => 'messages_by_flags'] ), $collection->createIndex( ['sid' => 1, 'urid' => 1], ['name' => 'messages_by_urid'] ), $collection->createIndex( ['tid' => 1, 'sid' => 1], ['name' => 'messages_by_tenant_service'] ), ]; } /** * Insert or replace the meta document of a message. * * Fields owned by other writers (e.g. blobs) are left untouched. */ public function upsert(string $tenantId, string $serviceId, int $uidValidity, EntityResource $entity): void { $document = $entity->toCacheMeta(); $key = [ 'sid' => $serviceId, 'mailbox' => $document['mailbox'], 'uidValidity' => $uidValidity, 'uid' => $document['uid'], ]; $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne( $key, ['$set' => ['tid' => $tenantId, ...$key, ...$document]], ['upsert' => true], ); } /** * Retrieve the meta document of a message. */ public function fetch(string $serviceId, string $mailbox, int $uidValidity, int $uid): ?array { return $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne([ 'sid' => $serviceId, 'mailbox' => $mailbox, 'uidValidity' => $uidValidity, 'uid' => $uid, ]); } /** * Retrieve the meta documents of several messages. * * @return array keyed by UID; UIDs that are not cached are absent */ public function fetchMany(string $serviceId, string $mailbox, int $uidValidity, int ...$uids): array { if ($uids === []) { return []; } $cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find([ 'sid' => $serviceId, 'mailbox' => $mailbox, 'uidValidity' => $uidValidity, 'uid' => ['$in' => array_values($uids)], ]); $list = []; foreach ($cursor as $document) { $list[(int) $document['uid']] = $document; } return $list; } /** * List the UIDs cached for a mailbox. * * @return int[] */ public function uids(string $serviceId, string $mailbox, int $uidValidity): array { $cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find( ['sid' => $serviceId, 'mailbox' => $mailbox, 'uidValidity' => $uidValidity], ['projection' => ['uid' => 1]], ); $uids = []; foreach ($cursor as $document) { $uids[] = (int) $document['uid']; } return $uids; } /** * List the flags of every message cached for a mailbox. * * @return array> set flags keyed by UID */ public function flags(string $serviceId, string $mailbox, int $uidValidity): array { $cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find( ['sid' => $serviceId, 'mailbox' => $mailbox, 'uidValidity' => $uidValidity], ['projection' => ['uid' => 1, 'flags' => 1]], ); $flags = []; foreach ($cursor as $document) { $flags[(int) $document['uid']] = array_values(array_map('strval', (array) ($document['flags'] ?? []))); } return $flags; } /** * Replace the flags of a message. * * @param string[] $flags set flags, e.g. ['seen', 'flagged'] */ public function updateFlags(string $serviceId, string $mailbox, int $uidValidity, int $uid, array $flags): void { $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne( ['sid' => $serviceId, 'mailbox' => $mailbox, 'uidValidity' => $uidValidity, 'uid' => $uid], ['$set' => ['flags' => array_values($flags)]], ); } /** * Delete the meta documents of messages. */ public function delete(string $serviceId, string $mailbox, int $uidValidity, int ...$uids): void { if ($uids === []) { return; } $this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteMany([ 'sid' => $serviceId, 'mailbox' => $mailbox, 'uidValidity' => $uidValidity, 'uid' => ['$in' => array_values($uids)], ]); } /** * Delete all meta documents of a mailbox (any UIDVALIDITY). */ public function deleteByMailbox(string $serviceId, string $mailbox): void { $this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteMany([ 'sid' => $serviceId, 'mailbox' => $mailbox, ]); } /** * Delete all meta documents of a service. */ public function deleteByService(string $serviceId): void { $this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteMany(['sid' => $serviceId]); } }