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:
224
lib/Stores/MessageStore.php
Normal file
224
lib/Stores/MessageStore.php
Normal file
@@ -0,0 +1,224 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
176
lib/Stores/ServiceStore.php
Normal file
176
lib/Stores/ServiceStore.php
Normal file
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: Sebastian Krupinski <krupinski01@gmail.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace KTXM\ProviderImapMail\Stores;
|
||||
|
||||
use KTXC\Db\DataStore;
|
||||
use KTXF\Security\Crypto;
|
||||
use KTXF\Utile\UUID;
|
||||
use KTXM\ProviderImapMail\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<string, 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<string, bool>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user