f7cloud_client/apps/mail/lib/IMAP/Sync/Synchronizer.php
root 8b6a0139db f7cloud_client
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 22:59:26 +00:00

195 lines
6.8 KiB
PHP

<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2017 F7cloud GmbH and F7cloud contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
namespace OCA\Mail\IMAP\Sync;
use Horde_Imap_Client;
use Horde_Imap_Client_Base;
use Horde_Imap_Client_Exception;
use Horde_Imap_Client_Exception_Sync;
use Horde_Imap_Client_Ids;
use Horde_Imap_Client_Mailbox;
use OCA\Mail\Exception\MailboxDoesNotSupportModSequencesException;
use OCA\Mail\Exception\UidValidityChangedException;
use OCA\Mail\IMAP\MessageMapper;
use function array_merge;
use function OCA\Mail\chunk_uid_sequence;
class Synchronizer {
/**
* This determines how many UIDs we send to IMAP for a check of changed or
* vanished messages. The number needs a balance between good performance
* (few chunks) and staying below the IMAP command size limits. 15k has
* shown to cause IMAP errors for some accounts where the UID list can't be
* compressed much by Horde.
*/
private const UID_CHUNK_MAX_BYTES = 10000;
/** @var MessageMapper */
private $messageMapper;
private ?string $requestId = null;
private ?Response $response = null;
public function __construct(MessageMapper $messageMapper) {
$this->messageMapper = $messageMapper;
}
/**
* @param Horde_Imap_Client_Base $imapClient
* @param Request $request
* @param int $criteria
*
* @return Response
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_Sync
* @throws UidValidityChangedException
* @throws MailboxDoesNotSupportModSequencesException
*/
public function sync(Horde_Imap_Client_Base $imapClient,
Request $request,
string $userId,
bool $hasQresync, // TODO: query client directly, but could be unsafe because login has to happen prior
int $criteria = Horde_Imap_Client::SYNC_NEWMSGSUIDS | Horde_Imap_Client::SYNC_FLAGSUIDS | Horde_Imap_Client::SYNC_VANISHEDUIDS): Response {
// Return cached response from last full sync when QRESYNC is enabled
if ($hasQresync && $this->response !== null && $request->getId() === $this->requestId) {
return $this->response;
}
$mailbox = new Horde_Imap_Client_Mailbox($request->getMailbox());
try {
// Do a full sync and cache the response when QRESYNC is enabled
[$newUids, $changedUids, $vanishedUids] = match ($hasQresync) {
true => $this->doCombinedSync($imapClient, $mailbox, $request),
false => $this->doSplitSync($imapClient, $mailbox, $request, $criteria),
};
} catch (Horde_Imap_Client_Exception_Sync $e) {
if ($e->getCode() === Horde_Imap_Client_Exception_Sync::UIDVALIDITY_CHANGED) {
throw new UidValidityChangedException();
}
throw $e;
} catch (Horde_Imap_Client_Exception $e) {
if ($e->getCode() === Horde_Imap_Client_Exception::MBOXNOMODSEQ) {
throw new MailboxDoesNotSupportModSequencesException($e->getMessage(), $e->getCode(), $e);
}
throw $e;
}
$newMessages = $this->messageMapper->findByIds($imapClient, $request->getMailbox(), $newUids, $userId);
$changedMessages = $this->messageMapper->findByIds($imapClient, $request->getMailbox(), $changedUids, $userId);
$vanishedMessageUids = $vanishedUids;
$this->requestId = $request->getId();
$this->response = new Response($newMessages, $changedMessages, $vanishedMessageUids, null);
return $this->response;
}
/**
* @psalm-return list{int[], int[], int[]} [$newUids, $changedUids, $vanishedUids]
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_Sync
*/
private function doCombinedSync(Horde_Imap_Client_Base $imapClient, Horde_Imap_Client_Mailbox $mailbox, Request $request): array {
$syncData = $imapClient->sync($mailbox, $request->getToken(), [
'criteria' => Horde_Imap_Client::SYNC_ALL,
]);
return [
$syncData->newmsgsuids->ids,
$syncData->flagsuids->ids,
$syncData->vanisheduids->ids,
];
}
/**
* @psalm-return list{int[], int[], int[]} [$newUids, $changedUids, $vanishedUids]
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_Sync
*/
private function doSplitSync(Horde_Imap_Client_Base $imapClient, Horde_Imap_Client_Mailbox $mailbox, Request $request, int $criteria): array {
if ($criteria & Horde_Imap_Client::SYNC_NEWMSGSUIDS) {
$newUids = $this->getNewMessageUids($imapClient, $mailbox, $request);
} else {
$newUids = [];
}
if ($criteria & Horde_Imap_Client::SYNC_FLAGSUIDS) {
$changedUids = $this->getChangedMessageUids($imapClient, $mailbox, $request);
} else {
$changedUids = [];
}
if ($criteria & Horde_Imap_Client::SYNC_VANISHEDUIDS) {
$vanishedUids = $this->getVanishedMessageUids($imapClient, $mailbox, $request);
} else {
$vanishedUids = [];
}
return [$newUids, $changedUids, $vanishedUids];
}
/**
* @param Horde_Imap_Client_Base $imapClient
* @param Horde_Imap_Client_Mailbox $mailbox
* @param Request $request
*
* @return array
* @throws Horde_Imap_Client_Exception
* @throws Horde_Imap_Client_Exception_Sync
*/
private function getNewMessageUids(Horde_Imap_Client_Base $imapClient, Horde_Imap_Client_Mailbox $mailbox, Request $request): array {
$newUids = $imapClient->sync($mailbox, $request->getToken(), [
'criteria' => Horde_Imap_Client::SYNC_NEWMSGSUIDS,
])->newmsgsuids->ids;
return $newUids;
}
/**
* @param Horde_Imap_Client_Base $imapClient
* @param Horde_Imap_Client_Mailbox $mailbox
* @param Request $request
*
* @return array
*/
private function getChangedMessageUids(Horde_Imap_Client_Base $imapClient, Horde_Imap_Client_Mailbox $mailbox, Request $request): array {
// Without QRESYNC we need to specify the known ids and in oder to avoid
// overly long IMAP commands they have to be chunked.
return array_merge(
[], // for php<7.4 https://www.php.net/manual/en/function.array-merge.php
...array_map(
static fn (Horde_Imap_Client_Ids $uids) => $imapClient->sync($mailbox, $request->getToken(), [
'criteria' => Horde_Imap_Client::SYNC_FLAGSUIDS,
'ids' => $uids,
])->flagsuids->ids,
chunk_uid_sequence($request->getUids(), self::UID_CHUNK_MAX_BYTES)
)
);
}
/**
* @param Horde_Imap_Client_Base $imapClient
* @param Horde_Imap_Client_Mailbox $mailbox
* @param Request $request
*
* @return array
*/
private function getVanishedMessageUids(Horde_Imap_Client_Base $imapClient, Horde_Imap_Client_Mailbox $mailbox, Request $request): array {
// Without QRESYNC we need to specify the known ids and in oder to avoid
// overly long IMAP commands they have to be chunked.
return array_merge(
[], // for php<7.4 https://www.php.net/manual/en/function.array-merge.php
...array_map(
static fn (Horde_Imap_Client_Ids $uids) => $imapClient->sync($mailbox, $request->getToken(), [
'criteria' => Horde_Imap_Client::SYNC_VANISHEDUIDS,
'ids' => $uids,
])->vanisheduids->ids,
chunk_uid_sequence($request->getUids(), self::UID_CHUNK_MAX_BYTES)
)
);
}
}