chore: Update copyright year from 2025 to 2026 across core files

- Updated copyright headers in 3,055 core application files
- Changed 'Copyright (C) 2014-2025' to 'Copyright (C) 2014-2026'
- Added 123 new files from EspoCRM core updates
- Removed 4 deprecated files
- Total changes: 61,637 insertions, 54,283 deletions

This is a routine maintenance update for the new year 2026.
This commit is contained in:
2026-02-07 16:05:21 +01:00
parent 6a8a4a2882
commit 127fa6503b
6468 changed files with 564781 additions and 31179 deletions

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -164,4 +164,12 @@ interface Account
* Get the last connection time;
*/
public function getConnectedAt(): ?DateTime;
/**
* Get an email folder mapped to the email folder.
*
* @since 9.3.0
*/
public function getMappedEmailFolder(string $folder): ?Link;
}

View File

@@ -0,0 +1,81 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* 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;
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\Params;
/**
* @since 9.3.0
*/
class CommonStorageFactory
{
public function __construct(
private InjectableFactory $injectableFactory,
) {}
public function create(Params $params): DirectoryTreeStorage
{
$handlerClassName = $params->getImapHandlerClassName();
if ($handlerClassName && $params->getId()) {
$handler = $this->injectableFactory->create($handlerClassName);
if ($handler instanceof Handler || method_exists($handler, 'handle')) {
$params = $handler->handle($params, $params->getId());
}
}
$encryption = match ($params->getSecurity()) {
Params::SECURITY_SSL => 'ssl',
Params::SECURITY_START_TLS => 'starttls',
default => null,
};
$authentication = $params->getAuthMechanism() === Params::AUTH_MECHANISM_XOAUTH ?
'oauth' : 'plain';
$config = [
'host' => $params->getHost(),
'port' => $params->getPort(),
'username' => $params->getUsername(),
'password' => $params->getPassword(),
'encryption' => $encryption,
'authentication' => $authentication,
];
$mailbox = new Mailbox($config);
return new DirectoryTreeStorage($mailbox);
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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) [];

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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;
@@ -100,12 +97,13 @@ class Fetcher
/**
* @param Collection<EmailFilter> $filterList
* @throws Error
* @throws ImapError
*/
private function fetchFolder(
Account $account,
string $folderOriginal,
Storage $storage,
Collection $filterList
Collection $filterList,
): void {
$fetchData = $account->getFetchData();
@@ -115,60 +113,59 @@ 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(
$account,
$storage,
$lastUniqueId,
$lastDate,
$forceByDate
$ids = $this->fetchIds(
account: $account,
storage: $storage,
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($account, $storage, $id, $filterList);
$email = $this->fetchEmail(
account: $account,
storage: $storage,
id: $id,
filterList: $filterList,
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();
@@ -181,7 +178,7 @@ class Fetcher
break;
}
$counter++;
$counter ++;
}
if ($forceByDate) {
@@ -189,25 +186,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);
@@ -219,21 +212,22 @@ class Fetcher
/**
* @return int[]
* @throws Error
* @throws ImapError
*/
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()) {
@@ -242,7 +236,7 @@ class Fetcher
$fetchSince = $account->getFetchSince()->toDateTime();
return $storage->getIdsSinceDate(
return $storage->getUidsSinceDate(
DateTimeField::fromDateTime($fetchSince)
);
}
@@ -254,7 +248,8 @@ class Fetcher
Account $account,
Storage $storage,
int $id,
Collection $filterList
Collection $filterList,
?string $mappedEmailFolderId,
): ?Email {
$teamIdList = $account->getTeams()->getIdList();
@@ -265,11 +260,7 @@ class Fetcher
$fetchOnlyHeader = $this->checkFetchOnlyHeader($storage, $id);
$folderData = [];
if ($userId && $account->getEmailFolder()) {
$folderData[$userId] = $account->getEmailFolder()->getId();
}
$folderData = $this->prepareFolderData($userId, $mappedEmailFolderId, $account);
$flags = null;
@@ -286,7 +277,12 @@ class Fetcher
->withGroupEmailFolderId($groupEmailFolderId);
try {
$message = new MessageWrapper($id, $storage, $parser);
$message = new MessageWrapper(
id: $id,
storage: $storage,
parser: $parser,
peek: $account->keepFetchedEmailsUnread(),
);
$hookResult = null;
@@ -313,7 +309,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(
@@ -433,10 +429,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();
@@ -447,13 +443,22 @@ class Fetcher
}
/**
* @param string[] $flags
* @return string[]
* @return array<string, string>
*/
private static function flagsWithoutRecent(array $flags): array
private function prepareFolderData(?string $userId, ?string $mappedEmailFolderId, Account $account): array
{
return array_values(
array_diff($flags, [Flag::RECENT])
);
if (!$userId) {
return [];
}
$folderData = [];
if ($mappedEmailFolderId) {
$folderData[$userId] = $mappedEmailFolderId;
} else if ($account->getEmailFolder()) {
$folderData[$userId] = $account->getEmailFolder()->getId();
}
return $folderData;
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -381,4 +381,9 @@ class Account implements AccountInterface
/** @var DateTime */
return $this->entity->getValueObject('connectedAt');
}
public function getMappedEmailFolder(string $folder): ?Link
{
return null;
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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') &&

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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);
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -29,31 +29,20 @@
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\ImapError;
use Espo\Core\Mail\Exceptions\NoImap;
use Espo\Core\Mail\Mail\Storage\Imap;
use Espo\Core\Utils\Log;
use Espo\Core\InjectableFactory;
use Espo\ORM\Name\Attribute;
use Laminas\Mail\Storage\Exception\RuntimeException;
use Laminas\Mail\Storage\Exception\InvalidArgumentException;
use Laminas\Mail\Protocol\Exception\RuntimeException as ProtocolRuntimeException;
use Throwable;
class StorageFactory implements StorageFactoryInterface
{
public function __construct(
private Log $log,
private InjectableFactory $injectableFactory
private CommonStorageFactory $commonStorageFactory,
) {}
public function create(Account $account): LaminasStorage
public function create(Account $account): Storage
{
$imapParams = $account->getImapParams();
@@ -74,62 +63,8 @@ class StorageFactory implements StorageFactoryInterface
return $this->createWithParams($params);
}
public function createWithParams(Params $params): LaminasStorage
public function createWithParams(Params $params): Storage
{
$rawParams = [
'host' => $params->getHost(),
'port' => $params->getPort(),
'username' => $params->getUsername(),
'password' => $params->getPassword(),
'imapHandler' => $params->getImapHandlerClassName(),
Attribute::ID => $params->getId(),
];
if ($params->getSecurity()) {
$rawParams['security'] = $params->getSecurity();
}
$imapParams = null;
$handlerClassName = $rawParams['imapHandler'] ?? null;
$handler = null;
if ($handlerClassName && !empty($rawParams['id'])) {
try {
$handler = $this->injectableFactory->create($handlerClassName);
} catch (Throwable $e) {
$this->log->error("InboundEmail: Could not create Imap Handler. Error: " . $e->getMessage());
}
if ($handler && method_exists($handler, 'prepareProtocol')) {
// for backward compatibility
$rawParams['ssl'] = $rawParams['security'] ?? null;
// @todo Incorporate an interface `LaminasProtocolPreparator`.
$imapParams = $handler->prepareProtocol($rawParams['id'], $rawParams);
}
}
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 (RuntimeException|InvalidArgumentException|ProtocolRuntimeException $e) {
throw new ImapError($e->getMessage(), 0, $e);
}
return new LaminasStorage($storage);
return $this->commonStorageFactory->create($params);
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -201,6 +201,11 @@ class Account implements AccountInterface
return $this->entity->getEmailFolder();
}
public function getMappedEmailFolder(string $folder): ?Link
{
return $this->entity->getMappedEmailFolder($folder);
}
/**
* @return string[]
*/

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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);

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -29,32 +29,22 @@
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\ImapError;
use Espo\Core\Mail\Exceptions\NoImap;
use Espo\Core\Mail\Mail\Storage\Imap;
use Espo\Core\Mail\Account\Storage\LaminasStorage;
use Espo\Core\Utils\Log;
use Espo\Core\InjectableFactory;
use Espo\ORM\Name\Attribute;
use Laminas\Mail\Protocol\Exception\RuntimeException as ProtocolRuntimeException;
use Laminas\Mail\Storage\Exception\InvalidArgumentException;
use Laminas\Mail\Storage\Exception\RuntimeException;
use LogicException;
use Throwable;
class StorageFactory implements StorageFactoryInterface
{
public function __construct(
private Log $log,
private InjectableFactory $injectableFactory,
private CommonStorageFactory $commonStorageFactory,
) {}
public function create(Account $account): LaminasStorage
public function create(Account $account): Storage
{
$userLink = $account->getUser();
@@ -85,65 +75,8 @@ class StorageFactory implements StorageFactoryInterface
return $this->createWithParams($params);
}
public function createWithParams(Params $params): LaminasStorage
public function createWithParams(Params $params): Storage
{
$rawParams = [
'host' => $params->getHost(),
'port' => $params->getPort(),
'username' => $params->getUsername(),
'password' => $params->getPassword(),
'emailAddress' => $params->getEmailAddress(),
'userId' => $params->getUserId(),
'imapHandler' => $params->getImapHandlerClassName(),
Attribute::ID => $params->getId(),
];
if ($params->getSecurity()) {
$rawParams['security'] = $params->getSecurity();
}
/** @var ?class-string $handlerClassName */
$handlerClassName = $rawParams['imapHandler'] ?? null;
$handler = null;
$imapParams = null;
if ($handlerClassName && !empty($rawParams['id'])) {
try {
$handler = $this->injectableFactory->create($handlerClassName);
} catch (Throwable $e) {
$this->log->error(
"EmailAccount: Could not create Imap Handler. Error: " . $e->getMessage()
);
}
if ($handler && method_exists($handler, 'prepareProtocol')) {
// for backward compatibility
$rawParams['ssl'] = $rawParams['security'] ?? null;
$imapParams = $handler->prepareProtocol($rawParams['id'], $rawParams);
}
}
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 (RuntimeException|InvalidArgumentException|ProtocolRuntimeException $e) {
throw new ImapError($e->getMessage(), 0, $e);
}
return new LaminasStorage($storage);
return $this->commonStorageFactory->create($params);
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -30,49 +30,57 @@
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
* @todo Move to the Message interface. Call from the MessageWrapper.
*
* @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;
public function getRawContent(int $id, bool $peek): 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 +91,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;
}

View File

@@ -0,0 +1,326 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* 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, bool $peek): string
{
$folder = $this->getSelectedFolder();
try {
$message = $folder->messages()
->withHeaders()
->withFlags()
->withBody()
->find($id);
if ($message instanceof Message && !$peek) {
$message->markSeen();
}
} 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();
}
/**
* @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);
$query->setFetchOrderAsc();
try {
$query->each(function (MessageInterface $message) use (&$output) {
$output[] = $message->uid();
});
} catch (CommonException $e) {
throw new ImapError($e->getMessage(), previous: $e);
}
// May return wrong items. Do not return one with matching id.
$output = array_filter($output, fn ($it) => $it > $id);
$output = array_values($output);
// Otherwise, it's in reverse order.
sort($output);
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);
$query->setFetchOrderAsc();
try {
$query->each(function (MessageInterface $message) use (&$output) {
$output[] = $message->uid();
});
} catch (CommonException $e) {
throw new ImapError($e->getMessage(), previous: $e);
}
// Otherwise, it's in reverse order.
sort($output);
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[] = mb_convert_encoding($folder->path(), 'UTF-8', 'UTF7-IMAP');
}
} 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;
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -27,7 +27,15 @@
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\Mail\Mail;
namespace Espo\Core\Mail\Account\Storage;
class Headers extends \Laminas\Mail\Headers
{}
/**
* Handle storage parameters.
* To be used by extensions.
*
* @since 9.3.0
*/
interface Handler
{
public function handle(Params $params, string $id): Params;
}

View File

@@ -1,134 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* 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);
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -36,11 +36,23 @@ 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<object> */
private ?string $imapHandlerClassName;
/** @since 9.3.0 */
public const string AUTH_MECHANISM_PLAIN = 'plain';
/** @since 9.3.0 */
public const string AUTH_MECHANISM_XOAUTH = 'xoauth';
/**
* @param ?class-string<object> $imapHandlerClassName
* @param self::AUTH_MECHANISM_* $authMechanism
*/
public function __construct(
private ?string $host,
@@ -51,7 +63,8 @@ class Params
?string $imapHandlerClassName,
private ?string $id,
private ?string $userId,
private ?string $emailAddress
private ?string $emailAddress,
private string $authMechanism = self::AUTH_MECHANISM_PLAIN,
) {
$this->imapHandlerClassName = $imapHandlerClassName;
}
@@ -109,6 +122,38 @@ class Params
return $this->emailAddress;
}
/**
* @return self::AUTH_MECHANISM_*
* @since 9.3.0
*/
public function getAuthMechanism(): string
{
return $this->authMechanism;
}
/**
* @param self::AUTH_MECHANISM_* $authMechanism
* @since 9.3.0
*/
public function withAuthMechanism(string $authMechanism): self
{
$obj = clone $this;
$obj->authMechanism = $authMechanism;
return $obj;
}
/**
* @since 9.3.0
*/
public function withUsername(?string $username): self
{
$obj = clone $this;
$obj->username = $username;
return $obj;
}
public function withPassword(#[SensitiveParameter] ?string $password): self
{
$obj = clone $this;

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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.
*

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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;
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -1,81 +0,0 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* 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,
];
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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;

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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,11 +43,15 @@ class MessageWrapper implements Message
/** @var ?string[] */
private ?array $flagList = null;
/**
* @throws ImapError
*/
public function __construct(
private int $id,
private ?Storage $storage = null,
private ?Parser $parser = null,
private ?string $fullRawContent = null
private ?string $fullRawContent = null,
private bool $peek = false,
) {
if ($storage) {
$data = $storage->getHeaderAndFlags($id);
@@ -108,7 +113,7 @@ class MessageWrapper implements Message
throw new RuntimeException();
}
$this->rawContent = $this->storage->getRawContent($this->id);
$this->rawContent = $this->storage->getRawContent($this->id, $this->peek);
}
return $this->rawContent ?? '';

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -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<string, mixed> */
private array $overrideParams = [];
private ?string $envelopeFromAddress = null;
private ?LaminasMessage $laminasMessage = null;
/** @var ?iterable<Attachment> */
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]);
}
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -53,15 +53,12 @@ class DefaultTransportPreparator implements TransportPreparator
// 'SSL' is treated as implicit SSL/TLS. 'TLS' is treated as STARTTLS.
// STARTTLS is the most common method.
$scheme = $smtpParams->getSecurity() === 'SSL' ? 'smtps' : 'smtp';
$scheme = $smtpParams->getSecurity() === SmtpParams::SECURITY_SSL_TLS ? 'smtps' : 'smtp';
if ($smtpParams->getSecurity() === 'TLS' && !defined('OPENSSL_VERSION_NUMBER')) {
if ($smtpParams->getSecurity() === SmtpParams::SECURITY_START_TLS && !defined('OPENSSL_VERSION_NUMBER')) {
throw new RuntimeException("OpenSSL is not available.");
}
// @todo Use `auto_tls=false` if no security when Symfony v7.1 is installed.
// @todo If starttls, it should be enforced.
$transport = (new EsmtpTransportFactory())
->create(
new Dsn(
@@ -71,6 +68,10 @@ class DefaultTransportPreparator implements TransportPreparator
)
);
if (!$smtpParams->getSecurity() && $transport instanceof EsmtpTransport) {
$transport->setAutoTls(false);
}
if (!$transport instanceof EsmtpTransport) {
throw new RuntimeException();
}

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify

View File

@@ -3,7 +3,7 @@
* This file is part of EspoCRM.
*
* EspoCRM Open Source CRM application.
* Copyright (C) 2014-2025 EspoCRM, Inc.
* Copyright (C) 2014-2026 EspoCRM, Inc.
* Website: https://www.espocrm.com
*
* This program is free software: you can redistribute it and/or modify
@@ -57,6 +57,9 @@ class SmtpParams
public const AUTH_MECHANISM_PLAIN = 'plain';
public const AUTH_MECHANISM_XOAUTH = 'xoauth';
public const string SECURITY_SSL_TLS = 'SSL';
public const string SECURITY_START_TLS = 'TLS';
/** @var string[] */
private array $paramList = [
'server',