generated from Nodarx/template
b02ca63208
Signed-off-by: Sebastian Krupinski <krupinski01@gmail.com>
198 lines
6.3 KiB
PHP
198 lines
6.3 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\Stores;
|
|
|
|
use KTXC\Db\DataStore;
|
|
use KTXM\ProviderImap\Providers\CollectionResource;
|
|
|
|
/**
|
|
* IMAP Mailbox Meta Store
|
|
*
|
|
* One MongoDB document per cached mailbox in `provider_imap_mail_mailboxes`,
|
|
* keyed by (sid, name). The document holds the mailbox itself and its
|
|
* harmonization state.
|
|
*/
|
|
class MailboxStore
|
|
{
|
|
protected const COLLECTION_NAME = 'provider_imap_mail_mailboxes';
|
|
|
|
/** Harmonization state fields and their defaults for a mailbox that was never harmonized */
|
|
public const STATE_DEFAULTS = [
|
|
'uidValidity' => null,
|
|
'uidNext' => null,
|
|
'highestModSeq' => null,
|
|
'changeSeq' => 0,
|
|
'harmonizedAt' => null,
|
|
'harmonizationComplete' => false,
|
|
];
|
|
|
|
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, 'name' => 1],
|
|
['name' => 'mailboxes_key', 'unique' => true]
|
|
),
|
|
$collection->createIndex(
|
|
['tid' => 1, 'sid' => 1],
|
|
['name' => 'mailboxes_by_tenant_service']
|
|
),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Insert or update the mailbox fields of a document.
|
|
*
|
|
* Harmonization state on an existing document is left untouched; a new document
|
|
* starts with the default (never harmonized) state.
|
|
*/
|
|
public function upsert(string $tenantId, string $serviceId, CollectionResource $collection): void
|
|
{
|
|
$document = $collection->toCacheMeta();
|
|
|
|
$this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
['sid' => $serviceId, 'name' => $document['name']],
|
|
[
|
|
'$set' => ['tid' => $tenantId, 'sid' => $serviceId, ...$document],
|
|
'$setOnInsert' => self::STATE_DEFAULTS,
|
|
],
|
|
['upsert' => true],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the document of a mailbox.
|
|
*/
|
|
public function fetch(string $serviceId, string $name): ?array
|
|
{
|
|
return $this->dataStore->selectCollection(self::COLLECTION_NAME)->findOne([
|
|
'sid' => $serviceId,
|
|
'name' => $name,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* List the documents of all mailboxes of a service.
|
|
*
|
|
* @return array<string, array> keyed by mailbox name
|
|
*/
|
|
public function list(string $serviceId): array
|
|
{
|
|
$cursor = $this->dataStore->selectCollection(self::COLLECTION_NAME)->find(['sid' => $serviceId]);
|
|
|
|
$list = [];
|
|
foreach ($cursor as $document) {
|
|
$list[(string) $document['name']] = $document;
|
|
}
|
|
return $list;
|
|
}
|
|
|
|
// ── Harmonization state ──────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Harmonization state of a mailbox; defaults when the mailbox is not cached.
|
|
*
|
|
* @return array{uidValidity: ?int, uidNext: ?int, highestModSeq: ?int, changeSeq: int, harmonizedAt: ?int, harmonizationComplete: bool}
|
|
*/
|
|
public function state(string $serviceId, string $name): array
|
|
{
|
|
$document = $this->fetch($serviceId, $name) ?? [];
|
|
|
|
return array_replace(self::STATE_DEFAULTS, array_intersect_key($document, self::STATE_DEFAULTS));
|
|
}
|
|
|
|
/**
|
|
* Update harmonization state fields of a mailbox; keys outside the state are ignored.
|
|
*/
|
|
public function updateState(string $serviceId, string $name, array $state): void
|
|
{
|
|
$state = array_intersect_key($state, self::STATE_DEFAULTS);
|
|
if ($state === []) {
|
|
return;
|
|
}
|
|
|
|
$this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
['sid' => $serviceId, 'name' => $name],
|
|
['$set' => $state],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Take the harmonization lock of a mailbox when it is free or expired.
|
|
*
|
|
* Never waits: returns false when another owner holds a valid lock or the
|
|
* mailbox is not cached.
|
|
*/
|
|
public function acquireLock(string $serviceId, string $name, string $owner, int $ttl): bool
|
|
{
|
|
$now = time();
|
|
|
|
$result = $this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
[
|
|
'sid' => $serviceId,
|
|
'name' => $name,
|
|
'$or' => [
|
|
['lock' => null],
|
|
['lock.expiresAt' => ['$lte' => $now]],
|
|
['lock.owner' => $owner],
|
|
],
|
|
],
|
|
['$set' => ['lock' => ['owner' => $owner, 'expiresAt' => $now + $ttl]]],
|
|
);
|
|
|
|
return $result->getMatchedCount() === 1;
|
|
}
|
|
|
|
/**
|
|
* Release the harmonization lock of a mailbox, if still held by the owner.
|
|
*/
|
|
public function releaseLock(string $serviceId, string $name, string $owner): void
|
|
{
|
|
$this->dataStore->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
['sid' => $serviceId, 'name' => $name, 'lock.owner' => $owner],
|
|
['$unset' => ['lock' => '']],
|
|
);
|
|
}
|
|
|
|
// ── Deletion ─────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Delete the document of a mailbox.
|
|
*/
|
|
public function delete(string $serviceId, string $name): void
|
|
{
|
|
$this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteOne([
|
|
'sid' => $serviceId,
|
|
'name' => $name,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Delete the documents of all mailboxes of a service.
|
|
*/
|
|
public function deleteByService(string $serviceId): void
|
|
{
|
|
$this->dataStore->selectCollection(self::COLLECTION_NAME)->deleteMany(['sid' => $serviceId]);
|
|
}
|
|
}
|