From 6d6512a05ed3bac07b2409423f8888784f3ad0a4 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Thu, 4 Dec 2025 13:08:49 +0200 Subject: [PATCH 1/2] imap migration --- .../Mail/Account/CommonStorageFactory.php | 132 +-- .../Espo/Core/Mail/Account/FetchData.php | 13 +- .../Espo/Core/Mail/Account/Fetcher.php | 103 +- .../GroupAccount/BouncedRecognizer.php | 7 +- .../Mail/Account/GroupAccount/Service.php | 6 +- .../Account/GroupAccount/StorageFactory.php | 6 +- .../Mail/Account/PersonalAccount/Service.php | 4 +- .../PersonalAccount/StorageFactory.php | 6 +- .../Espo/Core/Mail/Account/Storage.php | 34 +- .../Account/Storage/DirectoryTreeStorage.php | 309 ++++++ .../Mail/Account/Storage/LaminasStorage.php | 134 --- .../Espo/Core/Mail/Account/Storage/Params.php | 13 +- application/Espo/Core/Mail/EmailSender.php | 13 - application/Espo/Core/Mail/Importer.php | 4 + application/Espo/Core/Mail/Mail/Headers.php | 33 - .../Espo/Core/Mail/Mail/Storage/Imap.php | 81 -- application/Espo/Core/Mail/Message.php | 5 + application/Espo/Core/Mail/MessageWrapper.php | 4 + application/Espo/Core/Mail/Sender.php | 52 - .../Crm/Tools/MassEmail/SendingProcessor.php | 2 - .../Espo/Tools/Email/ImportEmlService.php | 8 +- composer.json | 13 +- composer.lock | 890 +++++++++--------- .../Espo/Core/Mail/Account/FetchDataTest.php | 8 +- 24 files changed, 875 insertions(+), 1005 deletions(-) create mode 100644 application/Espo/Core/Mail/Account/Storage/DirectoryTreeStorage.php delete mode 100644 application/Espo/Core/Mail/Account/Storage/LaminasStorage.php delete mode 100644 application/Espo/Core/Mail/Mail/Headers.php delete mode 100644 application/Espo/Core/Mail/Mail/Storage/Imap.php diff --git a/application/Espo/Core/Mail/Account/CommonStorageFactory.php b/application/Espo/Core/Mail/Account/CommonStorageFactory.php index d2031d422f..5ce4b3ae43 100644 --- a/application/Espo/Core/Mail/Account/CommonStorageFactory.php +++ b/application/Espo/Core/Mail/Account/CommonStorageFactory.php @@ -2,19 +2,11 @@ namespace Espo\Core\Mail\Account; +use DirectoryTree\ImapEngine\Mailbox; use Espo\Core\InjectableFactory; +use Espo\Core\Mail\Account\Storage\DirectoryTreeStorage; use Espo\Core\Mail\Account\Storage\Handler; -use Espo\Core\Mail\Account\Storage\LaminasStorage; use Espo\Core\Mail\Account\Storage\Params; -use Espo\Core\Mail\Exceptions\ImapError; -use Espo\Core\Mail\Mail\Storage\Imap; -use Espo\Core\Utils\Log; -use Laminas\Mail\Protocol\Exception\ExceptionInterface; -use Laminas\Mail\Protocol\Exception\RuntimeException as ProtocolRuntimeException; -use Laminas\Mail\Protocol\Imap as ImapProtocol; -use Laminas\Mail\Storage\Exception\InvalidArgumentException; -use Laminas\Mail\Storage\Exception\RuntimeException as LaminasRuntimeException; -use RuntimeException; /** * @since 9.3.0 @@ -23,124 +15,40 @@ class CommonStorageFactory { public function __construct( private InjectableFactory $injectableFactory, - private Log $log, ) {} - /** - * @throws ImapError - */ - public function create(Params $params): LaminasStorage + public function create(Params $params): DirectoryTreeStorage { $handlerClassName = $params->getImapHandlerClassName(); - $handler = null; - $isHandled = false; if ($handlerClassName && $params->getId()) { $handler = $this->injectableFactory->create($handlerClassName); if ($handler instanceof Handler || method_exists($handler, 'handle')) { $params = $handler->handle($params, $params->getId()); - - $isHandled = true; } } - if ($params->getAuthMechanism() === Params::AUTH_MECHANISM_XOAUTH) { - $imapParams = $this->prepareXoauthProtocol($params); - } else { - $rawParams = [ - 'host' => $params->getHost(), - 'port' => $params->getPort(), - 'username' => $params->getUsername(), - 'password' => $params->getPassword(), - 'id' => $params->getId(), - ]; + $encryption = match ($params->getSecurity()) { + Params::SECURITY_SSL => 'ssl', + Params::SECURITY_START_TLS => 'starttls', + default => null, + }; - if ($params->getSecurity()) { - $rawParams['security'] = $params->getSecurity(); - } + $authentication = $params->getAuthMechanism() === Params::AUTH_MECHANISM_XOAUTH ? + 'oauth' : 'plain'; - $imapParams = null; + $config = [ + 'host' => $params->getHost(), + 'port' => $params->getPort(), + 'username' => $params->getUsername(), + 'password' => $params->getPassword(), + 'encryption' => $encryption, + 'authentication' => $authentication, + ]; - // For bc. - if (!$isHandled && $handler && $params->getId() && method_exists($handler, 'prepareProtocol')) { - $imapParams = $handler->prepareProtocol($params->getId(), $rawParams); - } + $mailbox = new Mailbox($config); - if (!$imapParams) { - $imapParams = [ - 'host' => $rawParams['host'], - 'port' => $rawParams['port'], - 'user' => $rawParams['username'], - 'password' => $rawParams['password'], - ]; - - if (!empty($rawParams['security'])) { - $imapParams['ssl'] = $rawParams['security']; - } - } - } - - try { - $storage = new Imap($imapParams); - } catch (LaminasRuntimeException|InvalidArgumentException|ProtocolRuntimeException $e) { - throw new ImapError($e->getMessage(), 0, $e); - } - - return new LaminasStorage($storage); - } - - private function prepareXoauthProtocol(Params $params): ?ImapProtocol - { - $username = $params->getUsername(); - $accessToken = $params->getPassword(); - $host = $params->getHost() ?? throw new RuntimeException("No IMAP host."); - $port = $params->getPort(); - $ssl = $params->getSecurity() ?: false; - - try { - $protocol = new ImapProtocol($host, $port, $ssl); - } catch (ExceptionInterface $e) { - throw new RuntimeException($e->getMessage(), previous: $e); - } - - $authString = base64_encode("user=$username\1auth=Bearer $accessToken\1\1"); - - $authenticateParams = ['XOAUTH2', $authString]; - $protocol->sendRequest('AUTHENTICATE', $authenticateParams); - - $i = 0; - - while (true) { - if ($i === 10) { - return null; - } - - $response = ''; - $isPlus = $protocol->readLine($response, '+', true); - - if ($isPlus) { - $this->log->warning("Imap XOauth: Extra server challenge: " . var_export($response, true)); - - $protocol->sendRequest(''); - } else { - if ( - is_string($response) && - (preg_match('/^NO /i', $response) || preg_match('/^BAD /i', $response)) - ) { - $this->log->error("Imap XOauth: Failure: " . var_export($response, true)); - - return null; - } - - if (is_string($response) && preg_match("/^OK /i", $response)) { - break; - } - } - - $i++; - } - - return $protocol; + return new DirectoryTreeStorage($mailbox); } } diff --git a/application/Espo/Core/Mail/Account/FetchData.php b/application/Espo/Core/Mail/Account/FetchData.php index d605ad8c20..e0c07c6897 100644 --- a/application/Espo/Core/Mail/Account/FetchData.php +++ b/application/Espo/Core/Mail/Account/FetchData.php @@ -54,9 +54,16 @@ class FetchData return ObjectUtil::clone($this->data); } - public function getLastUniqueId(string $folder): ?string + public function getLastUid(string $folder): ?int { - return $this->data->lastUID->$folder ?? null; + $id = $this->data->lastUID->$folder ?? null; + + if ($id === null) { + return null; + } + + // To int for bc. It used to be string. + return (int) $id; } public function getLastDate(string $folder): ?DateTime @@ -84,7 +91,7 @@ class FetchData return $this->data->byDate->$folder ?? false; } - public function setLastUniqueId(string $folder, ?string $uniqueId): void + public function setLastUid(string $folder, ?int $uniqueId): void { if (!property_exists($this->data, 'lastUID')) { $this->data->lastUID = (object) []; diff --git a/application/Espo/Core/Mail/Account/Fetcher.php b/application/Espo/Core/Mail/Account/Fetcher.php index 63155be6e0..33d29501e1 100644 --- a/application/Espo/Core/Mail/Account/Fetcher.php +++ b/application/Espo/Core/Mail/Account/Fetcher.php @@ -30,7 +30,6 @@ namespace Espo\Core\Mail\Account; use Espo\Core\Exceptions\Error; - use Espo\Core\Mail\Account\Storage\Flag; use Espo\Core\Mail\Exceptions\ImapError; use Espo\Core\Mail\Exceptions\NoImap; @@ -41,7 +40,6 @@ use Espo\Core\Mail\MessageWrapper; use Espo\Core\Mail\Account\Hook\BeforeFetch as BeforeFetchHook; use Espo\Core\Mail\Account\Hook\AfterFetch as AfterFetchHook; use Espo\Core\Mail\Account\Hook\BeforeFetchResult as BeforeFetchHookResult; - use Espo\Core\Utils\Config; use Espo\Core\Utils\Log; use Espo\Core\Field\DateTime as DateTimeField; @@ -52,7 +50,6 @@ use Espo\ORM\Collection; use Espo\ORM\EntityManager; use Espo\ORM\Query\Part\Expression; use Espo\ORM\Query\Part\Order; - use Throwable; use DateTime; @@ -115,51 +112,44 @@ class Fetcher try { $storage->selectFolder($folderOriginal); } catch (Throwable $e) { - $this->log->error( - "{$account->getEntityType()} {$account->getId()}, " . - "could not select folder '$folder'; [{$e->getCode()}] {$e->getMessage()}" - ); + $message = "{$account->getEntityType()} {$account->getId()}, " . + "could not select folder '$folder'; {$e->getMessage()}"; + + $this->log->error($message, ['exception' => $e]); return; } - $lastUniqueId = $fetchData->getLastUniqueId($folder); + $lastId = $fetchData->getLastUid($folder); $lastDate = $fetchData->getLastDate($folder); $forceByDate = $fetchData->getForceByDate($folder); - $portionLimit = $forceByDate ? 0 : $account->getPortionLimit(); - $previousLastUniqueId = $lastUniqueId; + $previousLastId = $lastId; - $idList = $this->getIdList( + $ids = $this->fetchIds( account: $account, storage: $storage, - lastUID: $lastUniqueId, + lastUid: $lastId, lastDate: $lastDate, forceByDate: $forceByDate, ); - if (count($idList) === 1 && $lastUniqueId) { - if ($storage->getUniqueId($idList[0]) === $lastUniqueId) { - return; - } + if (count($ids) === 1 && $ids[0] === $lastId) { + return; } $counter = 0; - foreach ($idList as $id) { - if ($counter == count($idList) - 1) { - $lastUniqueId = $storage->getUniqueId($id); + foreach ($ids as $id) { + if ($counter === count($ids) - 1) { + $lastId = $id; } - if ($forceByDate && $previousLastUniqueId) { - $uid = $storage->getUniqueId($id); + if ($forceByDate && $previousLastId && $id <= $previousLastId) { + $counter++; - if ((int) $uid <= (int) $previousLastUniqueId) { - $counter++; - - continue; - } + continue; } $email = $this->fetchEmail( @@ -170,11 +160,11 @@ class Fetcher mappedEmailFolderId: $account->getMappedEmailFolder($folderOriginal)?->getId(), ); - $isLast = $counter === count($idList) - 1; + $isLast = $counter === count($ids) - 1; $isLastInPortion = $counter === $portionLimit - 1; if ($isLast || $isLastInPortion) { - $lastUniqueId = $storage->getUniqueId($id); + $lastId = $id; if ($email && $email->getDateSent()) { $lastDate = $email->getDateSent(); @@ -187,7 +177,7 @@ class Fetcher break; } - $counter++; + $counter ++; } if ($forceByDate) { @@ -195,25 +185,21 @@ class Fetcher } $fetchData->setLastDate($folder, $lastDate); - $fetchData->setLastUniqueId($folder, $lastUniqueId); + $fetchData->setLastUid($folder, $lastId); - if ($forceByDate && $previousLastUniqueId) { - $idList = $storage->getIdsFromUniqueId($previousLastUniqueId); + if ($forceByDate && $previousLastId) { + $ids = $storage->getUidsFromUid($previousLastId); - if (count($idList)) { - $uid1 = $storage->getUniqueId($idList[0]); - - if ((int) $uid1 > (int) $previousLastUniqueId) { - $fetchData->setForceByDate($folder, false); - } + if (count($ids) && $ids[0] > $previousLastId) { + $fetchData->setForceByDate($folder, false); } } if ( !$forceByDate && - $previousLastUniqueId && - count($idList) && - (int) $previousLastUniqueId >= (int) $lastUniqueId + count($ids) && + $previousLastId && + $previousLastId >= $lastId ) { // Handling broken numbering. Next time fetch since the last date rather than the last UID. $fetchData->setForceByDate($folder, true); @@ -226,20 +212,20 @@ class Fetcher * @return int[] * @throws Error */ - private function getIdList( + private function fetchIds( Account $account, Storage $storage, - ?string $lastUID, + ?int $lastUid, ?DateTimeField $lastDate, - bool $forceByDate + bool $forceByDate, ): array { - if (!empty($lastUID) && !$forceByDate) { - return $storage->getIdsFromUniqueId($lastUID); + if ($lastUid !== null && !$forceByDate) { + return $storage->getUidsFromUid($lastUid); } if ($lastDate) { - return $storage->getIdsSinceDate($lastDate); + return $storage->getUidsSinceDate($lastDate); } if (!$account->getFetchSince()) { @@ -248,7 +234,7 @@ class Fetcher $fetchSince = $account->getFetchSince()->toDateTime(); - return $storage->getIdsSinceDate( + return $storage->getUidsSinceDate( DateTimeField::fromDateTime($fetchSince) ); } @@ -316,7 +302,7 @@ class Fetcher $flags !== null && !in_array(Flag::SEEN, $flags) ) { - $storage->setFlags($id, self::flagsWithoutRecent($flags)); + $storage->unmarkSeen($id); } } catch (Throwable $e) { $this->log->error( @@ -436,10 +422,10 @@ class Fetcher try { return $this->importer->import($message, $data); } catch (Throwable $e) { - $this->log->error( - "{$account->getEntityType()} {$account->getId()}, import message; " . - "{$e->getCode()} {$e->getMessage()}" - ); + $message = "{$account->getEntityType()} {$account->getId()}, import message; " . + "{$e->getCode()} {$e->getMessage()}"; + + $this->log->error($message, ['exception' => $e]); if ($this->entityManager->getLocker()->isLocked()) { $this->entityManager->getLocker()->rollback(); @@ -449,17 +435,6 @@ class Fetcher return null; } - /** - * @param string[] $flags - * @return string[] - */ - private static function flagsWithoutRecent(array $flags): array - { - return array_values( - array_diff($flags, [Flag::RECENT]) - ); - } - /** * @return array */ diff --git a/application/Espo/Core/Mail/Account/GroupAccount/BouncedRecognizer.php b/application/Espo/Core/Mail/Account/GroupAccount/BouncedRecognizer.php index d793d57d61..9dbbde6bfb 100644 --- a/application/Espo/Core/Mail/Account/GroupAccount/BouncedRecognizer.php +++ b/application/Espo/Core/Mail/Account/GroupAccount/BouncedRecognizer.php @@ -29,6 +29,7 @@ namespace Espo\Core\Mail\Account\GroupAccount; +use Espo\Core\Mail\Exceptions\ImapError; use Espo\Core\Mail\Message; use Espo\Core\Mail\Message\Part; @@ -62,7 +63,11 @@ class BouncedRecognizer return true; } - $content = $message->getRawContent(); + try { + $content = $message->getRawContent(); + } catch (ImapError) { + return false; + } if ( str_contains($content, 'message/delivery-status') && diff --git a/application/Espo/Core/Mail/Account/GroupAccount/Service.php b/application/Espo/Core/Mail/Account/GroupAccount/Service.php index 9fd7a6740f..051be93aad 100644 --- a/application/Espo/Core/Mail/Account/GroupAccount/Service.php +++ b/application/Espo/Core/Mail/Account/GroupAccount/Service.php @@ -42,8 +42,6 @@ use Espo\Core\Mail\Sender\Message; use Espo\Core\Utils\Log; use Exception; -use Laminas\Mail\Exception\ExceptionInterface; - class Service { public function __construct( @@ -117,10 +115,10 @@ class Service 'message' => $e->getMessage(), ]); - $message = $e instanceof ExceptionInterface || $e instanceof ImapError ? + $message = $e instanceof ImapError ? $e->getMessage() : ''; - throw new ErrorSilent($message); + throw new ErrorSilent($message, previous: $e); } } diff --git a/application/Espo/Core/Mail/Account/GroupAccount/StorageFactory.php b/application/Espo/Core/Mail/Account/GroupAccount/StorageFactory.php index 7068c74395..2afcc88961 100644 --- a/application/Espo/Core/Mail/Account/GroupAccount/StorageFactory.php +++ b/application/Espo/Core/Mail/Account/GroupAccount/StorageFactory.php @@ -30,10 +30,10 @@ namespace Espo\Core\Mail\Account\GroupAccount; use Espo\Core\Mail\Account\CommonStorageFactory; +use Espo\Core\Mail\Account\Storage; use Espo\Core\Mail\Account\Storage\Params; use Espo\Core\Mail\Account\Account; use Espo\Core\Mail\Account\StorageFactory as StorageFactoryInterface; -use Espo\Core\Mail\Account\Storage\LaminasStorage; use Espo\Core\Mail\Exceptions\NoImap; class StorageFactory implements StorageFactoryInterface @@ -42,7 +42,7 @@ class StorageFactory implements StorageFactoryInterface private CommonStorageFactory $commonStorageFactory, ) {} - public function create(Account $account): LaminasStorage + public function create(Account $account): Storage { $imapParams = $account->getImapParams(); @@ -63,7 +63,7 @@ class StorageFactory implements StorageFactoryInterface return $this->createWithParams($params); } - public function createWithParams(Params $params): LaminasStorage + public function createWithParams(Params $params): Storage { return $this->commonStorageFactory->create($params); } diff --git a/application/Espo/Core/Mail/Account/PersonalAccount/Service.php b/application/Espo/Core/Mail/Account/PersonalAccount/Service.php index ec6de3ca7a..cd9d153195 100644 --- a/application/Espo/Core/Mail/Account/PersonalAccount/Service.php +++ b/application/Espo/Core/Mail/Account/PersonalAccount/Service.php @@ -43,8 +43,6 @@ use Espo\Core\Mail\Account\StorageFactory; use Espo\Entities\User; use Espo\Core\Mail\Sender\Message; -use Laminas\Mail\Exception\ExceptionInterface; - use Exception; class Service @@ -154,7 +152,7 @@ class Service 'message' => $e->getMessage(), ]); - $message = $e instanceof ExceptionInterface || $e instanceof ImapError ? + $message = $e instanceof ImapError ? $e->getMessage() : ''; throw new ErrorSilent($message); diff --git a/application/Espo/Core/Mail/Account/PersonalAccount/StorageFactory.php b/application/Espo/Core/Mail/Account/PersonalAccount/StorageFactory.php index cd3b30048d..2fa60eb1fa 100644 --- a/application/Espo/Core/Mail/Account/PersonalAccount/StorageFactory.php +++ b/application/Espo/Core/Mail/Account/PersonalAccount/StorageFactory.php @@ -30,11 +30,11 @@ namespace Espo\Core\Mail\Account\PersonalAccount; use Espo\Core\Mail\Account\CommonStorageFactory; +use Espo\Core\Mail\Account\Storage; use Espo\Core\Mail\Account\Storage\Params; use Espo\Core\Mail\Account\StorageFactory as StorageFactoryInterface; use Espo\Core\Mail\Account\Account; use Espo\Core\Mail\Exceptions\NoImap; -use Espo\Core\Mail\Account\Storage\LaminasStorage; use LogicException; @@ -44,7 +44,7 @@ class StorageFactory implements StorageFactoryInterface private CommonStorageFactory $commonStorageFactory, ) {} - public function create(Account $account): LaminasStorage + public function create(Account $account): Storage { $userLink = $account->getUser(); @@ -75,7 +75,7 @@ class StorageFactory implements StorageFactoryInterface return $this->createWithParams($params); } - public function createWithParams(Params $params): LaminasStorage + public function createWithParams(Params $params): Storage { return $this->commonStorageFactory->create($params); } diff --git a/application/Espo/Core/Mail/Account/Storage.php b/application/Espo/Core/Mail/Account/Storage.php index 9a53cdc4d5..c0a8abce94 100644 --- a/application/Espo/Core/Mail/Account/Storage.php +++ b/application/Espo/Core/Mail/Account/Storage.php @@ -30,49 +30,55 @@ namespace Espo\Core\Mail\Account; use Espo\Core\Field\DateTime; +use Espo\Core\Mail\Exceptions\ImapError; interface Storage { /** - * Set message flags. + * Mark as unseen. * - * @param string[] $flags + * @throws ImapError */ - public function setFlags(int $id, array $flags): void; + public function unmarkSeen(int $id): void; /** * Get a message size. + * + * @throws ImapError */ public function getSize(int $id): int; /** * Get message raw content. + * + * @throws ImapError */ public function getRawContent(int $id): string; - /** - * Get a message unique ID. - */ - public function getUniqueId(int $id): string; - /** * Get IDs from unique ID. * * @return int[] + * + * @throws ImapError */ - public function getIdsFromUniqueId(string $uniqueId): array; + public function getUidsFromUid(int $id): array; /** * Get IDs since a specific date. * * @return int[] + * + * @throws ImapError */ - public function getIdsSinceDate(DateTime $since): array; + public function getUidsSinceDate(DateTime $since): array; /** * Get only header and flags. Won't fetch the whole email. * * @return array{header: string, flags: string[]} + * + * @throws ImapError */ public function getHeaderAndFlags(int $id): array; @@ -83,16 +89,22 @@ interface Storage /** * @return string[] + * + * @throws ImapError */ public function getFolderNames(): array; /** * Select a folder. + * + * @throws ImapError */ public function selectFolder(string $name): void; /** * Store a message. + * + * @throws ImapError */ - public function appendMessage(string $content, ?string $folder = null): void; + public function appendMessage(string $content, string $folder): void; } diff --git a/application/Espo/Core/Mail/Account/Storage/DirectoryTreeStorage.php b/application/Espo/Core/Mail/Account/Storage/DirectoryTreeStorage.php new file mode 100644 index 0000000000..dc0bd4f48f --- /dev/null +++ b/application/Espo/Core/Mail/Account/Storage/DirectoryTreeStorage.php @@ -0,0 +1,309 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +namespace Espo\Core\Mail\Account\Storage; + +use DirectoryTree\ImapEngine\Exceptions\Exception as CommonException; +use DirectoryTree\ImapEngine\FolderInterface; +use DirectoryTree\ImapEngine\Mailbox; +use DirectoryTree\ImapEngine\Message; +use DirectoryTree\ImapEngine\MessageInterface; +use DirectoryTree\ImapEngine\MessageQuery; +use Espo\Core\Field\DateTime; +use Espo\Core\Mail\Account\Storage; +use Espo\Core\Mail\Exceptions\ImapError; +use LogicException; + +class DirectoryTreeStorage implements Storage +{ + private ?FolderInterface $selectedFolder = null; + + public function __construct( + private Mailbox $mailbox, + ) {} + + /** + * @todo Test. + * @inheritDoc + * @noinspection PhpRedundantCatchClauseInspection + */ + public function unmarkSeen(int $id): void + { + $folder = $this->getSelectedFolder(); + + try { + $message = $folder->messages() + ->withFlags() + ->find($id); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + + if (!$message) { + throw new ImapError("Could not fetch message $id."); + } + + try { + $message->unmarkSeen(); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + } + + /** + * @noinspection PhpRedundantCatchClauseInspection + */ + public function getSize(int $id): int + { + $folder = $this->getSelectedFolder(); + + try { + $message = $folder->messages() + ->withSize() + ->find($id); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + + if (!$message) { + throw new ImapError("Could not fetch message $id."); + } + + return $message->size() ?? 0; + } + + /** + * @inheritDoc + * @noinspection PhpRedundantCatchClauseInspection + */ + public function getRawContent(int $id): string + { + $folder = $this->getSelectedFolder(); + + try { + $message = $folder->messages() + ->withHeaders() + ->withFlags() + ->withBody() + ->find($id); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + + if (!$message) { + throw new ImapError("Could not fetch message $id."); + } + + if (!$message instanceof Message) { + throw new LogicException("Not supported message instance."); + } + + return $message->body(); + } + + /** + * @todo Test. + * @inheritDoc + * @noinspection PhpRedundantCatchClauseInspection + */ + public function getUidsFromUid(int $id): array + { + $output = []; + + $query = $this->getSelectedFolder() + ->messages() + ->withoutHeaders() + ->uid($id, INF); + + /** + * Magic methods are used. + * @noinspection PhpConditionAlreadyCheckedInspection + * @phpstan-ignore-next-line + */ + assert($query instanceof MessageQuery); + + try { + $query->each(function (MessageInterface $message) use (&$output) { + $output[] = $message->uid(); + }); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + + return $output; + } + + /** + * @todo Test. + * @inheritDoc + * @noinspection PhpRedundantCatchClauseInspection + */ + public function getUidsSinceDate(DateTime $since): array + { + $output = []; + + $query = $this->getSelectedFolder() + ->messages() + ->withoutHeaders() + ->since($since->toDateTime()); + + /** + * Magic methods are used. + * @noinspection PhpConditionAlreadyCheckedInspection + * @phpstan-ignore-next-line + */ + assert($query instanceof MessageQuery); + + try { + $query->each(function (MessageInterface $message) use (&$output) { + $output[] = $message->uid(); + }); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + + return $output; + } + + /** + * @return array{header: string, flags: string[]} + * @inheritDoc + * @noinspection PhpRedundantCatchClauseInspection + */ + public function getHeaderAndFlags(int $id): array + { + try { + $folder = $this->getSelectedFolder(); + + $message = $folder->messages() + ->withHeaders() + ->withFlags() + ->find($id); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + + if (!$message) { + throw new ImapError("Could not fetch message $id."); + } + + if (!$message instanceof Message) { + // Whenever the library is upgraded, it's reasonable to check whether + // the Message instance is still returned by the library. + throw new LogicException("Not supported message instance."); + } + + return [ + 'header' => $message->head(), + 'flags' => $folder->flags(), + ]; + } + + public function close(): void + { + $this->mailbox->disconnect(); + $this->selectedFolder = null; + } + + /** + * @return string[] + * @inheritDoc + * @noinspection PhpRedundantCatchClauseInspection + */ + public function getFolderNames(): array + { + $output = []; + + try { + $folders = $this->mailbox->folders()->get(); + + foreach ($folders as $folder) { + $output[] = $folder->name(); + } + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + + return $output; + } + + /** + * @inheritDoc + * @noinspection PhpRedundantCatchClauseInspection + */ + public function selectFolder(string $name): void + { + try { + $folder = $this->getFolder($name); + $this->selectedFolder = $folder; + $folder->select(); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + } + + /** + * @inheritDoc + * @noinspection PhpRedundantCatchClauseInspection + */ + public function appendMessage(string $content, string $folder): void + { + try { + $this->getFolder($folder) + ->messages() + ->append($content); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + } + + private function getSelectedFolder(): FolderInterface + { + return $this->selectedFolder ?: $this->mailbox->inbox(); + } + + /** + * @throws ImapError + * @noinspection PhpRedundantCatchClauseInspection + */ + private function getFolder(string $name): FolderInterface + { + try { + $folder = $this->mailbox->folders()->find($name); + } catch (CommonException $e) { + throw new ImapError($e->getMessage(), previous: $e); + } + + if (!$folder) { + throw new ImapError("Could not select folder '$name'."); + } + + return $folder; + } +} diff --git a/application/Espo/Core/Mail/Account/Storage/LaminasStorage.php b/application/Espo/Core/Mail/Account/Storage/LaminasStorage.php deleted file mode 100644 index ac6ba84146..0000000000 --- a/application/Espo/Core/Mail/Account/Storage/LaminasStorage.php +++ /dev/null @@ -1,134 +0,0 @@ -. - * - * The interactive user interfaces in modified source and object code versions - * of this program must display Appropriate Legal Notices, as required under - * Section 5 of the GNU Affero General Public License version 3. - * - * In accordance with Section 7(b) of the GNU Affero General Public License version 3, - * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. - ************************************************************************/ - -namespace Espo\Core\Mail\Account\Storage; - -use Espo\Core\Mail\Account\Storage; -use Espo\Core\Mail\Mail\Storage\Imap; -use Espo\Core\Field\DateTime; - -use RecursiveIteratorIterator; - -class LaminasStorage implements Storage -{ - public function __construct(private Imap $imap) - {} - - /** - * @param string[] $flags - */ - public function setFlags(int $id, array $flags): void - { - $this->imap->setFlags($id, $flags); - } - - public function getSize(int $id): int - { - /** @var int */ - return $this->imap->getSize($id); - } - - public function getRawContent(int $id): string - { - return $this->imap->getRawContent($id); - } - - public function getUniqueId(int $id): string - { - /** @var string */ - return $this->imap->getUniqueId($id); - } - - /** - * @return int[] - */ - public function getIdsFromUniqueId(string $uniqueId): array - { - return $this->imap->getIdsFromUniqueId($uniqueId); - } - - /** - * @return int[] - */ - public function getIdsSinceDate(DateTime $since): array - { - return $this->imap->getIdsSinceDate( - $since->toDateTime()->format('d-M-Y') - ); - } - - /** - * @return array{header: string, flags: string[]} - */ - public function getHeaderAndFlags(int $id): array - { - return $this->imap->getHeaderAndFlags($id); - } - - public function close(): void - { - $this->imap->close(); - } - - /** - * @return string[] - */ - public function getFolderNames(): array - { - $folderIterator = new RecursiveIteratorIterator( - $this->imap->getFolders(), - RecursiveIteratorIterator::SELF_FIRST - ); - - $list = []; - - foreach ($folderIterator as $folder) { - $list[] = mb_convert_encoding($folder->getGlobalName(), 'UTF-8', 'UTF7-IMAP'); - } - - /** @var string[] */ - return $list; - } - - public function selectFolder(string $name): void - { - $nameConverted = mb_convert_encoding($name, 'UTF7-IMAP', 'UTF-8'); - - $this->imap->selectFolder($nameConverted); - } - - public function appendMessage(string $content, ?string $folder = null): void - { - if ($folder !== null) { - $folder = mb_convert_encoding($folder, 'UTF7-IMAP', 'UTF-8'); - } - - $this->imap->appendMessage($content, $folder); - } -} diff --git a/application/Espo/Core/Mail/Account/Storage/Params.php b/application/Espo/Core/Mail/Account/Storage/Params.php index 6ecc6f64ba..960c39cfea 100644 --- a/application/Espo/Core/Mail/Account/Storage/Params.php +++ b/application/Espo/Core/Mail/Account/Storage/Params.php @@ -36,17 +36,18 @@ use SensitiveParameter; */ class Params { + /** @since 9.3.0 */ + public const string SECURITY_SSL = 'SSL'; + /** @since 9.3.0 */ + public const string SECURITY_START_TLS = 'TLS'; + /** @var ?class-string */ private ?string $imapHandlerClassName; - /** - * @since 9.3.0 - */ + /** @since 9.3.0 */ public const string AUTH_MECHANISM_PLAIN = 'plain'; - /** - * @since 9.3.0 - */ + /** @since 9.3.0 */ public const string AUTH_MECHANISM_XOAUTH = 'xoauth'; /** diff --git a/application/Espo/Core/Mail/EmailSender.php b/application/Espo/Core/Mail/EmailSender.php index e2f5054e8f..aca339d96f 100644 --- a/application/Espo/Core/Mail/EmailSender.php +++ b/application/Espo/Core/Mail/EmailSender.php @@ -37,8 +37,6 @@ use Espo\Core\Utils\Config; use Espo\Entities\Attachment; use Espo\Entities\Email; -use Laminas\Mail\Message; - /** * A service for email sending. Can send with SMTP parameters of the system email account or with specific parameters. * Uses a builder to send with specific parameters. @@ -124,17 +122,6 @@ class EmailSender return $this->createSender()->withEnvelopeOptions($options); } - /** - * Set a message instance. - * - * @deprecated As of v9.1. Use `withAddedHeader`. - * @todo Remove in v10.0. - */ - public function withMessage(Message $message): Sender - { - return $this->createSender()->withMessage($message); - } - /** * Add a header. * diff --git a/application/Espo/Core/Mail/Importer.php b/application/Espo/Core/Mail/Importer.php index e72ae36d91..a34d12bd3b 100644 --- a/application/Espo/Core/Mail/Importer.php +++ b/application/Espo/Core/Mail/Importer.php @@ -29,6 +29,7 @@ namespace Espo\Core\Mail; +use Espo\Core\Mail\Exceptions\ImapError; use Espo\Core\Mail\Importer\Data; use Espo\Entities\Email; @@ -37,5 +38,8 @@ use Espo\Entities\Email; */ interface Importer { + /** + * @throws ImapError + */ public function import(Message $message, Data $data): ?Email; } diff --git a/application/Espo/Core/Mail/Mail/Headers.php b/application/Espo/Core/Mail/Mail/Headers.php deleted file mode 100644 index db2b6c5628..0000000000 --- a/application/Espo/Core/Mail/Mail/Headers.php +++ /dev/null @@ -1,33 +0,0 @@ -. - * - * The interactive user interfaces in modified source and object code versions - * of this program must display Appropriate Legal Notices, as required under - * Section 5 of the GNU Affero General Public License version 3. - * - * In accordance with Section 7(b) of the GNU Affero General Public License version 3, - * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. - ************************************************************************/ - -namespace Espo\Core\Mail\Mail; - -class Headers extends \Laminas\Mail\Headers -{} diff --git a/application/Espo/Core/Mail/Mail/Storage/Imap.php b/application/Espo/Core/Mail/Mail/Storage/Imap.php deleted file mode 100644 index da5c865b20..0000000000 --- a/application/Espo/Core/Mail/Mail/Storage/Imap.php +++ /dev/null @@ -1,81 +0,0 @@ -. - * - * The interactive user interfaces in modified source and object code versions - * of this program must display Appropriate Legal Notices, as required under - * Section 5 of the GNU Affero General Public License version 3. - * - * In accordance with Section 7(b) of the GNU Affero General Public License version 3, - * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. - ************************************************************************/ - -namespace Espo\Core\Mail\Mail\Storage; - -class Imap extends \Laminas\Mail\Storage\Imap -{ - /** - * @return int[] - */ - public function getIdsFromUniqueId(string $uid): array - { - $nextUid = strval(intval($uid) + 1); - - assert($this->protocol !== null); - - return $this->protocol->search(['UID ' . $nextUid . ':*']); - } - - /** - * @param string $date A date in the `d-M-Y` format. - * @return int[] - */ - public function getIdsSinceDate(string $date): array - { - assert($this->protocol !== null); - - return $this->protocol->search(['SINCE ' . $date]); - } - - /** - * @param int $id - * @return array{header: string, flags: string[]} - */ - public function getHeaderAndFlags(int $id): array - { - assert($this->protocol !== null); - - /** @var array{'RFC822.HEADER': string, 'FLAGS': string[]} $data */ - $data = $this->protocol->fetch(['FLAGS', 'RFC822.HEADER'], $id); - - $header = $data['RFC822.HEADER']; - - $flags = []; - - foreach ($data['FLAGS'] as $flag) { - $flags[] = static::$knownFlags[$flag] ?? $flag; - } - - return [ - 'flags' => $flags, - 'header' => $header, - ]; - } -} diff --git a/application/Espo/Core/Mail/Message.php b/application/Espo/Core/Mail/Message.php index d695bb91e7..a63154a43b 100644 --- a/application/Espo/Core/Mail/Message.php +++ b/application/Espo/Core/Mail/Message.php @@ -29,6 +29,7 @@ namespace Espo\Core\Mail; +use Espo\Core\Mail\Exceptions\ImapError; use Espo\Core\Mail\Message\Part; interface Message @@ -50,11 +51,15 @@ interface Message /** * Get a raw content part. + * + * @throws ImapError */ public function getRawContent(): string; /** * Get a full raw message. + * + * @throws ImapError */ public function getFullRawContent(): string; diff --git a/application/Espo/Core/Mail/MessageWrapper.php b/application/Espo/Core/Mail/MessageWrapper.php index bdcb9a0a39..585e37fbda 100644 --- a/application/Espo/Core/Mail/MessageWrapper.php +++ b/application/Espo/Core/Mail/MessageWrapper.php @@ -30,6 +30,7 @@ namespace Espo\Core\Mail; use Espo\Core\Mail\Account\Storage; +use Espo\Core\Mail\Exceptions\ImapError; use Espo\Core\Mail\Message\Part; use RuntimeException; @@ -42,6 +43,9 @@ class MessageWrapper implements Message /** @var ?string[] */ private ?array $flagList = null; + /** + * @throws ImapError + */ public function __construct( private int $id, private ?Storage $storage = null, diff --git a/application/Espo/Core/Mail/Sender.php b/application/Espo/Core/Mail/Sender.php index a24f9f5477..ee53416cda 100644 --- a/application/Espo/Core/Mail/Sender.php +++ b/application/Espo/Core/Mail/Sender.php @@ -44,10 +44,6 @@ use Espo\Entities\Attachment; use Espo\Entities\Email; use Espo\ORM\EntityManager; -use Laminas\Mail\Headers; -use Laminas\Mail\Message as LaminasMessage; - -use RuntimeException; use Symfony\Component\Mailer\Envelope; use Symfony\Component\Mailer\Exception\TransportExceptionInterface; use Symfony\Component\Mailer\Transport\TransportInterface; @@ -71,7 +67,6 @@ class Sender /** @var array */ private array $overrideParams = []; private ?string $envelopeFromAddress = null; - private ?LaminasMessage $laminasMessage = null; /** @var ?iterable */ private $attachmentList = null; /** @var array{string, string}[] */ @@ -96,7 +91,6 @@ class Sender { $this->params = []; $this->envelopeFromAddress = null; - $this->laminasMessage = null; $this->attachmentList = null; $this->overrideParams = []; $this->headers = []; @@ -197,19 +191,6 @@ class Sender return $this; } - /** - * Set a message instance. - * - * @deprecated As of v9.1. Use `withAddedHeader`. - * @todo Remove in v10.0. - */ - public function withMessage(LaminasMessage $message): self - { - $this->laminasMessage = $message; - - return $this; - } - /** * Add a header. * @@ -313,8 +294,6 @@ class Sender $this->applyBody($email, $message); $this->applyMessageId($email, $message); - $this->applyLaminasMessageHeaders($message); - if (!$this->transport) { throw new LogicException(); } @@ -595,17 +574,6 @@ class Sender $message->getHeaders()->addTextHeader($item[0], $item[1]); } - if ($this->laminasMessage) { - // For bc. - foreach ($this->laminasMessage->getHeaders() as $it) { - if ($it->getFieldName() === 'Date') { - continue; - } - - $message->getHeaders()->addTextHeader($it->getFieldName(), $it->getFieldValue()); - } - } - if ($email->isAutoReply() && !$message->getHeaders()->has('Auto-Submitted')) { $message->getHeaders()->addTextHeader('Auto-Submitted', 'auto-replied'); } @@ -625,24 +593,4 @@ class Sender return new Envelope(new Address($this->envelopeFromAddress), $recipients); } - - private function applyLaminasMessageHeaders(Message $message): void - { - if (!$this->laminasMessage) { - return; - } - - $parts = preg_split("/\R\R/", $message->toString(), 2); - - if (!is_array($parts) || count($parts) < 2) { - throw new RuntimeException("Could not split email."); - } - - /** @noinspection PhpMultipleClassDeclarationsInspection */ - $this->laminasMessage - ->setHeaders( - Headers::fromString($parts[0]) - ) - ->setBody($parts[1]); - } } diff --git a/application/Espo/Modules/Crm/Tools/MassEmail/SendingProcessor.php b/application/Espo/Modules/Crm/Tools/MassEmail/SendingProcessor.php index 23647c26ee..c8efe82810 100644 --- a/application/Espo/Modules/Crm/Tools/MassEmail/SendingProcessor.php +++ b/application/Espo/Modules/Crm/Tools/MassEmail/SendingProcessor.php @@ -30,8 +30,6 @@ namespace Espo\Modules\Crm\Tools\MassEmail; use Espo\Modules\Crm\Tools\MassEmail\MessagePreparator\Headers; -use Laminas\Mail\Message; - use Espo\Core\Field\DateTime; use Espo\Core\Mail\ConfigDataProvider; use Espo\ORM\EntityCollection; diff --git a/application/Espo/Tools/Email/ImportEmlService.php b/application/Espo/Tools/Email/ImportEmlService.php index b76ae4fe22..46b8cdc1fa 100644 --- a/application/Espo/Tools/Email/ImportEmlService.php +++ b/application/Espo/Tools/Email/ImportEmlService.php @@ -33,6 +33,7 @@ use Espo\Core\Exceptions\Conflict; use Espo\Core\Exceptions\Error; use Espo\Core\Exceptions\NotFound; use Espo\Core\FileStorage\Manager; +use Espo\Core\Mail\Exceptions\ImapError; use Espo\Core\Mail\Importer; use Espo\Core\Mail\Importer\Data; use Espo\Core\Mail\MessageWrapper; @@ -40,6 +41,7 @@ use Espo\Core\Mail\Parsers\MailMimeParser; use Espo\Entities\Attachment; use Espo\Entities\Email; use Espo\ORM\EntityManager; +use RuntimeException; class ImportEmlService { @@ -66,7 +68,11 @@ class ImportEmlService $attachment = $this->getAttachment($fileId); $contents = $this->fileStorageManager->getContents($attachment); - $message = new MessageWrapper(1, null, $this->parser, $contents); + try { + $message = new MessageWrapper(1, null, $this->parser, $contents); + } catch (ImapError $e) { + throw new RuntimeException(previous: $e); + } $this->checkDuplicate($message); diff --git a/composer.json b/composer.json index 48fa47c6b6..95106e301d 100644 --- a/composer.json +++ b/composer.json @@ -23,8 +23,6 @@ "slim/slim": "^4.15", "slim/psr7": "^1", "dragonmantank/cron-expression": "^3.4", - "laminas/laminas-mail": "dev-master#3befe2ed6193c10fb045369bd6473bd70fb9ceac", - "laminas/laminas-mime": "dev-master#25659453400e0b2970e490dd0e4045f62acf5371", "laminas/laminas-ldap": "2.20.x-dev#e7b9fe0e295f3898fb97d08b320654c92849918a", "monolog/monolog": "^3.9", "zordius/lightncandy": "dev-espo#v1.2.5e", @@ -56,7 +54,8 @@ "league/html-to-markdown": "^5.1", "psr/clock": "^1.0", "react/child-process": "^0.6.6", - "lasserafn/php-initial-avatar-generator": "dev-update-image-lib#a46ab8f1427f93c5b37957e739205da7fcca0290" + "lasserafn/php-initial-avatar-generator": "dev-update-image-lib#a46ab8f1427f93c5b37957e739205da7fcca0290", + "directorytree/imapengine": "^1.19" }, "require-dev": { "phpunit/phpunit": "^11.5", @@ -96,14 +95,6 @@ "type": "git", "url": "https://github.com/yurikuzn/lightncandy.git" }, - { - "type": "git", - "url": "https://github.com/yurikuzn/laminas-mail.git" - }, - { - "type": "git", - "url": "https://github.com/yurikuzn/laminas-mime.git" - }, { "type": "git", "url": "https://github.com/yurikuzn/laminas-ldap.git" diff --git a/composer.lock b/composer.lock index 58a791ddd5..714f0409cb 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c120697edc5492876e53f288b9a41e13", + "content-hash": "1d054b72274e6a650dc6dabcab11660f", "packages": [ { "name": "async-aws/core", @@ -638,6 +638,68 @@ ], "time": "2024-09-19T14:15:21+00:00" }, + { + "name": "directorytree/imapengine", + "version": "v1.19.0", + "source": { + "type": "git", + "url": "https://github.com/DirectoryTree/ImapEngine.git", + "reference": "fd8d25780f76cde4e716767d557ea0d4f9be37f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DirectoryTree/ImapEngine/zipball/fd8d25780f76cde4e716767d557ea0d4f9be37f2", + "reference": "fd8d25780f76cde4e716767d557ea0d4f9be37f2", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^4.0", + "illuminate/collections": ">=9.0", + "nesbot/carbon": ">=2.0", + "php": "^8.1", + "symfony/mime": ">=6.0", + "zbateson/mail-mime-parser": "^3.0" + }, + "require-dev": { + "pestphp/pest": "^2.0|^3.0", + "spatie/ray": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "DirectoryTree\\ImapEngine\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Steve Bauman", + "email": "steven_bauman@outlook.com", + "role": "Developer" + } + ], + "description": "A fully-featured IMAP library -- without the PHP extension", + "homepage": "https://github.com/directorytree/imapengine", + "keywords": [ + "engine", + "imap", + "mail" + ], + "support": { + "issues": "https://github.com/DirectoryTree/ImapEngine/issues", + "source": "https://github.com/DirectoryTree/ImapEngine/tree/v1.19.0" + }, + "funding": [ + { + "url": "https://github.com/stevebauman", + "type": "github" + } + ], + "time": "2025-11-19T19:26:05+00:00" + }, { "name": "doctrine/dbal", "version": "3.10.4", @@ -1813,6 +1875,205 @@ ], "time": "2024-07-18T11:15:46+00:00" }, + { + "name": "illuminate/collections", + "version": "v12.41.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/collections.git", + "reference": "1cf6115f711ff775fcce547dee82d59cac33fedb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/collections/zipball/1cf6115f711ff775fcce547dee82d59cac33fedb", + "reference": "1cf6115f711ff775fcce547dee82d59cac33fedb", + "shasum": "" + }, + "require": { + "illuminate/conditionable": "^12.0", + "illuminate/contracts": "^12.0", + "illuminate/macroable": "^12.0", + "php": "^8.2", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33" + }, + "suggest": { + "illuminate/http": "Required to convert collections to API resources (^12.0).", + "symfony/var-dumper": "Required to use the dump method (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php", + "helpers.php" + ], + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Collections package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-11-29T13:55:43+00:00" + }, + { + "name": "illuminate/conditionable", + "version": "v12.41.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/conditionable.git", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/conditionable/zipball/ec677967c1f2faf90b8428919124d2184a4c9b49", + "reference": "ec677967c1f2faf90b8428919124d2184a4c9b49", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Conditionable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-05-13T15:08:45+00:00" + }, + { + "name": "illuminate/contracts", + "version": "v12.41.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/contracts.git", + "reference": "19e8938edb73047017cfbd443b96844b86da4a59" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/19e8938edb73047017cfbd443b96844b86da4a59", + "reference": "19e8938edb73047017cfbd443b96844b86da4a59", + "shasum": "" + }, + "require": { + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/simple-cache": "^1.0|^2.0|^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Contracts\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Contracts package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-11-26T21:36:01+00:00" + }, + { + "name": "illuminate/macroable", + "version": "v12.41.1", + "source": { + "type": "git", + "url": "https://github.com/illuminate/macroable.git", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/macroable/zipball/e862e5648ee34004fa56046b746f490dfa86c613", + "reference": "e862e5648ee34004fa56046b746f490dfa86c613", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Support\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Macroable package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2024-07-23T16:31:01+00:00" + }, { "name": "intervention/gif", "version": "4.2.2", @@ -2101,463 +2362,6 @@ }, "time": "2025-12-03T11:18:00+00:00" }, - { - "name": "laminas/laminas-loader", - "version": "2.11.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-loader.git", - "reference": "f2eedd3a6e774d965158fd11946bb1eba72e298c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-loader/zipball/f2eedd3a6e774d965158fd11946bb1eba72e298c", - "reference": "f2eedd3a6e774d965158fd11946bb1eba72e298c", - "shasum": "" - }, - "require": { - "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" - }, - "conflict": { - "zendframework/zend-loader": "*" - }, - "require-dev": { - "laminas/laminas-coding-standard": "~2.4.0", - "phpunit/phpunit": "~9.5.25" - }, - "type": "library", - "autoload": { - "psr-4": { - "Laminas\\Loader\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Autoloading and plugin loading strategies", - "homepage": "https://laminas.dev", - "keywords": [ - "laminas", - "loader" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-loader/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-loader/issues", - "rss": "https://github.com/laminas/laminas-loader/releases.atom", - "source": "https://github.com/laminas/laminas-loader" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "abandoned": true, - "time": "2024-10-16T09:06:57+00:00" - }, - { - "name": "laminas/laminas-mail", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/yurikuzn/laminas-mail.git", - "reference": "3befe2ed6193c10fb045369bd6473bd70fb9ceac" - }, - "require": { - "ext-iconv": "*", - "laminas/laminas-loader": "^2.9.0", - "laminas/laminas-mime": "dev-master#25659453400e0b2970e490dd0e4045f62acf5371", - "laminas/laminas-stdlib": "^3.17.0", - "laminas/laminas-validator": "^2.31.0", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "symfony/polyfill-intl-idn": "^1.27.0", - "symfony/polyfill-mbstring": "^1.27.0", - "webmozart/assert": "^1.11.0" - }, - "require-dev": { - "laminas/laminas-coding-standard": "~2.5.0", - "laminas/laminas-db": "^2.18", - "laminas/laminas-servicemanager": "^3.22.1", - "phpunit/phpunit": "^10.4.2", - "psalm/plugin-phpunit": "^0.18.4", - "symfony/process": "^6.3.4 || ^7.0.0", - "vimeo/psalm": "^5.15" - }, - "suggest": { - "laminas/laminas-servicemanager": "^3.21 when using SMTP to deliver messages" - }, - "default-branch": true, - "type": "library", - "extra": { - "laminas": { - "component": "Laminas\\Mail", - "config-provider": "Laminas\\Mail\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Laminas\\Mail\\": "src/" - } - }, - "autoload-dev": { - "psr-4": { - "LaminasTest\\Mail\\": "test/" - } - }, - "scripts": { - "check": [ - "@cs-check", - "@static-analysis", - "@test" - ], - "cs-check": [ - "phpcs" - ], - "cs-fix": [ - "phpcbf" - ], - "static-analysis": [ - "psalm --shepherd --stats" - ], - "test": [ - "phpunit --colors=always" - ], - "test-coverage": [ - "phpunit --colors=always --coverage-clover clover.xml" - ] - }, - "license": [ - "BSD-3-Clause" - ], - "description": "Provides generalized functionality to compose and send both text and MIME-compliant multipart e-mail messages", - "homepage": "https://laminas.dev", - "keywords": [ - "laminas", - "mail" - ], - "support": { - "issues": "https://github.com/laminas/laminas-mail/issues", - "forum": "https://discourse.laminas.dev", - "chat": "https://laminas.dev/chat", - "source": "https://github.com/laminas/laminas-mail", - "docs": "https://docs.laminas.dev/laminas-mail/", - "rss": "https://github.com/laminas/laminas-mail/releases.atom" - }, - "abandoned": "symfony/mailer", - "time": "2024-12-05T11:07:50+00:00" - }, - { - "name": "laminas/laminas-mime", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/yurikuzn/laminas-mime.git", - "reference": "25659453400e0b2970e490dd0e4045f62acf5371" - }, - "require": { - "laminas/laminas-stdlib": "^2.7 || ^3.0", - "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" - }, - "conflict": { - "zendframework/zend-mime": "*" - }, - "require-dev": { - "laminas/laminas-coding-standard": "~2.4.0", - "laminas/laminas-mail": "^2.19.0", - "phpunit/phpunit": "~9.5.25" - }, - "suggest": { - "laminas/laminas-mail": "Laminas\\Mail component" - }, - "default-branch": true, - "type": "library", - "autoload": { - "psr-4": { - "Laminas\\Mime\\": "src/" - } - }, - "autoload-dev": { - "files": [ - "test/TestAsset/Mail/Headers.php" - ], - "psr-4": { - "LaminasTest\\Mime\\": "test/" - } - }, - "scripts": { - "check": [ - "@cs-check", - "@test" - ], - "cs-check": [ - "phpcs" - ], - "cs-fix": [ - "phpcbf" - ], - "test": [ - "phpunit --colors=always" - ], - "test-coverage": [ - "phpunit --colors=always --coverage-clover clover.xml" - ] - }, - "license": [ - "BSD-3-Clause" - ], - "description": "Create and parse MIME messages and parts", - "homepage": "https://laminas.dev", - "keywords": [ - "laminas", - "mime" - ], - "support": { - "docs": "https://docs.laminas.dev/laminas-mime/", - "issues": "https://github.com/laminas/laminas-mime/issues", - "source": "https://github.com/laminas/laminas-mime", - "rss": "https://github.com/laminas/laminas-mime/releases.atom", - "chat": "https://laminas.dev/chat", - "forum": "https://discourse.laminas.dev" - }, - "abandoned": "symfony/mime", - "time": "2024-12-05T10:39:00+00:00" - }, - { - "name": "laminas/laminas-servicemanager", - "version": "3.23.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-servicemanager.git", - "reference": "a8640182b892b99767d54404d19c5c3b3699f79b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-servicemanager/zipball/a8640182b892b99767d54404d19c5c3b3699f79b", - "reference": "a8640182b892b99767d54404d19c5c3b3699f79b", - "shasum": "" - }, - "require": { - "laminas/laminas-stdlib": "^3.19", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/container": "^1.0" - }, - "conflict": { - "ext-psr": "*", - "laminas/laminas-code": "<4.10.0", - "zendframework/zend-code": "<3.3.1", - "zendframework/zend-servicemanager": "*" - }, - "provide": { - "psr/container-implementation": "^1.0" - }, - "replace": { - "container-interop/container-interop": "^1.2.0" - }, - "require-dev": { - "composer/package-versions-deprecated": "^1.11.99.5", - "friendsofphp/proxy-manager-lts": "^1.0.18", - "laminas/laminas-code": "^4.14.0", - "laminas/laminas-coding-standard": "~2.5.0", - "laminas/laminas-container-config-test": "^0.8", - "mikey179/vfsstream": "^1.6.12", - "phpbench/phpbench": "^1.3.1", - "phpunit/phpunit": "^10.5.36", - "psalm/plugin-phpunit": "^0.18.4", - "vimeo/psalm": "^5.26.1" - }, - "suggest": { - "friendsofphp/proxy-manager-lts": "ProxyManager ^2.1.1 to handle lazy initialization of services" - }, - "bin": [ - "bin/generate-deps-for-config-factory", - "bin/generate-factory-for-class" - ], - "type": "library", - "autoload": { - "files": [ - "src/autoload.php" - ], - "psr-4": { - "Laminas\\ServiceManager\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Factory-Driven Dependency Injection Container", - "homepage": "https://laminas.dev", - "keywords": [ - "PSR-11", - "dependency-injection", - "di", - "dic", - "laminas", - "service-manager", - "servicemanager" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-servicemanager/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-servicemanager/issues", - "rss": "https://github.com/laminas/laminas-servicemanager/releases.atom", - "source": "https://github.com/laminas/laminas-servicemanager" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2024-10-28T21:32:16+00:00" - }, - { - "name": "laminas/laminas-stdlib", - "version": "3.20.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-stdlib.git", - "reference": "8974a1213be42c3e2f70b2c27b17f910291ab2f4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-stdlib/zipball/8974a1213be42c3e2f70b2c27b17f910291ab2f4", - "reference": "8974a1213be42c3e2f70b2c27b17f910291ab2f4", - "shasum": "" - }, - "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" - }, - "conflict": { - "zendframework/zend-stdlib": "*" - }, - "require-dev": { - "laminas/laminas-coding-standard": "^3.0", - "phpbench/phpbench": "^1.3.1", - "phpunit/phpunit": "^10.5.38", - "psalm/plugin-phpunit": "^0.19.0", - "vimeo/psalm": "^5.26.1" - }, - "type": "library", - "autoload": { - "psr-4": { - "Laminas\\Stdlib\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "SPL extensions, array utilities, error handlers, and more", - "homepage": "https://laminas.dev", - "keywords": [ - "laminas", - "stdlib" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-stdlib/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-stdlib/issues", - "rss": "https://github.com/laminas/laminas-stdlib/releases.atom", - "source": "https://github.com/laminas/laminas-stdlib" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2024-10-29T13:46:07+00:00" - }, - { - "name": "laminas/laminas-validator", - "version": "2.64.2", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-validator.git", - "reference": "771e504760448ac7af660710237ceb93be602e08" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/771e504760448ac7af660710237ceb93be602e08", - "reference": "771e504760448ac7af660710237ceb93be602e08", - "shasum": "" - }, - "require": { - "laminas/laminas-servicemanager": "^3.21.0", - "laminas/laminas-stdlib": "^3.19", - "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", - "psr/http-message": "^1.0.1 || ^2.0.0" - }, - "conflict": { - "zendframework/zend-validator": "*" - }, - "require-dev": { - "laminas/laminas-coding-standard": "^2.5", - "laminas/laminas-db": "^2.20", - "laminas/laminas-filter": "^2.35.2", - "laminas/laminas-i18n": "^2.26.0", - "laminas/laminas-session": "^2.20", - "laminas/laminas-uri": "^2.11.0", - "phpunit/phpunit": "^10.5.20", - "psalm/plugin-phpunit": "^0.19.0", - "psr/http-client": "^1.0.3", - "psr/http-factory": "^1.1.0", - "vimeo/psalm": "^5.24.0" - }, - "suggest": { - "laminas/laminas-db": "Laminas\\Db component, required by the (No)RecordExists validator", - "laminas/laminas-filter": "Laminas\\Filter component, required by the Digits validator", - "laminas/laminas-i18n": "Laminas\\I18n component to allow translation of validation error messages", - "laminas/laminas-i18n-resources": "Translations of validator messages", - "laminas/laminas-servicemanager": "Laminas\\ServiceManager component to allow using the ValidatorPluginManager and validator chains", - "laminas/laminas-session": "Laminas\\Session component, ^2.8; required by the Csrf validator", - "laminas/laminas-uri": "Laminas\\Uri component, required by the Uri and Sitemap\\Loc validators", - "psr/http-message": "psr/http-message, required when validating PSR-7 UploadedFileInterface instances via the Upload and UploadFile validators" - }, - "type": "library", - "extra": { - "laminas": { - "component": "Laminas\\Validator", - "config-provider": "Laminas\\Validator\\ConfigProvider" - } - }, - "autoload": { - "psr-4": { - "Laminas\\Validator\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "Validation classes for a wide range of domains, and the ability to chain validators to create complex validation criteria", - "homepage": "https://laminas.dev", - "keywords": [ - "laminas", - "validator" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-validator/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-validator/issues", - "rss": "https://github.com/laminas/laminas-validator/releases.atom", - "source": "https://github.com/laminas/laminas-validator" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2024-11-26T21:29:17+00:00" - }, { "name": "laravel/serializable-closure", "version": "v1.3.7", @@ -7361,6 +7165,166 @@ ], "time": "2025-07-08T02:45:35+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, { "name": "symfony/process", "version": "v7.4.0", @@ -9915,12 +9879,10 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "laminas/laminas-mail": 20, - "laminas/laminas-mime": 20, - "laminas/laminas-ldap": 20, - "zordius/lightncandy": 20, "cboden/ratchet": 20, - "lasserafn/php-initial-avatar-generator": 20 + "laminas/laminas-ldap": 20, + "lasserafn/php-initial-avatar-generator": 20, + "zordius/lightncandy": 20 }, "prefer-stable": false, "prefer-lowest": false, @@ -9938,7 +9900,7 @@ "ext-pdo": "*", "ext-ctype": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.3.0" }, diff --git a/tests/unit/Espo/Core/Mail/Account/FetchDataTest.php b/tests/unit/Espo/Core/Mail/Account/FetchDataTest.php index 00e25f2c94..c3e6a5674a 100644 --- a/tests/unit/Espo/Core/Mail/Account/FetchDataTest.php +++ b/tests/unit/Espo/Core/Mail/Account/FetchDataTest.php @@ -48,9 +48,9 @@ class FetchDataTest extends \PHPUnit\Framework\TestCase $data = FetchData::fromRaw($raw); - $this->assertEquals('10', $data->getLastUniqueId('test')); + $this->assertEquals(10, $data->getLastUid('test')); $this->assertEquals('2022-01-01 00:00:00', $data->getLastDate('test')->toString()); - $this->assertEquals(null, $data->getLastUniqueId('not-existing')); + $this->assertEquals(null, $data->getLastUid('not-existing')); $this->assertEquals(null, $data->getLastDate('not-existing')); $this->assertEquals(false, $data->getForceByDate('test')); $this->assertEquals($raw, $data->getRaw()); @@ -58,10 +58,10 @@ class FetchDataTest extends \PHPUnit\Framework\TestCase $now = DateTime::createNow(); $data->setForceByDate('test', true); - $data->setLastUniqueId('test', '11'); + $data->setLastUid('test', 11); $data->setLastDate('test', $now); - $this->assertEquals('11', $data->getLastUniqueId('test')); + $this->assertEquals(11, $data->getLastUid('test')); $this->assertEquals(true, $data->getForceByDate('test')); $this->assertEquals($now, $data->getLastDate('test')); } From 5cbeea8119d141bfff034ee99be876ee98f28e42 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Fri, 5 Dec 2025 11:18:57 +0200 Subject: [PATCH 2/2] upgrade check --- upgrades/9.3/data.json | 3 + upgrades/9.3/scripts/BeforeUpgrade.php | 95 ++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 upgrades/9.3/data.json create mode 100644 upgrades/9.3/scripts/BeforeUpgrade.php diff --git a/upgrades/9.3/data.json b/upgrades/9.3/data.json new file mode 100644 index 0000000000..3d74c7fbee --- /dev/null +++ b/upgrades/9.3/data.json @@ -0,0 +1,3 @@ +{ + "manifest": {} +} diff --git a/upgrades/9.3/scripts/BeforeUpgrade.php b/upgrades/9.3/scripts/BeforeUpgrade.php new file mode 100644 index 0000000000..a94a7b1755 --- /dev/null +++ b/upgrades/9.3/scripts/BeforeUpgrade.php @@ -0,0 +1,95 @@ +. + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +use Espo\Core\Container; +use Espo\Entities\Extension; +use Espo\ORM\EntityManager; + +/** @noinspection PhpMultipleClassDeclarationsInspection */ +class BeforeUpgrade +{ + private ?Container $container = null; + + /** + * @throws Exception + */ + public function run(Container $container): void + { + $this->container = $container; + + $this->processCheckExtensions(); + } + + /** + * @throws Error + */ + private function processCheckExtensions(): void + { + $errorMessageList = []; + + $this->processCheckExtension('Google Integration', '1.8.3', $errorMessageList); + $this->processCheckExtension('Outlook Integration', '1.6.12', $errorMessageList); + + if (!count($errorMessageList)) { + return; + } + + $message = implode("\n\n", $errorMessageList); + + throw new Error($message); + } + + private function processCheckExtension(string $name, string $minVersion, array &$errorMessageList): void + { + $em = $this->container->getByClass(EntityManager::class); + + $extension = $em->getRDBRepository(Extension::ENTITY_TYPE) + ->where([ + 'name' => $name, + 'isInstalled' => true, + ]) + ->findOne(); + + if (!$extension) { + return; + } + + $version = $extension->get('version'); + + if (version_compare($version, $minVersion, '>=')) { + return; + } + + $message = + "EspoCRM 9.3 is not compatible with '$name' extension of versions lower than $minVersion. " . + "You need to upgrade the extension."; + + $errorMessageList[] = $message; + } +}