* SPDX-License-Identifier: AGPL-3.0-or-later */ namespace KTXM\ProviderImap\Stores; use KTXC\Db\DataStore; use KTXF\Security\Crypto; use KTXF\Utile\UUID; use KTXM\ProviderImap\Providers\Service; /** * IMAP Service Store * * Persists IMAP service configurations (one MongoDB document per account / * user pairing) in the collection `provider_imap_mail_services`. */ class ServiceStore { protected const COLLECTION_NAME = 'provider_imap_mail_services'; public function __construct( protected readonly DataStore $dataStore, protected readonly Crypto $crypto, ) {} // ── List ───────────────────────────────────────────────────────────────── /** * List services for a tenant+user, optionally filtered to specific IDs. * * @param string[]|null $filter Service IDs to restrict results to * @return array Keyed by service ID */ public function list(string $tenantId, string $userId, ?array $filter = null): array { $condition = ['tid' => $tenantId, 'uid' => $userId]; if ($filter !== null && !empty($filter)) { $condition['sid'] = ['$in' => $filter]; } $cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find($condition); $list = []; foreach ($cursor as $entry) { if (isset($entry['identity']['secret'])) { $entry['identity']['secret'] = $this->crypto->decrypt($entry['identity']['secret']); } $list[$entry['sid']] = $entry; } return $list; } // ── Extant ─────────────────────────────────────────────────────────────── /** * Check which of the supplied service IDs exist for the given tenant/user. * * @param string[]|int[] $identifiers * @return array */ public function extant(string $tenantId, string $userId, array $identifiers): array { if (empty($identifiers)) { return []; } $cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find( [ 'tid' => $tenantId, 'uid' => $userId, 'sid' => ['$in' => array_map('strval', $identifiers)], ], ['projection' => ['sid' => 1]], ); $existing = []; foreach ($cursor as $doc) { $existing[] = $doc['sid']; } $result = []; foreach ($identifiers as $id) { $result[(string)$id] = in_array((string)$id, $existing, true); } return $result; } // ── Fetch ──────────────────────────────────────────────────────────────── public function fetch(string $tenantId, string $userId, string|int $serviceId): ?Service { $document = $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne([ 'tid' => $tenantId, 'uid' => $userId, 'sid' => (string)$serviceId, ]); if (!$document) { return null; } if (isset($document['identity']['secret'])) { $document['identity']['secret'] = $this->crypto->decrypt($document['identity']['secret']); } return (new Service())->fromStore($document); } // ── Create ─────────────────────────────────────────────────────────────── public function create(string $tenantId, string $userId, Service $service): Service { $document = $service->toStore(); $document['tid'] = $tenantId; $document['uid'] = $userId; $document['sid'] = UUID::v4(); $document['createdOn'] = new \MongoDB\BSON\UTCDateTime(); $document['modifiedOn'] = new \MongoDB\BSON\UTCDateTime(); if (isset($document['identity']['secret'])) { $document['identity']['secret'] = $this->crypto->encrypt($document['identity']['secret']); } $this->dataStore->selectCollection(self::COLLECTION_NAME)->insertOne($document); return (new Service())->fromStore($document); } // ── Modify ─────────────────────────────────────────────────────────────── public function modify(string $tenantId, string $userId, Service $service): Service { $serviceId = $service->identifier(); if (empty($serviceId)) { throw new \InvalidArgumentException('Service ID is required for update'); } $document = $service->toStore(); $document['modifiedOn'] = new \MongoDB\BSON\UTCDateTime(); if (isset($document['identity']['secret'])) { $document['identity']['secret'] = $this->crypto->encrypt($document['identity']['secret']); } unset($document['sid'], $document['tid'], $document['uid'], $document['createdOn']); $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne( ['tid' => $tenantId, 'uid' => $userId, 'sid' => (string)$serviceId], ['$set' => $document], ); return (new Service())->fromStore($document); } // ── Delete ─────────────────────────────────────────────────────────────── public function delete(string $tenantId, string $userId, string|int $serviceId): bool { $result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteOne([ 'tid' => $tenantId, 'uid' => $userId, 'sid' => (string)$serviceId, ]); return $result->getDeletedCount() > 0; } }