Initial commit
This commit is contained in:
167
application/Espo/Core/Mail/Account/Account.php
Normal file
167
application/Espo/Core/Mail/Account/Account.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Field\Date;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Field\Link;
|
||||
use Espo\Core\Field\LinkMultiple;
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Espo\Entities\Email;
|
||||
|
||||
interface Account
|
||||
{
|
||||
/**
|
||||
* Update fetch-data.
|
||||
*/
|
||||
public function updateFetchData(FetchData $fetchData): void;
|
||||
|
||||
/**
|
||||
* Update connected-at.
|
||||
*/
|
||||
public function updateConnectedAt(): void;
|
||||
|
||||
/**
|
||||
* Relate an email with the account.
|
||||
*/
|
||||
public function relateEmail(Email $email): void;
|
||||
|
||||
/**
|
||||
* Max number of emails to be fetched per iteration.
|
||||
*/
|
||||
public function getPortionLimit(): int;
|
||||
|
||||
/**
|
||||
* Is available for fetching.
|
||||
*/
|
||||
public function isAvailableForFetching(): bool;
|
||||
|
||||
/**
|
||||
* An email address of the account.
|
||||
*/
|
||||
public function getEmailAddress(): ?string;
|
||||
|
||||
/**
|
||||
* A user fetched emails will be assigned to (through the `assignedUsers` field).
|
||||
*/
|
||||
public function getAssignedUser(): ?Link;
|
||||
|
||||
/**
|
||||
* A user the account belongs to.
|
||||
*/
|
||||
public function getUser(): ?Link;
|
||||
|
||||
/**
|
||||
* Users email should be related to (put into inbox).
|
||||
*/
|
||||
public function getUsers(): LinkMultiple;
|
||||
|
||||
/**
|
||||
* Teams email should be related to.
|
||||
*/
|
||||
public function getTeams(): LinkMultiple;
|
||||
|
||||
/**
|
||||
* Fetched emails won't be marked as read upon fetching.
|
||||
*/
|
||||
public function keepFetchedEmailsUnread(): bool;
|
||||
|
||||
/**
|
||||
* Get fetch-data.
|
||||
*/
|
||||
public function getFetchData(): FetchData;
|
||||
|
||||
/**
|
||||
* Fetch email since a specific date.
|
||||
*/
|
||||
public function getFetchSince(): ?Date;
|
||||
|
||||
/**
|
||||
* A folder fetched emails should be put into.
|
||||
*/
|
||||
public function getEmailFolder(): ?Link;
|
||||
|
||||
/**
|
||||
* A group folder fetched emails should be put into.
|
||||
*/
|
||||
public function getGroupEmailFolder(): ?Link;
|
||||
|
||||
/**
|
||||
* Folders to fetch from.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMonitoredFolderList(): array;
|
||||
|
||||
/**
|
||||
* Gen an ID.
|
||||
*/
|
||||
public function getId(): ?string;
|
||||
|
||||
/**
|
||||
* Get an entity type.
|
||||
*/
|
||||
public function getEntityType(): string;
|
||||
|
||||
/**
|
||||
* Get IMAP params.
|
||||
*/
|
||||
public function getImapParams(): ?ImapParams;
|
||||
|
||||
/**
|
||||
* @return ?class-string<object>
|
||||
*/
|
||||
public function getImapHandlerClassName(): ?string;
|
||||
|
||||
/**
|
||||
* Get a SENT folder.
|
||||
*/
|
||||
public function getSentFolder(): ?string;
|
||||
|
||||
/**
|
||||
* Is available for sending.
|
||||
*/
|
||||
public function isAvailableForSending(): bool;
|
||||
|
||||
/**
|
||||
* Get SMTP params.
|
||||
*/
|
||||
public function getSmtpParams(): ?SmtpParams;
|
||||
|
||||
/**
|
||||
* Store sent emails on IMAP.
|
||||
*/
|
||||
public function storeSentEmails(): bool;
|
||||
|
||||
/**
|
||||
* Get the last connection time;
|
||||
*/
|
||||
public function getConnectedAt(): ?DateTime;
|
||||
}
|
||||
119
application/Espo/Core/Mail/Account/FetchData.php
Normal file
119
application/Espo/Core/Mail/Account/FetchData.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Utils\ObjectUtil;
|
||||
use Espo\Core\Field\DateTime;
|
||||
|
||||
use stdClass;
|
||||
use RuntimeException;
|
||||
|
||||
class FetchData
|
||||
{
|
||||
private stdClass $data;
|
||||
|
||||
public function __construct(stdClass $data)
|
||||
{
|
||||
$this->data = ObjectUtil::clone($data);
|
||||
}
|
||||
|
||||
public static function fromRaw(stdClass $data): self
|
||||
{
|
||||
return new self($data);
|
||||
}
|
||||
|
||||
public function getRaw(): stdClass
|
||||
{
|
||||
return ObjectUtil::clone($this->data);
|
||||
}
|
||||
|
||||
public function getLastUniqueId(string $folder): ?string
|
||||
{
|
||||
return $this->data->lastUID->$folder ?? null;
|
||||
}
|
||||
|
||||
public function getLastDate(string $folder): ?DateTime
|
||||
{
|
||||
$value = $this->data->lastDate->$folder ?? null;
|
||||
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For backward compatibility.
|
||||
if ($value === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_string($value)) {
|
||||
throw new RuntimeException("Bad value in fetch-data.");
|
||||
}
|
||||
|
||||
return DateTime::fromString($value);
|
||||
}
|
||||
|
||||
public function getForceByDate(string $folder): bool
|
||||
{
|
||||
return $this->data->byDate->$folder ?? false;
|
||||
}
|
||||
|
||||
public function setLastUniqueId(string $folder, ?string $uniqueId): void
|
||||
{
|
||||
if (!property_exists($this->data, 'lastUID')) {
|
||||
$this->data->lastUID = (object) [];
|
||||
}
|
||||
|
||||
$this->data->lastUID->$folder = $uniqueId;
|
||||
}
|
||||
|
||||
public function setLastDate(string $folder, ?DateTime $lastDate): void
|
||||
{
|
||||
if (!property_exists($this->data, 'lastDate')) {
|
||||
$this->data->lastDate = (object) [];
|
||||
}
|
||||
|
||||
if ($lastDate === null) {
|
||||
$this->data->lastDate->$folder = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->data->lastDate->$folder = $lastDate->toString();
|
||||
}
|
||||
|
||||
public function setForceByDate(string $folder, bool $forceByDate): void
|
||||
{
|
||||
if (!property_exists($this->data, 'byDate')) {
|
||||
$this->data->byDate = (object) [];
|
||||
}
|
||||
|
||||
$this->data->byDate->$folder = $forceByDate;
|
||||
}
|
||||
}
|
||||
459
application/Espo/Core/Mail/Account/Fetcher.php
Normal file
459
application/Espo/Core/Mail/Account/Fetcher.php
Normal file
@@ -0,0 +1,459 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\Mail\Account\Storage\Flag;
|
||||
use Espo\Core\Mail\Exceptions\ImapError;
|
||||
use Espo\Core\Mail\Exceptions\NoImap;
|
||||
use Espo\Core\Mail\Importer;
|
||||
use Espo\Core\Mail\Importer\Data as ImporterData;
|
||||
use Espo\Core\Mail\ParserFactory;
|
||||
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;
|
||||
use Espo\Entities\EmailFilter;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\ORM\Collection;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Query\Part\Expression;
|
||||
use Espo\ORM\Query\Part\Order;
|
||||
|
||||
use Throwable;
|
||||
use DateTime;
|
||||
|
||||
class Fetcher
|
||||
{
|
||||
public function __construct(
|
||||
private Importer $importer,
|
||||
private StorageFactory $storageFactory,
|
||||
private Config $config,
|
||||
private Log $log,
|
||||
private EntityManager $entityManager,
|
||||
private ParserFactory $parserFactory,
|
||||
private ?BeforeFetchHook $beforeFetchHook,
|
||||
private ?AfterFetchHook $afterFetchHook
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
* @throws ImapError
|
||||
* @throws NoImap
|
||||
*/
|
||||
public function fetch(Account $account): void
|
||||
{
|
||||
if (!$account->isAvailableForFetching()) {
|
||||
throw new Error("{$account->getEntityType()} {$account->getId()} is not active.");
|
||||
}
|
||||
|
||||
$monitoredFolderList = $account->getMonitoredFolderList();
|
||||
|
||||
if (count($monitoredFolderList) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filterList = $this->getFilterList($account);
|
||||
|
||||
$storage = $this->storageFactory->create($account);
|
||||
|
||||
foreach ($monitoredFolderList as $folder) {
|
||||
$this->fetchFolder($account, $folder, $storage, $filterList);
|
||||
}
|
||||
|
||||
$storage->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<EmailFilter> $filterList
|
||||
* @throws Error
|
||||
*/
|
||||
private function fetchFolder(
|
||||
Account $account,
|
||||
string $folderOriginal,
|
||||
Storage $storage,
|
||||
Collection $filterList
|
||||
): void {
|
||||
|
||||
$fetchData = $account->getFetchData();
|
||||
|
||||
$folder = mb_convert_encoding($folderOriginal, 'UTF7-IMAP', 'UTF-8');
|
||||
|
||||
try {
|
||||
$storage->selectFolder($folderOriginal);
|
||||
} catch (Throwable $e) {
|
||||
$this->log->error(
|
||||
"{$account->getEntityType()} {$account->getId()}, " .
|
||||
"could not select folder '$folder'; [{$e->getCode()}] {$e->getMessage()}"
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$lastUniqueId = $fetchData->getLastUniqueId($folder);
|
||||
$lastDate = $fetchData->getLastDate($folder);
|
||||
$forceByDate = $fetchData->getForceByDate($folder);
|
||||
|
||||
$portionLimit = $forceByDate ? 0 : $account->getPortionLimit();
|
||||
|
||||
$previousLastUniqueId = $lastUniqueId;
|
||||
|
||||
$idList = $this->getIdList(
|
||||
$account,
|
||||
$storage,
|
||||
$lastUniqueId,
|
||||
$lastDate,
|
||||
$forceByDate
|
||||
);
|
||||
|
||||
if (count($idList) === 1 && $lastUniqueId) {
|
||||
if ($storage->getUniqueId($idList[0]) === $lastUniqueId) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$counter = 0;
|
||||
|
||||
foreach ($idList as $id) {
|
||||
if ($counter == count($idList) - 1) {
|
||||
$lastUniqueId = $storage->getUniqueId($id);
|
||||
}
|
||||
|
||||
if ($forceByDate && $previousLastUniqueId) {
|
||||
$uid = $storage->getUniqueId($id);
|
||||
|
||||
if ((int) $uid <= (int) $previousLastUniqueId) {
|
||||
$counter++;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$email = $this->fetchEmail($account, $storage, $id, $filterList);
|
||||
|
||||
$isLast = $counter === count($idList) - 1;
|
||||
$isLastInPortion = $counter === $portionLimit - 1;
|
||||
|
||||
if ($isLast || $isLastInPortion) {
|
||||
$lastUniqueId = $storage->getUniqueId($id);
|
||||
|
||||
if ($email && $email->getDateSent()) {
|
||||
$lastDate = $email->getDateSent();
|
||||
|
||||
if ($lastDate->toTimestamp() >= (new DateTime())->getTimestamp()) {
|
||||
$lastDate = DateTimeField::createNow();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$counter++;
|
||||
}
|
||||
|
||||
if ($forceByDate) {
|
||||
$lastDate = DateTimeField::createNow();
|
||||
}
|
||||
|
||||
$fetchData->setLastDate($folder, $lastDate);
|
||||
$fetchData->setLastUniqueId($folder, $lastUniqueId);
|
||||
|
||||
if ($forceByDate && $previousLastUniqueId) {
|
||||
$idList = $storage->getIdsFromUniqueId($previousLastUniqueId);
|
||||
|
||||
if (count($idList)) {
|
||||
$uid1 = $storage->getUniqueId($idList[0]);
|
||||
|
||||
if ((int) $uid1 > (int) $previousLastUniqueId) {
|
||||
$fetchData->setForceByDate($folder, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!$forceByDate &&
|
||||
$previousLastUniqueId &&
|
||||
count($idList) &&
|
||||
(int) $previousLastUniqueId >= (int) $lastUniqueId
|
||||
) {
|
||||
// Handling broken numbering. Next time fetch since the last date rather than the last UID.
|
||||
$fetchData->setForceByDate($folder, true);
|
||||
}
|
||||
|
||||
$account->updateFetchData($fetchData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
* @throws Error
|
||||
*/
|
||||
private function getIdList(
|
||||
Account $account,
|
||||
Storage $storage,
|
||||
?string $lastUID,
|
||||
?DateTimeField $lastDate,
|
||||
bool $forceByDate
|
||||
): array {
|
||||
|
||||
if (!empty($lastUID) && !$forceByDate) {
|
||||
return $storage->getIdsFromUniqueId($lastUID);
|
||||
}
|
||||
|
||||
if ($lastDate) {
|
||||
return $storage->getIdsSinceDate($lastDate);
|
||||
}
|
||||
|
||||
if (!$account->getFetchSince()) {
|
||||
throw new Error("{$account->getEntityType()} {$account->getId()}, no fetch-since.");
|
||||
}
|
||||
|
||||
$fetchSince = $account->getFetchSince()->toDateTime();
|
||||
|
||||
return $storage->getIdsSinceDate(
|
||||
DateTimeField::fromDateTime($fetchSince)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<EmailFilter> $filterList
|
||||
*/
|
||||
private function fetchEmail(
|
||||
Account $account,
|
||||
Storage $storage,
|
||||
int $id,
|
||||
Collection $filterList
|
||||
): ?Email {
|
||||
|
||||
$teamIdList = $account->getTeams()->getIdList();
|
||||
$userIdList = $account->getUsers()->getIdList();
|
||||
$userId = $account->getUser() ? $account->getUser()->getId() : null;
|
||||
$assignedUserId = $account->getAssignedUser() ? $account->getAssignedUser()->getId() : null;
|
||||
$groupEmailFolderId = $account->getGroupEmailFolder() ? $account->getGroupEmailFolder()->getId() : null;
|
||||
|
||||
$fetchOnlyHeader = $this->checkFetchOnlyHeader($storage, $id);
|
||||
|
||||
$folderData = [];
|
||||
|
||||
if ($userId && $account->getEmailFolder()) {
|
||||
$folderData[$userId] = $account->getEmailFolder()->getId();
|
||||
}
|
||||
|
||||
$flags = null;
|
||||
|
||||
$parser = $this->parserFactory->create();
|
||||
|
||||
$importerData = ImporterData
|
||||
::create()
|
||||
->withTeamIdList($teamIdList)
|
||||
->withFilterList($filterList)
|
||||
->withFetchOnlyHeader($fetchOnlyHeader)
|
||||
->withFolderData($folderData)
|
||||
->withUserIdList($userIdList)
|
||||
->withAssignedUserId($assignedUserId)
|
||||
->withGroupEmailFolderId($groupEmailFolderId);
|
||||
|
||||
try {
|
||||
$message = new MessageWrapper($id, $storage, $parser);
|
||||
|
||||
$hookResult = null;
|
||||
|
||||
if ($this->beforeFetchHook) {
|
||||
$hookResult = $this->processBeforeFetchHook($account, $message);
|
||||
}
|
||||
|
||||
if ($hookResult && $hookResult->toSkip()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($message->isFetched() && $account->keepFetchedEmailsUnread()) {
|
||||
$flags = $message->getFlags();
|
||||
}
|
||||
|
||||
$email = $this->importMessage($account, $message, $importerData);
|
||||
|
||||
if (!$email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
$account->keepFetchedEmailsUnread() &&
|
||||
$flags !== null &&
|
||||
!in_array(Flag::SEEN, $flags)
|
||||
) {
|
||||
$storage->setFlags($id, self::flagsWithoutRecent($flags));
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
$this->log->error(
|
||||
"{$account->getEntityType()} {$account->getId()}, get message; " .
|
||||
"{$e->getCode()} {$e->getMessage()}"
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$account->relateEmail($email);
|
||||
|
||||
if (!$this->afterFetchHook) {
|
||||
return $email;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->afterFetchHook->process(
|
||||
$account,
|
||||
$email,
|
||||
$hookResult ?? BeforeFetchHookResult::create()
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
$this->log->error(
|
||||
"{$account->getEntityType()} {$account->getId()}, after-fetch hook; " .
|
||||
"{$e->getCode()} {$e->getMessage()}"
|
||||
);
|
||||
}
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
private function processBeforeFetchHook(Account $account, MessageWrapper $message): BeforeFetchHookResult
|
||||
{
|
||||
assert($this->beforeFetchHook !== null);
|
||||
|
||||
try {
|
||||
return $this->beforeFetchHook->process($account, $message);
|
||||
} catch (Throwable $e) {
|
||||
$this->log->error(
|
||||
"{$account->getEntityType()} {$account->getId()}, before-fetch hook; " .
|
||||
"{$e->getCode()} {$e->getMessage()}"
|
||||
);
|
||||
}
|
||||
|
||||
return BeforeFetchHookResult::create()->withToSkip();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<EmailFilter>
|
||||
*/
|
||||
private function getFilterList(Account $account): Collection
|
||||
{
|
||||
$actionList = [EmailFilter::ACTION_SKIP];
|
||||
|
||||
if ($account->getEntityType() === InboundEmail::ENTITY_TYPE) {
|
||||
$actionList[] = EmailFilter::ACTION_MOVE_TO_GROUP_FOLDER;
|
||||
}
|
||||
|
||||
$builder = $this->entityManager
|
||||
->getRDBRepository(EmailFilter::ENTITY_TYPE)
|
||||
->where([
|
||||
'action' => $actionList,
|
||||
'OR' => [
|
||||
[
|
||||
'parentType' => $account->getEntityType(),
|
||||
'parentId' => $account->getId(),
|
||||
'action' => $actionList,
|
||||
],
|
||||
[
|
||||
'parentId' => null,
|
||||
'action' => EmailFilter::ACTION_SKIP,
|
||||
],
|
||||
]
|
||||
]);
|
||||
|
||||
if (count($actionList) > 1) {
|
||||
$builder->order(
|
||||
Order::createByPositionInList(
|
||||
Expression::column('action'),
|
||||
$actionList
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/** @var Collection<EmailFilter> */
|
||||
return $builder->find();
|
||||
}
|
||||
|
||||
private function checkFetchOnlyHeader(Storage $storage, int $id): bool
|
||||
{
|
||||
$maxSize = $this->config->get('emailMessageMaxSize');
|
||||
|
||||
if (!$maxSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$size = $storage->getSize($id);
|
||||
} catch (Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($size > $maxSize * 1024 * 1024) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function importMessage(
|
||||
Account $account,
|
||||
MessageWrapper $message,
|
||||
ImporterData $data
|
||||
): ?Email {
|
||||
|
||||
try {
|
||||
return $this->importer->import($message, $data);
|
||||
} catch (Throwable $e) {
|
||||
$this->log->error(
|
||||
"{$account->getEntityType()} {$account->getId()}, import message; " .
|
||||
"{$e->getCode()} {$e->getMessage()}"
|
||||
);
|
||||
|
||||
if ($this->entityManager->getLocker()->isLocked()) {
|
||||
$this->entityManager->getLocker()->rollback();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $flags
|
||||
* @return string[]
|
||||
*/
|
||||
private static function flagsWithoutRecent(array $flags): array
|
||||
{
|
||||
return array_values(
|
||||
array_diff($flags, [Flag::RECENT])
|
||||
);
|
||||
}
|
||||
}
|
||||
384
application/Espo/Core/Mail/Account/GroupAccount/Account.php
Normal file
384
application/Espo/Core/Mail/Account/GroupAccount/Account.php
Normal file
@@ -0,0 +1,384 @@
|
||||
<?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\GroupAccount;
|
||||
|
||||
use Espo\Core\Field\Date;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Field\Link;
|
||||
use Espo\Core\Field\LinkMultiple;
|
||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||
use Espo\Core\Mail\Account\ImapParams;
|
||||
use Espo\Core\Mail\Smtp\HandlerProcessor;
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Crypt;
|
||||
use Espo\Entities\GroupEmailFolder;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\Mail\Account\Account as AccountInterface;
|
||||
use Espo\Core\Mail\Account\FetchData;
|
||||
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use RuntimeException;
|
||||
|
||||
class Account implements AccountInterface
|
||||
{
|
||||
private const PORTION_LIMIT = 20;
|
||||
|
||||
private ?LinkMultiple $users = null;
|
||||
private ?LinkMultiple $teams = null;
|
||||
private ?GroupEmailFolder $groupEmailFolder = null;
|
||||
|
||||
public function __construct(
|
||||
private InboundEmail $entity,
|
||||
private EntityManager $entityManager,
|
||||
private Config $config,
|
||||
private HandlerProcessor $handlerProcessor,
|
||||
private Crypt $crypt
|
||||
) {}
|
||||
|
||||
public function updateFetchData(FetchData $fetchData): void
|
||||
{
|
||||
$this->entity->set('fetchData', $fetchData->getRaw());
|
||||
|
||||
$this->entityManager->saveEntity($this->entity, [SaveOption::SILENT => true]);
|
||||
}
|
||||
|
||||
public function updateConnectedAt(): void
|
||||
{
|
||||
$this->entity->set('connectedAt', DateTime::createNow()->toString());
|
||||
|
||||
$this->entityManager->saveEntity($this->entity, [SaveOption::SILENT => true]);
|
||||
}
|
||||
|
||||
public function relateEmail(Email $email): void
|
||||
{
|
||||
$this->entityManager
|
||||
->getRDBRepository(InboundEmail::ENTITY_TYPE)
|
||||
->getRelation($this->entity, 'emails')
|
||||
->relate($email);
|
||||
}
|
||||
|
||||
public function getEntity(): InboundEmail
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
public function getPortionLimit(): int
|
||||
{
|
||||
return $this->config->get('inboundEmailMaxPortionSize', self::PORTION_LIMIT);
|
||||
}
|
||||
|
||||
public function isAvailableForFetching(): bool
|
||||
{
|
||||
return $this->entity->isAvailableForFetching();
|
||||
}
|
||||
|
||||
public function getEmailAddress(): ?string
|
||||
{
|
||||
return $this->entity->getEmailAddress();
|
||||
}
|
||||
|
||||
public function getUsers(): LinkMultiple
|
||||
{
|
||||
if (!$this->users) {
|
||||
$this->users = $this->loadUsers();
|
||||
}
|
||||
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
private function getGroupEmailFolderEntity(): ?GroupEmailFolder
|
||||
{
|
||||
if ($this->groupEmailFolder) {
|
||||
return $this->groupEmailFolder;
|
||||
}
|
||||
|
||||
if ($this->entity->getGroupEmailFolder()) {
|
||||
$this->groupEmailFolder = $this->entityManager
|
||||
->getRDBRepositoryByClass(GroupEmailFolder::class)
|
||||
->getById($this->entity->getGroupEmailFolder()->getId());
|
||||
|
||||
return $this->groupEmailFolder;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function loadUsers(): LinkMultiple
|
||||
{
|
||||
$teamIds = [];
|
||||
|
||||
if ($this->entity->getGroupEmailFolder()) {
|
||||
$groupEmailFolder = $this->getGroupEmailFolderEntity();
|
||||
|
||||
if ($groupEmailFolder) {
|
||||
$teamIds = $groupEmailFolder->getTeams()->getIdList();
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->entity->addAllTeamUsers()) {
|
||||
$teamIds = array_merge($teamIds, $this->entity->getTeams()->getIdList());
|
||||
}
|
||||
|
||||
$teamIds = array_unique($teamIds);
|
||||
$teamIds = array_values($teamIds);
|
||||
|
||||
if ($teamIds === []) {
|
||||
return LinkMultiple::create();
|
||||
}
|
||||
|
||||
$users = $this->entityManager
|
||||
->getRDBRepositoryByClass(User::class)
|
||||
->select([Attribute::ID])
|
||||
->distinct()
|
||||
->join(Field::TEAMS)
|
||||
->where([
|
||||
'type' => [User::TYPE_REGULAR, User::TYPE_ADMIN],
|
||||
'isActive' => true,
|
||||
'teamsMiddle.teamId' => $teamIds,
|
||||
])
|
||||
->find();
|
||||
|
||||
$linkMultiple = LinkMultiple::create();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$linkMultiple = $linkMultiple->withAddedId($user->getId());
|
||||
}
|
||||
|
||||
return $linkMultiple;
|
||||
}
|
||||
|
||||
public function getUser(): ?Link
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getAssignedUser(): ?Link
|
||||
{
|
||||
return $this->entity->getAssignToUser();
|
||||
}
|
||||
|
||||
public function getTeams(): LinkMultiple
|
||||
{
|
||||
if (!$this->teams) {
|
||||
$this->teams = $this->loadTeams();
|
||||
}
|
||||
|
||||
return $this->teams;
|
||||
}
|
||||
|
||||
private function loadTeams(): LinkMultiple
|
||||
{
|
||||
$teams = $this->entity->getTeams();
|
||||
|
||||
if ($this->entity->getTeam()) {
|
||||
$teams = $teams->withAddedId($this->entity->getTeam()->getId());
|
||||
}
|
||||
|
||||
if ($this->getGroupEmailFolder()) {
|
||||
$groupEmailFolder = $this->getGroupEmailFolderEntity();
|
||||
|
||||
if ($groupEmailFolder) {
|
||||
$teams = $teams->withAddedIdList($groupEmailFolder->getTeams()->getIdList());
|
||||
}
|
||||
}
|
||||
|
||||
return $teams;
|
||||
}
|
||||
|
||||
public function keepFetchedEmailsUnread(): bool
|
||||
{
|
||||
return $this->entity->keepFetchedEmailsUnread();
|
||||
}
|
||||
|
||||
public function getFetchData(): FetchData
|
||||
{
|
||||
return FetchData::fromRaw(
|
||||
$this->entity->getFetchData()
|
||||
);
|
||||
}
|
||||
|
||||
public function getFetchSince(): ?Date
|
||||
{
|
||||
return $this->entity->getFetchSince();
|
||||
}
|
||||
|
||||
public function getEmailFolder(): ?Link
|
||||
{
|
||||
return $this->entity->getEmailFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMonitoredFolderList(): array
|
||||
{
|
||||
return $this->entity->getMonitoredFolderList();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->entity->getId();
|
||||
}
|
||||
|
||||
public function getEntityType(): string
|
||||
{
|
||||
return $this->entity->getEntityType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<object>
|
||||
*/
|
||||
public function getImapHandlerClassName(): ?string
|
||||
{
|
||||
return $this->entity->getImapHandlerClassName();
|
||||
}
|
||||
|
||||
public function createCase(): bool
|
||||
{
|
||||
return $this->entity->createCase();
|
||||
}
|
||||
|
||||
public function autoReply(): bool
|
||||
{
|
||||
return $this->entity->autoReply();
|
||||
}
|
||||
|
||||
public function getSentFolder(): ?string
|
||||
{
|
||||
return $this->entity->getSentFolder();
|
||||
}
|
||||
|
||||
public function getGroupEmailFolder(): ?Link
|
||||
{
|
||||
return $this->entity->getGroupEmailFolder();
|
||||
}
|
||||
|
||||
public function isAvailableForSending(): bool
|
||||
{
|
||||
return $this->entity->isAvailableForSending();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
public function getSmtpParams(): ?SmtpParams
|
||||
{
|
||||
$host = $this->entity->getSmtpHost();
|
||||
|
||||
if (!$host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = $this->entity->getSmtpPort();
|
||||
|
||||
if ($port === null) {
|
||||
throw new NoSmtp("Empty port.");
|
||||
}
|
||||
|
||||
$smtpParams = SmtpParams::create($host, $port)
|
||||
->withSecurity($this->entity->getSmtpSecurity())
|
||||
->withAuth($this->entity->getSmtpAuth());
|
||||
|
||||
if ($this->entity->getSmtpAuth()) {
|
||||
$password = $this->entity->getSmtpPassword();
|
||||
|
||||
if ($password !== null) {
|
||||
$password = $this->crypt->decrypt($password);
|
||||
}
|
||||
|
||||
$smtpParams = $smtpParams
|
||||
->withUsername($this->entity->getSmtpUsername())
|
||||
->withPassword($password)
|
||||
->withAuthMechanism($this->entity->getSmtpAuthMechanism());
|
||||
}
|
||||
|
||||
if ($this->entity->getFromName()) {
|
||||
$smtpParams = $smtpParams->withFromName($this->entity->getFromName());
|
||||
}
|
||||
|
||||
$handlerClassName = $this->entity->getSmtpHandlerClassName();
|
||||
|
||||
if (!$handlerClassName) {
|
||||
return $smtpParams;
|
||||
}
|
||||
|
||||
return $this->handlerProcessor->handle($handlerClassName, $smtpParams, $this->getId());
|
||||
}
|
||||
|
||||
public function storeSentEmails(): bool
|
||||
{
|
||||
return $this->entity->storeSentEmails();
|
||||
}
|
||||
|
||||
public function getImapParams(): ?ImapParams
|
||||
{
|
||||
$host = $this->entity->getHost();
|
||||
$port = $this->entity->getPort();
|
||||
$username = $this->entity->getUsername();
|
||||
$password = $this->entity->getPassword();
|
||||
$security = $this->entity->getSecurity();
|
||||
|
||||
if (!$host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($port === null) {
|
||||
throw new RuntimeException("No port.");
|
||||
}
|
||||
|
||||
if ($username === null) {
|
||||
throw new RuntimeException("No username.");
|
||||
}
|
||||
|
||||
if ($password !== null) {
|
||||
$password = $this->crypt->decrypt($password);
|
||||
}
|
||||
|
||||
return new ImapParams(
|
||||
$host,
|
||||
$port,
|
||||
$username,
|
||||
$password,
|
||||
$security
|
||||
);
|
||||
}
|
||||
|
||||
public function getConnectedAt(): ?DateTime
|
||||
{
|
||||
/** @var DateTime */
|
||||
return $this->entity->getValueObject('connectedAt');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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\GroupAccount;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class AccountFactory
|
||||
{
|
||||
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function create(string $id): Account
|
||||
{
|
||||
$entity = $this->entityManager->getEntityById(InboundEmail::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new Error("InboundEmail '{$id}' not found.");
|
||||
}
|
||||
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(InboundEmail::class, $entity)
|
||||
->build();
|
||||
|
||||
return $this->injectableFactory->createWithBinding(Account::class, $binding);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?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\GroupAccount;
|
||||
|
||||
use Espo\Core\Mail\Message;
|
||||
use Espo\Core\Mail\Message\Part;
|
||||
|
||||
use const PREG_SPLIT_NO_EMPTY;
|
||||
|
||||
class BouncedRecognizer
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $hardBounceCodeList = [
|
||||
'5.0.0',
|
||||
'5.1.1', // bad destination mailbox address
|
||||
'5.1.2', // bad destination system address
|
||||
'5.1.6', // destination mailbox has moved, no forwarding address
|
||||
'5.4.1', // no answer from host
|
||||
];
|
||||
|
||||
public function isBounced(Message $message): bool
|
||||
{
|
||||
$from = $message->getHeader('From');
|
||||
$contentType = $message->getHeader('Content-Type');
|
||||
|
||||
if (preg_match('/MAILER-DAEMON|POSTMASTER/i', $from ?? '')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (str_starts_with($contentType ?? '', 'multipart/report')) {
|
||||
// @todo Check whether ever works.
|
||||
$deliveryStatusPart = $this->getDeliveryStatusPart($message);
|
||||
|
||||
if ($deliveryStatusPart) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$content = $message->getRawContent();
|
||||
|
||||
if (
|
||||
str_contains($content, 'message/delivery-status') &&
|
||||
str_contains($content, 'Status: ')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isHard(Message $message): bool
|
||||
{
|
||||
$content = $message->getRawContent();
|
||||
|
||||
/** @noinspection RegExpSimplifiable */
|
||||
/** @noinspection RegExpDuplicateCharacterInClass */
|
||||
if (preg_match('/permanent[ ]*[error|failure]/', $content)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$m = null;
|
||||
|
||||
$has5xxStatus = preg_match('/Status: (5\.[0-9]\.[0-9])/', $content, $m);
|
||||
|
||||
if ($has5xxStatus) {
|
||||
$status = $m[1] ?? null;
|
||||
|
||||
if (in_array($status, $this->hardBounceCodeList)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function extractStatus(Message $message): ?string
|
||||
{
|
||||
$content = $message->getRawContent();
|
||||
|
||||
$m = null;
|
||||
|
||||
$hasStatus = preg_match('/Status: ([0-9]\.[0-9]\.[0-9])/', $content, $m);
|
||||
|
||||
if ($hasStatus) {
|
||||
return $m[1] ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function extractQueueItemId(Message $message): ?string
|
||||
{
|
||||
$content = $message->getRawContent();
|
||||
|
||||
if (preg_match('/X-Queue-Item-Id: [a-z0-9\-]*/', $content, $m)) {
|
||||
/** @var array{string} $arr */
|
||||
$arr = preg_split('/X-Queue-Item-Id: /', $m[0], -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
return $arr[0];
|
||||
}
|
||||
|
||||
$to = $message->getHeader('to');
|
||||
|
||||
if (preg_match('/\+bounce-qid-[a-z0-9\-]*/', $to ?? '', $m)) {
|
||||
/** @var array{string} $arr */
|
||||
$arr = preg_split('/\+bounce-qid-/', $m[0], -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
return $arr[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getDeliveryStatusPart(Message $message): ?Part
|
||||
{
|
||||
foreach ($message->getPartList() as $part) {
|
||||
if ($part->getContentType() === 'message/delivery-status') {
|
||||
return $part;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?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\GroupAccount;
|
||||
|
||||
use Espo\Core\Binding\Factory;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
|
||||
use Espo\Core\Mail\Account\Hook\BeforeFetch;
|
||||
use Espo\Core\Mail\Account\Hook\AfterFetch;
|
||||
use Espo\Core\Mail\Account\GroupAccount\Hooks\BeforeFetch as GroupAccountBeforeFetch;
|
||||
use Espo\Core\Mail\Account\GroupAccount\Hooks\AfterFetch as GroupAccountAfterFetch;
|
||||
|
||||
use Espo\Core\Mail\Account\StorageFactory;
|
||||
use Espo\Core\Mail\Account\GroupAccount\StorageFactory as GroupAccountStorageFactory;
|
||||
use Espo\Core\Mail\Account\Fetcher;
|
||||
|
||||
/**
|
||||
* @implements Factory<Fetcher>
|
||||
*/
|
||||
class FetcherFactory implements Factory
|
||||
{
|
||||
private InjectableFactory $injectableFactory;
|
||||
|
||||
public function __construct(InjectableFactory $injectableFactory)
|
||||
{
|
||||
$this->injectableFactory = $injectableFactory;
|
||||
}
|
||||
|
||||
public function create(): Fetcher
|
||||
{
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindImplementation(BeforeFetch::class, GroupAccountBeforeFetch::class)
|
||||
->bindImplementation(AfterFetch::class, GroupAccountAfterFetch::class)
|
||||
->bindImplementation(StorageFactory::class, GroupAccountStorageFactory::class)
|
||||
->build();
|
||||
|
||||
return $this->injectableFactory->createWithBinding(Fetcher::class, $binding);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,612 @@
|
||||
<?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\GroupAccount\Hooks;
|
||||
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Name\Link;
|
||||
|
||||
use Espo\Core\Mail\Account\GroupAccount\AccountFactory as GroupAccountFactory;
|
||||
use Espo\Core\Mail\SenderParams;
|
||||
use Espo\Core\Templates\Entities\Person;
|
||||
use Espo\Core\Utils\SystemUser;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\Modules\Crm\Entities\Lead;
|
||||
use Espo\Tools\Email\Util;
|
||||
use Espo\Tools\EmailTemplate\Data as EmailTemplateData;
|
||||
use Espo\Tools\EmailTemplate\Params as EmailTemplateParams;
|
||||
use Espo\Tools\EmailTemplate\Service as EmailTemplateService;
|
||||
use Espo\Core\Mail\Account\Account;
|
||||
use Espo\Core\Mail\Account\Hook\BeforeFetchResult;
|
||||
use Espo\Core\Mail\Account\Hook\AfterFetch as AfterFetchInterface;
|
||||
use Espo\Core\Mail\Account\GroupAccount\Account as GroupAccount;
|
||||
use Espo\Core\Mail\EmailSender;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Tools\Stream\Service as StreamService;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Entities\Team;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Modules\Crm\Entities\CaseObj;
|
||||
use Espo\Repositories\EmailAddress as EmailAddressRepository;
|
||||
use Espo\Repositories\Attachment as AttachmentRepository;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Modules\Crm\Tools\Case\Distribution\RoundRobin;
|
||||
use Espo\Modules\Crm\Tools\Case\Distribution\LeastBusy;
|
||||
|
||||
use Throwable;
|
||||
use DateTime;
|
||||
|
||||
class AfterFetch implements AfterFetchInterface
|
||||
{
|
||||
private const DEFAULT_AUTOREPLY_LIMIT = 5;
|
||||
private const DEFAULT_AUTOREPLY_SUPPRESS_PERIOD = '2 hours';
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private StreamService $streamService,
|
||||
private Config $config,
|
||||
private EmailSender $emailSender,
|
||||
private Log $log,
|
||||
private RoundRobin $roundRobin,
|
||||
private LeastBusy $leastBusy,
|
||||
private GroupAccountFactory $groupAccountFactory,
|
||||
private EmailTemplateService $emailTemplateService,
|
||||
private SystemUser $systemUser
|
||||
) {}
|
||||
|
||||
public function process(Account $account, Email $email, BeforeFetchResult $beforeFetchResult): void
|
||||
{
|
||||
if (!$account instanceof GroupAccount) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$account->createCase() && !$email->isFetched()) {
|
||||
$this->noteAboutEmail($email);
|
||||
}
|
||||
|
||||
if ($account->createCase()) {
|
||||
if ($beforeFetchResult->get('isAutoSubmitted')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emailToProcess = $email;
|
||||
|
||||
if ($email->isFetched()) {
|
||||
$emailToProcess = $this->entityManager->getEntityById(Email::ENTITY_TYPE, $email->getId());
|
||||
} else {
|
||||
$emailToProcess->updateFetchedValues();
|
||||
}
|
||||
|
||||
if ($emailToProcess) {
|
||||
$this->createCase($account, $email);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($account->autoReply()) {
|
||||
if (
|
||||
$beforeFetchResult->get('isAutoSubmitted') ||
|
||||
$beforeFetchResult->get('skipAutoReply')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = null;
|
||||
|
||||
if ($account->getAssignedUser()) {
|
||||
$user = $this->entityManager->getEntityById(User::ENTITY_TYPE, $account->getAssignedUser()->getId());
|
||||
}
|
||||
|
||||
$this->autoReply($account, $email, null, $user);
|
||||
}
|
||||
}
|
||||
|
||||
private function noteAboutEmail(Email $email): void
|
||||
{
|
||||
if (!$email->getParent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->streamService->noteEmailReceived($email->getParent(), $email);
|
||||
}
|
||||
|
||||
private function autoReply(
|
||||
GroupAccount $account,
|
||||
Email $email,
|
||||
?CaseObj $case = null,
|
||||
?User $user = null
|
||||
): void {
|
||||
|
||||
$inboundEmail = $account->getEntity();
|
||||
|
||||
$fromAddress = $email->getFromAddress();
|
||||
|
||||
if (!$fromAddress) {
|
||||
return;
|
||||
}
|
||||
|
||||
$replyEmailTemplateId = $inboundEmail->get('replyEmailTemplateId');
|
||||
|
||||
if (!$replyEmailTemplateId) {
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = $this->config->get('emailAutoReplyLimit', self::DEFAULT_AUTOREPLY_LIMIT);
|
||||
|
||||
$d = new DateTime();
|
||||
|
||||
$period = $this->config->get('emailAutoReplySuppressPeriod', self::DEFAULT_AUTOREPLY_SUPPRESS_PERIOD);
|
||||
|
||||
$d->modify('-' . $period);
|
||||
|
||||
$threshold = $d->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
|
||||
|
||||
/** @var EmailAddressRepository $emailAddressRepository */
|
||||
$emailAddressRepository = $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE);
|
||||
|
||||
$emailAddress = $emailAddressRepository->getByAddress($fromAddress);
|
||||
|
||||
if ($emailAddress) {
|
||||
$sentCount = $this->entityManager
|
||||
->getRDBRepository(Email::ENTITY_TYPE)
|
||||
->where([
|
||||
'toEmailAddresses.id' => $emailAddress->getId(),
|
||||
'dateSent>' => $threshold,
|
||||
'status' => Email::STATUS_SENT,
|
||||
'createdById' => $this->systemUser->getId(),
|
||||
])
|
||||
->join('toEmailAddresses')
|
||||
->count();
|
||||
|
||||
if ($sentCount >= $limit) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$sender = $this->emailSender->create();
|
||||
|
||||
if ($email->getMessageId()) {
|
||||
$sender->withAddedHeader('In-Reply-To', $email->getMessageId());
|
||||
}
|
||||
|
||||
$sender
|
||||
->withAddedHeader('Auto-Submitted', 'auto-replied')
|
||||
->withAddedHeader('X-Auto-Response-Suppress', 'All')
|
||||
->withAddedHeader('Precedence', 'auto_reply');
|
||||
|
||||
try {
|
||||
$entityHash = [];
|
||||
|
||||
$contact = null;
|
||||
|
||||
if ($case) {
|
||||
$entityHash[CaseObj::ENTITY_TYPE] = $case;
|
||||
|
||||
$contact = $case->getContact();
|
||||
}
|
||||
|
||||
if (!$contact) {
|
||||
$contact = $this->entityManager->getNewEntity(Contact::ENTITY_TYPE);
|
||||
|
||||
$fromName = Util::parseFromName($email->getFromString() ?? '');
|
||||
|
||||
if (!empty($fromName)) {
|
||||
$contact->set(Field::NAME, $fromName);
|
||||
}
|
||||
}
|
||||
|
||||
$entityHash[Person::TEMPLATE_TYPE] = $contact;
|
||||
$entityHash[Contact::ENTITY_TYPE] = $contact;
|
||||
$entityHash[Email::ENTITY_TYPE] = $email;
|
||||
|
||||
if ($user) {
|
||||
$entityHash[User::ENTITY_TYPE] = $user;
|
||||
}
|
||||
|
||||
$replyData = $this->emailTemplateService->process(
|
||||
$replyEmailTemplateId,
|
||||
EmailTemplateData::create()
|
||||
->withEntityHash($entityHash),
|
||||
EmailTemplateParams::create()
|
||||
->withApplyAcl(false)
|
||||
->withCopyAttachments()
|
||||
);
|
||||
|
||||
$subject = $replyData->getSubject();
|
||||
|
||||
if ($case && $case->getNumber() !== null) {
|
||||
$subject = "[#{$case->getNumber()}] $subject";
|
||||
}
|
||||
|
||||
$reply = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();
|
||||
|
||||
$reply
|
||||
->addToAddress($fromAddress)
|
||||
->setSubject($subject)
|
||||
->setBody($replyData->getBody())
|
||||
->setIsHtml($replyData->isHtml())
|
||||
->setIsAutoReply()
|
||||
->setTeams($email->getTeams());
|
||||
|
||||
if ($email->getParentId() && $email->getParentType()) {
|
||||
$reply->setParent($email->getParent());
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($reply);
|
||||
|
||||
$senderParams = SenderParams::create();
|
||||
|
||||
if ($inboundEmail->isAvailableForSending()) {
|
||||
$groupAccount = $this->groupAccountFactory->create($inboundEmail->getId());
|
||||
|
||||
$smtpParams = $groupAccount->getSmtpParams();
|
||||
|
||||
if ($smtpParams) {
|
||||
$sender->withSmtpParams($smtpParams);
|
||||
}
|
||||
|
||||
if ($groupAccount->getEmailAddress()) {
|
||||
$senderParams = $senderParams->withFromAddress($groupAccount->getEmailAddress());
|
||||
}
|
||||
}
|
||||
|
||||
if ($inboundEmail->getFromName()) {
|
||||
$senderParams = $senderParams->withFromName($inboundEmail->getFromName());
|
||||
}
|
||||
|
||||
if ($inboundEmail->getReplyFromAddress()) {
|
||||
$senderParams = $senderParams->withFromAddress($inboundEmail->getReplyFromAddress());
|
||||
}
|
||||
|
||||
if ($inboundEmail->getReplyFromName()) {
|
||||
$senderParams = $senderParams->withFromName($inboundEmail->getReplyFromName());
|
||||
}
|
||||
|
||||
if ($inboundEmail->getReplyToAddress()) {
|
||||
$senderParams = $senderParams->withReplyToAddress($inboundEmail->getReplyToAddress());
|
||||
}
|
||||
|
||||
$sender
|
||||
->withParams($senderParams)
|
||||
->withAttachments($replyData->getAttachmentList())
|
||||
->send($reply);
|
||||
|
||||
$this->entityManager->saveEntity($reply);
|
||||
} catch (Throwable $e) {
|
||||
$this->log->error("Inbound Email: Auto-reply error: " . $e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
private function createCase(GroupAccount $account, Email $email): void
|
||||
{
|
||||
$inboundEmail = $account->getEntity();
|
||||
|
||||
$parentId = $email->getParentId();
|
||||
|
||||
if (
|
||||
$email->getParentType() === CaseObj::ENTITY_TYPE &&
|
||||
$parentId
|
||||
) {
|
||||
$case = $this->entityManager
|
||||
->getRDBRepositoryByClass(CaseObj::class)
|
||||
->getById($parentId);
|
||||
|
||||
if (!$case) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processCaseToEmailFields($case, $email);
|
||||
|
||||
if (!$email->isFetched()) {
|
||||
$this->streamService->noteEmailReceived($case, $email);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
/** @noinspection RegExpRedundantEscape */
|
||||
if (preg_match('/\[#([0-9]+)[^0-9]*\]/', $email->get(Field::NAME), $m)) {
|
||||
$caseNumber = $m[1];
|
||||
|
||||
$case = $this->entityManager
|
||||
->getRDBRepository(CaseObj::ENTITY_TYPE)
|
||||
->where([
|
||||
'number' => $caseNumber,
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if (!$case) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email->set('parentType', CaseObj::ENTITY_TYPE);
|
||||
$email->set('parentId', $case->getId());
|
||||
|
||||
$this->processCaseToEmailFields($case, $email);
|
||||
|
||||
if (!$email->isFetched()) {
|
||||
$this->streamService->noteEmailReceived($case, $email);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$params = [
|
||||
'caseDistribution' => $inboundEmail->getCaseDistribution(),
|
||||
'teamId' => $inboundEmail->get('teamId'),
|
||||
'userId' => $inboundEmail->get('assignToUserId'),
|
||||
'targetUserPosition' => $inboundEmail->getTargetUserPosition(),
|
||||
'inboundEmailId' => $inboundEmail->getId(),
|
||||
];
|
||||
|
||||
$case = $this->emailToCase($email, $params);
|
||||
|
||||
$assignedUserLink = $case->getAssignedUser();
|
||||
|
||||
$user = $assignedUserLink ?
|
||||
$this->entityManager->getEntityById(User::ENTITY_TYPE, $assignedUserLink->getId())
|
||||
: null;
|
||||
|
||||
$this->streamService->noteEmailReceived($case, $email, true);
|
||||
|
||||
if ($account->autoReply()) {
|
||||
$this->autoReply($account, $email, $case, $user);
|
||||
}
|
||||
}
|
||||
|
||||
private function processCaseToEmailFields(CaseObj $case, Email $email): void
|
||||
{
|
||||
$userIdList = [];
|
||||
|
||||
if ($case->hasLinkMultipleField(Field::ASSIGNED_USERS)) {
|
||||
$userIdList = $case->getLinkMultipleIdList(Field::ASSIGNED_USERS);
|
||||
} else {
|
||||
$assignedUserLink = $case->getAssignedUser();
|
||||
|
||||
if ($assignedUserLink) {
|
||||
$userIdList[] = $assignedUserLink->getId();
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($userIdList as $userId) {
|
||||
$email->addLinkMultipleId('users', $userId);
|
||||
}
|
||||
|
||||
$teamIdList = $case->getLinkMultipleIdList(Field::TEAMS);
|
||||
|
||||
foreach ($teamIdList as $teamId) {
|
||||
$email->addLinkMultipleId(Field::TEAMS, $teamId);
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($email, [
|
||||
'skipLinkMultipleRemove' => true,
|
||||
'skipLinkMultipleUpdate' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
private function emailToCase(Email $email, array $params): CaseObj
|
||||
{
|
||||
/** @var CaseObj $case */
|
||||
$case = $this->entityManager->getNewEntity(CaseObj::ENTITY_TYPE);
|
||||
|
||||
$case->populateDefaults();
|
||||
|
||||
$case->set(Field::NAME, $email->get(Field::NAME));
|
||||
|
||||
$bodyPlain = $email->getBodyPlain() ?? '';
|
||||
|
||||
/** @var string $replacedBodyPlain */
|
||||
$replacedBodyPlain = preg_replace('/\s+/', '', $bodyPlain);
|
||||
|
||||
if (trim($replacedBodyPlain) === '') {
|
||||
$bodyPlain = '';
|
||||
}
|
||||
|
||||
if ($bodyPlain) {
|
||||
$case->set('description', $bodyPlain);
|
||||
}
|
||||
|
||||
$attachmentIdList = $email->getLinkMultipleIdList('attachments');
|
||||
|
||||
$copiedAttachmentIdList = [];
|
||||
|
||||
/** @var AttachmentRepository $attachmentRepository*/
|
||||
$attachmentRepository = $this->entityManager->getRepository(Attachment::ENTITY_TYPE);
|
||||
|
||||
foreach ($attachmentIdList as $attachmentId) {
|
||||
$attachment = $attachmentRepository->getById($attachmentId);
|
||||
|
||||
if (!$attachment) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$copiedAttachment = $attachmentRepository->getCopiedAttachment($attachment);
|
||||
|
||||
$copiedAttachmentIdList[] = $copiedAttachment->getId();
|
||||
}
|
||||
|
||||
if (count($copiedAttachmentIdList)) {
|
||||
$case->setLinkMultipleIdList('attachments', $copiedAttachmentIdList);
|
||||
}
|
||||
|
||||
$userId = null;
|
||||
|
||||
if (!empty($params['userId'])) {
|
||||
$userId = $params['userId'];
|
||||
}
|
||||
|
||||
if (!empty($params['inboundEmailId'])) {
|
||||
$case->set('inboundEmailId', $params['inboundEmailId']);
|
||||
}
|
||||
|
||||
$teamId = false;
|
||||
|
||||
if (!empty($params['teamId'])) {
|
||||
$teamId = $params['teamId'];
|
||||
}
|
||||
|
||||
if ($teamId) {
|
||||
$case->set('teamsIds', [$teamId]);
|
||||
}
|
||||
|
||||
$caseDistribution = '';
|
||||
|
||||
if (!empty($params['caseDistribution'])) {
|
||||
$caseDistribution = $params['caseDistribution'];
|
||||
}
|
||||
|
||||
$targetUserPosition = null;
|
||||
|
||||
if (!empty($params['targetUserPosition'])) {
|
||||
$targetUserPosition = $params['targetUserPosition'];
|
||||
}
|
||||
|
||||
switch ($caseDistribution) {
|
||||
case InboundEmail::CASE_DISTRIBUTION_DIRECT_ASSIGNMENT:
|
||||
if ($userId) {
|
||||
$case->set('assignedUserId', $userId);
|
||||
$case->set('status', CaseObj::STATUS_ASSIGNED);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case InboundEmail::CASE_DISTRIBUTION_ROUND_ROBIN:
|
||||
if ($teamId) {
|
||||
/** @var ?Team $team */
|
||||
$team = $this->entityManager->getEntityById(Team::ENTITY_TYPE, $teamId);
|
||||
|
||||
if ($team) {
|
||||
$this->assignRoundRobin($case, $team, $targetUserPosition);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case InboundEmail::CASE_DISTRIBUTION_LEAST_BUSY:
|
||||
if ($teamId) {
|
||||
/** @var ?Team $team */
|
||||
$team = $this->entityManager->getEntityById(Team::ENTITY_TYPE, $teamId);
|
||||
|
||||
if ($team) {
|
||||
$this->assignLeastBusy($case, $team, $targetUserPosition);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$assignedUserLink = $case->getAssignedUser();
|
||||
|
||||
if ($assignedUserLink) {
|
||||
$email->set('assignedUserId', $assignedUserLink->getId());
|
||||
}
|
||||
|
||||
if ($email->get('accountId')) {
|
||||
$case->set('accountId', $email->get('accountId'));
|
||||
}
|
||||
|
||||
$contact = $this->entityManager
|
||||
->getRDBRepository(Contact::ENTITY_TYPE)
|
||||
->join(Link::EMAIL_ADDRESSES, 'emailAddressesMultiple')
|
||||
->where([
|
||||
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId'),
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if ($contact) {
|
||||
$case->set('contactId', $contact->getId());
|
||||
} else {
|
||||
if (!$case->get('accountId')) {
|
||||
$lead = $this->entityManager
|
||||
->getRDBRepository(Lead::ENTITY_TYPE)
|
||||
->join(Link::EMAIL_ADDRESSES, 'emailAddressesMultiple')
|
||||
->where([
|
||||
'emailAddressesMultiple.id' => $email->get('fromEmailAddressId')
|
||||
])
|
||||
->findOne();
|
||||
|
||||
if ($lead) {
|
||||
$case->set('leadId', $lead->getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->entityManager->saveEntity($case);
|
||||
|
||||
$email->set('parentType', CaseObj::ENTITY_TYPE);
|
||||
$email->set('parentId', $case->getId());
|
||||
|
||||
$this->entityManager->saveEntity($email, [
|
||||
'skipLinkMultipleRemove' => true,
|
||||
'skipLinkMultipleUpdate' => true,
|
||||
]);
|
||||
|
||||
// Unknown reason of doing this.
|
||||
$fetchedCase = $this->entityManager
|
||||
->getRDBRepositoryByClass(CaseObj::class)
|
||||
->getById($case->getId());
|
||||
|
||||
if ($fetchedCase) {
|
||||
return $fetchedCase;
|
||||
}
|
||||
|
||||
$this->entityManager->refreshEntity($case);
|
||||
|
||||
return $case;
|
||||
}
|
||||
|
||||
private function assignRoundRobin(CaseObj $case, Team $team, ?string $targetUserPosition): void
|
||||
{
|
||||
$user = $this->roundRobin->getUser($team, $targetUserPosition);
|
||||
|
||||
if ($user) {
|
||||
$case->set('assignedUserId', $user->getId());
|
||||
$case->set('status', CaseObj::STATUS_ASSIGNED);
|
||||
}
|
||||
}
|
||||
|
||||
private function assignLeastBusy(CaseObj $case, Team $team, ?string $targetUserPosition): void
|
||||
{
|
||||
$user = $this->leastBusy->getUser($team, $targetUserPosition);
|
||||
|
||||
if ($user) {
|
||||
$case->set('assignedUserId', $user->getId());
|
||||
$case->set('status', CaseObj::STATUS_ASSIGNED);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
<?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\GroupAccount\Hooks;
|
||||
|
||||
use Espo\Core\Mail\Account\Hook\BeforeFetch as BeforeFetchInterface;
|
||||
use Espo\Core\Mail\Account\Hook\BeforeFetchResult;
|
||||
use Espo\Core\Mail\Account\Account;
|
||||
use Espo\Core\Mail\Importer\AutoReplyDetector;
|
||||
use Espo\Core\Mail\Message;
|
||||
use Espo\Core\Mail\Account\GroupAccount\BouncedRecognizer;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\EmailAddress as EmailAddressRepository;
|
||||
use Espo\Modules\Crm\Entities\EmailQueueItem;
|
||||
use Espo\Modules\Crm\Tools\Campaign\LogService as CampaignService;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class BeforeFetch implements BeforeFetchInterface
|
||||
{
|
||||
public function __construct(
|
||||
private Log $log,
|
||||
private EntityManager $entityManager,
|
||||
private BouncedRecognizer $bouncedRecognizer,
|
||||
private CampaignService $campaignService,
|
||||
private AutoReplyDetector $autoReplyDetector,
|
||||
) {}
|
||||
|
||||
public function process(Account $account, Message $message): BeforeFetchResult
|
||||
{
|
||||
if ($this->bouncedRecognizer->isBounced($message)) {
|
||||
try {
|
||||
$toSkip = $this->processBounced($message);
|
||||
} catch (Throwable $e) {
|
||||
$logMessage = 'InboundEmail ' . $account->getId() . ' ' .
|
||||
'Process Bounced Message; ' . $e->getCode() . ' ' . $e->getMessage();
|
||||
|
||||
$this->log->error($logMessage, ['exception' => $e]);
|
||||
|
||||
return BeforeFetchResult::create()->withToSkip();
|
||||
}
|
||||
|
||||
if ($toSkip) {
|
||||
return BeforeFetchResult::create()->withToSkip();
|
||||
}
|
||||
}
|
||||
|
||||
return BeforeFetchResult::create()
|
||||
->with('skipAutoReply', $this->checkMessageCannotBeAutoReplied($message))
|
||||
->with('isAutoSubmitted', $this->checkMessageIsAutoSubmitted($message));
|
||||
}
|
||||
|
||||
private function processBounced(Message $message): bool
|
||||
{
|
||||
$isHard = $this->bouncedRecognizer->isHard($message);
|
||||
$queueItemId = $this->bouncedRecognizer->extractQueueItemId($message);
|
||||
|
||||
if (!$queueItemId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$queueItem = $this->entityManager->getRDBRepositoryByClass(EmailQueueItem::class)->getById($queueItemId);
|
||||
|
||||
if (!$queueItem) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$campaignId = $queueItem->getMassEmail()?->getCampaignId();
|
||||
$emailAddress = $queueItem->getEmailAddress();
|
||||
|
||||
if (!$emailAddress) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var EmailAddressRepository $emailAddressRepository */
|
||||
$emailAddressRepository = $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE);
|
||||
|
||||
if ($isHard) {
|
||||
$emailAddressEntity = $emailAddressRepository->getByAddress($emailAddress);
|
||||
|
||||
if ($emailAddressEntity) {
|
||||
$emailAddressEntity->setInvalid(true);
|
||||
|
||||
$this->entityManager->saveEntity($emailAddressEntity);
|
||||
}
|
||||
}
|
||||
|
||||
$targetType = $queueItem->getTargetType();
|
||||
$targetId = $queueItem->getTargetId();
|
||||
|
||||
$target = $this->entityManager->getEntityById($targetType, $targetId);
|
||||
|
||||
if ($campaignId && $target) {
|
||||
$this->campaignService->logBounced($campaignId, $queueItem, $isHard);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function checkMessageIsAutoReply(Message $message): bool
|
||||
{
|
||||
if ($this->checkMessageIsAutoSubmitted($message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->autoReplyDetector->detect($message);
|
||||
}
|
||||
|
||||
private function checkMessageCannotBeAutoReplied(Message $message): bool
|
||||
{
|
||||
if (
|
||||
$message->getHeader('X-Auto-Response-Suppress') === 'AutoReply' ||
|
||||
$message->getHeader('X-Auto-Response-Suppress') === 'All'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->checkMessageIsAutoSubmitted($message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->checkMessageIsAutoReply($message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function checkMessageIsAutoSubmitted(Message $message): bool
|
||||
{
|
||||
if ($this->autoReplyDetector->detect($message)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $message->getHeader('Auto-Submitted') &&
|
||||
strtolower($message->getHeader('Auto-Submitted')) !== 'no';
|
||||
}
|
||||
}
|
||||
160
application/Espo/Core/Mail/Account/GroupAccount/Service.php
Normal file
160
application/Espo/Core/Mail/Account/GroupAccount/Service.php
Normal file
@@ -0,0 +1,160 @@
|
||||
<?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\GroupAccount;
|
||||
|
||||
use Espo\Core\Exceptions\ErrorSilent;
|
||||
use Espo\Core\Mail\Account\Account as Account;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Mail\Account\Fetcher;
|
||||
use Espo\Core\Mail\Account\Storage\Params;
|
||||
use Espo\Core\Mail\Account\StorageFactory;
|
||||
use Espo\Core\Mail\Account\Util\NotificationHelper;
|
||||
use Espo\Core\Mail\Exceptions\ImapError;
|
||||
use Espo\Core\Mail\Exceptions\NoImap;
|
||||
use Espo\Core\Mail\Sender\Message;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Exception;
|
||||
|
||||
use Laminas\Mail\Exception\ExceptionInterface;
|
||||
|
||||
class Service
|
||||
{
|
||||
public function __construct(
|
||||
private Fetcher $fetcher,
|
||||
private AccountFactory $accountFactory,
|
||||
private StorageFactory $storageFactory,
|
||||
private Log $log,
|
||||
private NotificationHelper $notificationHelper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string $id Account ID.
|
||||
* @throws Error
|
||||
* @throws NoImap
|
||||
* @throws ImapError
|
||||
*/
|
||||
public function fetch(string $id): void
|
||||
{
|
||||
$account = $this->accountFactory->create($id);
|
||||
|
||||
try {
|
||||
$this->fetcher->fetch($account);
|
||||
} catch (ImapError $e) {
|
||||
$this->notificationHelper->processImapError($account);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$account->updateConnectedAt();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @throws Error
|
||||
* @throws ImapError
|
||||
*/
|
||||
public function getFolderList(Params $params): array
|
||||
{
|
||||
if ($params->getId()) {
|
||||
$account = $this->accountFactory->create($params->getId());
|
||||
|
||||
$params = $params
|
||||
->withPassword($this->getPassword($params, $account))
|
||||
->withImapHandlerClassName($account->getImapHandlerClassName());
|
||||
}
|
||||
|
||||
$storage = $this->storageFactory->createWithParams($params);
|
||||
|
||||
return $storage->getFolderNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function testConnection(Params $params): void
|
||||
{
|
||||
if ($params->getId()) {
|
||||
$account = $this->accountFactory->create($params->getId());
|
||||
|
||||
$params = $params
|
||||
->withPassword($this->getPassword($params, $account))
|
||||
->withImapHandlerClassName($account->getImapHandlerClassName());
|
||||
}
|
||||
|
||||
try {
|
||||
$storage = $this->storageFactory->createWithParams($params);
|
||||
$storage->getFolderNames();
|
||||
} catch (Exception $e) {
|
||||
$this->log->warning("IMAP test connection failed; {message}", [
|
||||
'exception' => $e,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$message = $e instanceof ExceptionInterface || $e instanceof ImapError ?
|
||||
$e->getMessage() : '';
|
||||
|
||||
throw new ErrorSilent($message);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPassword(Params $params, Account $account): ?string
|
||||
{
|
||||
$password = $params->getPassword();
|
||||
|
||||
if ($password !== null) {
|
||||
return $password;
|
||||
}
|
||||
|
||||
$imapParams = $account->getImapParams();
|
||||
|
||||
return $imapParams?->getPassword();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id Account ID.
|
||||
* @throws Error
|
||||
* @throws ImapError
|
||||
* @throws NoImap
|
||||
*/
|
||||
public function storeSentMessage(string $id, Message $message): void
|
||||
{
|
||||
$account = $this->accountFactory->create($id);
|
||||
|
||||
$folder = $account->getSentFolder();
|
||||
|
||||
if (!$folder) {
|
||||
throw new Error("No sent folder for Group Email Account $id.");
|
||||
}
|
||||
|
||||
$storage = $this->storageFactory->create($account);
|
||||
|
||||
$storage->appendMessage($message->toString(), $folder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?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\GroupAccount;
|
||||
|
||||
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
|
||||
) {}
|
||||
|
||||
public function create(Account $account): LaminasStorage
|
||||
{
|
||||
$imapParams = $account->getImapParams();
|
||||
|
||||
if (!$imapParams) {
|
||||
throw new NoImap("No IMAP params.");
|
||||
}
|
||||
|
||||
$params = Params::createBuilder()
|
||||
->setHost($imapParams->getHost())
|
||||
->setPort($imapParams->getPort())
|
||||
->setSecurity($imapParams->getSecurity())
|
||||
->setUsername($imapParams->getUsername())
|
||||
->setPassword($imapParams->getPassword())
|
||||
->setId($account->getId())
|
||||
->setImapHandlerClassName($account->getImapHandlerClassName())
|
||||
->build();
|
||||
|
||||
return $this->createWithParams($params);
|
||||
}
|
||||
|
||||
public function createWithParams(Params $params): LaminasStorage
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
38
application/Espo/Core/Mail/Account/Hook/AfterFetch.php
Normal file
38
application/Espo/Core/Mail/Account/Hook/AfterFetch.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Hook;
|
||||
|
||||
use Espo\Core\Mail\Account\Account;
|
||||
use Espo\Entities\Email;
|
||||
|
||||
interface AfterFetch
|
||||
{
|
||||
public function process(Account $account, Email $email, BeforeFetchResult $beforeFetchResult): void;
|
||||
}
|
||||
38
application/Espo/Core/Mail/Account/Hook/BeforeFetch.php
Normal file
38
application/Espo/Core/Mail/Account/Hook/BeforeFetch.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?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\Hook;
|
||||
|
||||
use Espo\Core\Mail\Account\Account;
|
||||
use Espo\Core\Mail\Message;
|
||||
|
||||
interface BeforeFetch
|
||||
{
|
||||
public function process(Account $account, Message $message): BeforeFetchResult;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?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\Hook;
|
||||
|
||||
class BeforeFetchResult
|
||||
{
|
||||
private bool $toSkip = false;
|
||||
/** @var array<string, mixed> */
|
||||
private array $data = [];
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function withToSkip(bool $toSkip = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->toSkip = $toSkip;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function with(string $name, mixed $value): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->data[$name] = $value;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function toSkip(): bool
|
||||
{
|
||||
return $this->toSkip;
|
||||
}
|
||||
|
||||
public function get(string $name): mixed
|
||||
{
|
||||
return $this->data[$name] ?? null;
|
||||
}
|
||||
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->data);
|
||||
}
|
||||
}
|
||||
69
application/Espo/Core/Mail/Account/ImapParams.php
Normal file
69
application/Espo/Core/Mail/Account/ImapParams.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class ImapParams
|
||||
{
|
||||
public function __construct(
|
||||
private string $host,
|
||||
private int $port,
|
||||
private string $username,
|
||||
private ?string $password,
|
||||
private ?string $security
|
||||
) {}
|
||||
|
||||
public function getHost(): string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getPort(): int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getSecurity(): ?string
|
||||
{
|
||||
return $this->security;
|
||||
}
|
||||
}
|
||||
331
application/Espo/Core/Mail/Account/PersonalAccount/Account.php
Normal file
331
application/Espo/Core/Mail/Account/PersonalAccount/Account.php
Normal file
@@ -0,0 +1,331 @@
|
||||
<?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\PersonalAccount;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\Field\Date;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Field\Link;
|
||||
use Espo\Core\Field\LinkMultiple;
|
||||
use Espo\Core\Field\LinkMultipleItem;
|
||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||
use Espo\Core\Mail\Account\ImapParams;
|
||||
use Espo\Core\Mail\Smtp\HandlerProcessor;
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Crypt;
|
||||
use Espo\Entities\EmailAccount;
|
||||
use Espo\Entities\User;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Core\Mail\Account\Account as AccountInterface;
|
||||
use Espo\Core\Mail\Account\FetchData;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class Account implements AccountInterface
|
||||
{
|
||||
private const PORTION_LIMIT = 10;
|
||||
|
||||
private User $user;
|
||||
private Crypt $crypt;
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function __construct(
|
||||
private EmailAccount $entity,
|
||||
private EntityManager $entityManager,
|
||||
private Config $config,
|
||||
private HandlerProcessor $handlerProcessor,
|
||||
Crypt $crypt
|
||||
) {
|
||||
if (!$this->entity->getAssignedUser()) {
|
||||
throw new Error("No assigned user.");
|
||||
}
|
||||
|
||||
$userId = $this->entity->getAssignedUser()->getId();
|
||||
|
||||
$user = $this->entityManager->getRDBRepositoryByClass(User::class)->getById($userId);
|
||||
|
||||
if (!$user) {
|
||||
throw new Error("Assigned user not found.");
|
||||
}
|
||||
|
||||
$this->user = $user;
|
||||
$this->crypt = $crypt;
|
||||
}
|
||||
|
||||
public function updateFetchData(FetchData $fetchData): void
|
||||
{
|
||||
$this->entity->set('fetchData', $fetchData->getRaw());
|
||||
|
||||
$this->entityManager->saveEntity($this->entity, [SaveOption::SILENT => true]);
|
||||
}
|
||||
|
||||
public function updateConnectedAt(): void
|
||||
{
|
||||
$this->entity->set('connectedAt', DateTime::createNow()->toString());
|
||||
|
||||
$this->entityManager->saveEntity($this->entity, [SaveOption::SILENT => true]);
|
||||
}
|
||||
|
||||
public function relateEmail(Email $email): void
|
||||
{
|
||||
$this->entityManager
|
||||
->getRDBRepository(EmailAccount::ENTITY_TYPE)
|
||||
->getRelation($this->entity, 'emails')
|
||||
->relate($email);
|
||||
}
|
||||
|
||||
public function getEntity(): EmailAccount
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
public function getPortionLimit(): int
|
||||
{
|
||||
return $this->config->get('personalEmailMaxPortionSize', self::PORTION_LIMIT);
|
||||
}
|
||||
|
||||
public function isAvailableForFetching(): bool
|
||||
{
|
||||
return $this->entity->isAvailableForFetching();
|
||||
}
|
||||
|
||||
public function getEmailAddress(): ?string
|
||||
{
|
||||
return $this->entity->getEmailAddress();
|
||||
}
|
||||
|
||||
public function getUsers(): LinkMultiple
|
||||
{
|
||||
$linkMultiple = LinkMultiple::create();
|
||||
|
||||
$userLink = $this->getUser();
|
||||
|
||||
return $linkMultiple->withAdded(
|
||||
LinkMultipleItem
|
||||
::create($userLink->getId())
|
||||
->withName($userLink->getName() ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A user to assign emails to. Not need for personal accounts.
|
||||
*/
|
||||
public function getAssignedUser(): ?Link
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function getUser(): Link
|
||||
{
|
||||
$userLink = $this->entity->getAssignedUser();
|
||||
|
||||
if (!$userLink) {
|
||||
throw new Error("No assigned user.");
|
||||
}
|
||||
|
||||
return $userLink;
|
||||
}
|
||||
|
||||
public function getTeams(): LinkMultiple
|
||||
{
|
||||
$linkMultiple = LinkMultiple::create();
|
||||
|
||||
$team = $this->user->getDefaultTeam();
|
||||
|
||||
if (!$team) {
|
||||
return $linkMultiple;
|
||||
}
|
||||
|
||||
return $linkMultiple->withAdded(
|
||||
LinkMultipleItem
|
||||
::create($team->getId())
|
||||
->withName($team->getName() ?? '')
|
||||
);
|
||||
}
|
||||
|
||||
public function keepFetchedEmailsUnread(): bool
|
||||
{
|
||||
return $this->entity->keepFetchedEmailsUnread();
|
||||
}
|
||||
|
||||
public function getFetchData(): FetchData
|
||||
{
|
||||
return FetchData::fromRaw(
|
||||
$this->entity->getFetchData()
|
||||
);
|
||||
}
|
||||
|
||||
public function getFetchSince(): ?Date
|
||||
{
|
||||
return $this->entity->getFetchSince();
|
||||
}
|
||||
|
||||
public function getEmailFolder(): ?Link
|
||||
{
|
||||
return $this->entity->getEmailFolder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMonitoredFolderList(): array
|
||||
{
|
||||
return $this->entity->getMonitoredFolderList();
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->entity->getId();
|
||||
}
|
||||
|
||||
public function getEntityType(): string
|
||||
{
|
||||
return $this->entity->getEntityType();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<object>
|
||||
*/
|
||||
public function getImapHandlerClassName(): ?string
|
||||
{
|
||||
return $this->entity->getImapHandlerClassName();
|
||||
}
|
||||
|
||||
public function getSentFolder(): ?string
|
||||
{
|
||||
return $this->entity->getSentFolder();
|
||||
}
|
||||
|
||||
public function getGroupEmailFolder(): ?Link
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isAvailableForSending(): bool
|
||||
{
|
||||
return $this->entity->isAvailableForSending();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
public function getSmtpParams(): ?SmtpParams
|
||||
{
|
||||
$host = $this->entity->getSmtpHost();
|
||||
|
||||
if (!$host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$port = $this->entity->getSmtpPort();
|
||||
|
||||
if ($port === null) {
|
||||
throw new NoSmtp("Empty port.");
|
||||
}
|
||||
|
||||
$smtpParams = SmtpParams::create($host, $port)
|
||||
->withSecurity($this->entity->getSmtpSecurity())
|
||||
->withAuth($this->entity->getSmtpAuth());
|
||||
|
||||
if ($this->entity->getSmtpAuth()) {
|
||||
$password = $this->entity->getSmtpPassword();
|
||||
|
||||
if ($password !== null) {
|
||||
$password = $this->crypt->decrypt($password);
|
||||
}
|
||||
|
||||
$smtpParams = $smtpParams
|
||||
->withUsername($this->entity->getSmtpUsername())
|
||||
->withPassword($password)
|
||||
->withAuthMechanism($this->entity->getSmtpAuthMechanism());
|
||||
}
|
||||
|
||||
$handlerClassName = $this->entity->getSmtpHandlerClassName();
|
||||
|
||||
if (!$handlerClassName) {
|
||||
return $smtpParams;
|
||||
}
|
||||
|
||||
return $this->handlerProcessor->handle($handlerClassName, $smtpParams, $this->getId());
|
||||
}
|
||||
|
||||
public function storeSentEmails(): bool
|
||||
{
|
||||
return $this->entity->storeSentEmails();
|
||||
}
|
||||
|
||||
public function getImapParams(): ?ImapParams
|
||||
{
|
||||
$host = $this->entity->getHost();
|
||||
$port = $this->entity->getPort();
|
||||
$username = $this->entity->getUsername();
|
||||
$password = $this->entity->getPassword();
|
||||
$security = $this->entity->getSecurity();
|
||||
|
||||
if (!$host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($port === null) {
|
||||
throw new RuntimeException("No port.");
|
||||
}
|
||||
|
||||
if ($username === null) {
|
||||
throw new RuntimeException("No username.");
|
||||
}
|
||||
|
||||
if ($password !== null) {
|
||||
$password = $this->crypt->decrypt($password);
|
||||
}
|
||||
|
||||
return new ImapParams(
|
||||
$host,
|
||||
$port,
|
||||
$username,
|
||||
$password,
|
||||
$security
|
||||
);
|
||||
}
|
||||
|
||||
public function getConnectedAt(): ?DateTime
|
||||
{
|
||||
/** @var DateTime */
|
||||
return $this->entity->getValueObject('connectedAt');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?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\PersonalAccount;
|
||||
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Entities\EmailAccount;
|
||||
use Espo\ORM\EntityManager;
|
||||
|
||||
class AccountFactory
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
private EntityManager $entityManager
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @throws Error
|
||||
*/
|
||||
public function create(string $id): Account
|
||||
{
|
||||
$entity = $this->entityManager->getEntityById(EmailAccount::ENTITY_TYPE, $id);
|
||||
|
||||
if (!$entity) {
|
||||
throw new Error("EmailAccount '{$id}' not found.");
|
||||
}
|
||||
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindInstance(EmailAccount::class, $entity)
|
||||
->build();
|
||||
|
||||
return $this->injectableFactory->createWithBinding(Account::class, $binding);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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\PersonalAccount;
|
||||
|
||||
use Espo\Core\Binding\Factory;
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Mail\Account\Hook\AfterFetch;
|
||||
use Espo\Core\Mail\Account\PersonalAccount\Hooks\AfterFetch as PersonalAccountAfterFetch;
|
||||
use Espo\Core\Mail\Account\Fetcher;
|
||||
use Espo\Core\Mail\Account\StorageFactory;
|
||||
use Espo\Core\Mail\Account\PersonalAccount\StorageFactory as PersonalAccountStorageFactory;
|
||||
|
||||
/**
|
||||
* @implements Factory<Fetcher>
|
||||
*/
|
||||
class FetcherFactory implements Factory
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function create(): Fetcher
|
||||
{
|
||||
$binding = BindingContainerBuilder::create()
|
||||
->bindImplementation(StorageFactory::class, PersonalAccountStorageFactory::class)
|
||||
->bindImplementation(AfterFetch::class, PersonalAccountAfterFetch::class)
|
||||
->build();
|
||||
|
||||
return $this->injectableFactory->createWithBinding(Fetcher::class, $binding);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?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\PersonalAccount\Hooks;
|
||||
|
||||
use Espo\Core\Mail\Account\Account;
|
||||
use Espo\Core\Mail\Account\Hook\BeforeFetchResult;
|
||||
use Espo\Core\Mail\Account\Hook\AfterFetch as AfterFetchInterface;
|
||||
use Espo\Tools\Stream\Service as StreamService;
|
||||
use Espo\Entities\Email;
|
||||
|
||||
class AfterFetch implements AfterFetchInterface
|
||||
{
|
||||
public function __construct(
|
||||
private StreamService $streamService
|
||||
) {}
|
||||
|
||||
public function process(Account $account, Email $email, BeforeFetchResult $beforeFetchResult): void
|
||||
{
|
||||
if (!$email->isFetched()) {
|
||||
$this->noteAboutEmail($email);
|
||||
}
|
||||
}
|
||||
|
||||
private function noteAboutEmail(Email $email): void
|
||||
{
|
||||
if (!$email->getParent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->streamService->noteEmailReceived($email->getParent(), $email);
|
||||
}
|
||||
}
|
||||
197
application/Espo/Core/Mail/Account/PersonalAccount/Service.php
Normal file
197
application/Espo/Core/Mail/Account/PersonalAccount/Service.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?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\PersonalAccount;
|
||||
|
||||
use Espo\Core\Exceptions\ErrorSilent;
|
||||
use Espo\Core\Mail\Account\Util\NotificationHelper;
|
||||
use Espo\Core\Mail\Exceptions\ImapError;
|
||||
use Espo\Core\Mail\Exceptions\NoImap;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Mail\Account\Account as Account;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Mail\Account\Fetcher;
|
||||
use Espo\Core\Mail\Account\Storage\Params;
|
||||
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
|
||||
{
|
||||
public function __construct(
|
||||
private Fetcher $fetcher,
|
||||
private AccountFactory $accountFactory,
|
||||
private StorageFactory $storageFactory,
|
||||
private User $user,
|
||||
private Log $log,
|
||||
private NotificationHelper $notificationHelper
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param string $id Account ID.
|
||||
* @throws Error
|
||||
* @throws NoImap
|
||||
* @throws ImapError
|
||||
*/
|
||||
public function fetch(string $id): void
|
||||
{
|
||||
$account = $this->accountFactory->create($id);
|
||||
|
||||
try {
|
||||
$this->fetcher->fetch($account);
|
||||
} catch (ImapError $e) {
|
||||
$this->notificationHelper->processImapError($account);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$account->updateConnectedAt();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
* @throws ImapError
|
||||
*/
|
||||
public function getFolderList(Params $params): array
|
||||
{
|
||||
$userId = $params->getUserId();
|
||||
|
||||
if (
|
||||
$userId &&
|
||||
!$this->user->isAdmin() &&
|
||||
$userId !== $this->user->getId()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($params->getId()) {
|
||||
$account = $this->accountFactory->create($params->getId());
|
||||
|
||||
$params = $params
|
||||
->withPassword($this->getPassword($params, $account))
|
||||
->withImapHandlerClassName($account->getImapHandlerClassName());
|
||||
}
|
||||
|
||||
$storage = $this->storageFactory->createWithParams($params);
|
||||
|
||||
return $storage->getFolderNames();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws Forbidden
|
||||
* @throws Error
|
||||
*/
|
||||
public function testConnection(Params $params): void
|
||||
{
|
||||
$userId = $params->getUserId();
|
||||
|
||||
if (
|
||||
$userId &&
|
||||
!$this->user->isAdmin() &&
|
||||
$userId !== $this->user->getId()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if (!$params->getId() && $params->getPassword() === null) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
if ($params->getId()) {
|
||||
$account = $this->accountFactory->create($params->getId());
|
||||
|
||||
if (
|
||||
!$this->user->isAdmin() &&
|
||||
$account->getUser()->getId() !== $this->user->getId()
|
||||
) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
$params = $params
|
||||
->withPassword($this->getPassword($params, $account))
|
||||
->withImapHandlerClassName($account->getImapHandlerClassName());
|
||||
}
|
||||
|
||||
try {
|
||||
$storage = $this->storageFactory->createWithParams($params);
|
||||
$storage->getFolderNames();
|
||||
} catch (Exception $e) {
|
||||
$this->log->warning("IMAP test connection failed; {message}", [
|
||||
'exception' => $e,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
$message = $e instanceof ExceptionInterface || $e instanceof ImapError ?
|
||||
$e->getMessage() : '';
|
||||
|
||||
throw new ErrorSilent($message);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPassword(Params $params, Account $account): ?string
|
||||
{
|
||||
$password = $params->getPassword();
|
||||
|
||||
if ($password !== null) {
|
||||
return $password;
|
||||
}
|
||||
|
||||
$imapParams = $account->getImapParams();
|
||||
|
||||
return $imapParams?->getPassword();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id Account ID.
|
||||
* @throws Error
|
||||
* @throws ImapError
|
||||
* @throws NoImap
|
||||
*/
|
||||
public function storeSentMessage(string $id, Message $message): void
|
||||
{
|
||||
$account = $this->accountFactory->create($id);
|
||||
|
||||
$folder = $account->getSentFolder();
|
||||
|
||||
if (!$folder) {
|
||||
throw new Error("No sent folder for Email Account $id.");
|
||||
}
|
||||
|
||||
$storage = $this->storageFactory->create($account);
|
||||
|
||||
$storage->appendMessage($message->toString(), $folder);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?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\PersonalAccount;
|
||||
|
||||
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,
|
||||
) {}
|
||||
|
||||
public function create(Account $account): LaminasStorage
|
||||
{
|
||||
$userLink = $account->getUser();
|
||||
|
||||
if (!$userLink) {
|
||||
throw new LogicException("No user for mail account.");
|
||||
}
|
||||
|
||||
$userId = $userLink->getId();
|
||||
|
||||
$imapParams = $account->getImapParams();
|
||||
|
||||
if (!$imapParams) {
|
||||
throw new NoImap("No IMAP params.");
|
||||
}
|
||||
|
||||
$params = Params::createBuilder()
|
||||
->setHost($imapParams->getHost())
|
||||
->setPort($imapParams->getPort())
|
||||
->setSecurity($imapParams->getSecurity())
|
||||
->setUsername($imapParams->getUsername())
|
||||
->setPassword($imapParams->getPassword())
|
||||
->setEmailAddress($account->getEmailAddress())
|
||||
->setUserId($userId)
|
||||
->setId($account->getId())
|
||||
->setImapHandlerClassName($account->getImapHandlerClassName())
|
||||
->build();
|
||||
|
||||
return $this->createWithParams($params);
|
||||
}
|
||||
|
||||
public function createWithParams(Params $params): LaminasStorage
|
||||
{
|
||||
$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);
|
||||
}
|
||||
}
|
||||
248
application/Espo/Core/Mail/Account/SendingAccountProvider.php
Normal file
248
application/Espo/Core/Mail/Account/SendingAccountProvider.php
Normal file
@@ -0,0 +1,248 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Acl\Table;
|
||||
use Espo\Core\AclManager;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
use Espo\Core\Mail\Account\GroupAccount\AccountFactory as GroupAccountFactory;
|
||||
use Espo\Core\Mail\Account\PersonalAccount\AccountFactory as PersonalAccountFactory;
|
||||
use Espo\Core\Mail\ConfigDataProvider;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Entities\EmailAccount as EmailAccountEntity;
|
||||
use Espo\Entities\InboundEmail as InboundEmailEntity;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\ORM\Query\Part\Condition;
|
||||
use Espo\ORM\Query\Part\Expression;
|
||||
use RuntimeException;
|
||||
|
||||
class SendingAccountProvider
|
||||
{
|
||||
private ?Account $system = null;
|
||||
private bool $systemIsCached = false;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private GroupAccountFactory $groupAccountFactory,
|
||||
private PersonalAccountFactory $personalAccountFactory,
|
||||
private AclManager $aclManager,
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
) {}
|
||||
|
||||
public function getShared(User $user, string $emailAddress): ?Account
|
||||
{
|
||||
$level = $this->aclManager->getPermissionLevel($user, 'groupEmailAccount');
|
||||
|
||||
$entity = null;
|
||||
|
||||
if ($level === Table::LEVEL_TEAM) {
|
||||
$teamIdList = $user->getTeamIdList();
|
||||
|
||||
if ($teamIdList === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = $this->entityManager
|
||||
->getRDBRepositoryByClass(InboundEmailEntity::class)
|
||||
->select([Attribute::ID])
|
||||
->distinct()
|
||||
->join(Field::TEAMS)
|
||||
->where([
|
||||
'status' => InboundEmailEntity::STATUS_ACTIVE,
|
||||
'useSmtp' => true,
|
||||
'smtpIsShared' => true,
|
||||
'teamsMiddle.teamId' => $teamIdList,
|
||||
])
|
||||
->where(
|
||||
Condition::equal(
|
||||
Expression::lowerCase(
|
||||
Expression::column('emailAddress')
|
||||
),
|
||||
strtolower($emailAddress)
|
||||
)
|
||||
)
|
||||
->findOne();
|
||||
}
|
||||
|
||||
if ($level === Table::LEVEL_ALL) {
|
||||
$entity = $this->entityManager
|
||||
->getRDBRepositoryByClass(InboundEmailEntity::class)
|
||||
->select([Attribute::ID])
|
||||
->where([
|
||||
'status' => InboundEmailEntity::STATUS_ACTIVE,
|
||||
'useSmtp' => true,
|
||||
'smtpIsShared' => true,
|
||||
])
|
||||
->where(
|
||||
Condition::equal(
|
||||
Expression::lowerCase(
|
||||
Expression::column('emailAddress')
|
||||
),
|
||||
strtolower($emailAddress)
|
||||
)
|
||||
)
|
||||
->findOne();
|
||||
}
|
||||
|
||||
if (!$entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->groupAccountFactory->create($entity->getId());
|
||||
} catch (Error) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
public function getGroup(string $emailAddress): ?Account
|
||||
{
|
||||
$entity = $this->entityManager
|
||||
->getRDBRepositoryByClass(InboundEmailEntity::class)
|
||||
->select([Attribute::ID])
|
||||
->where([
|
||||
'status' => InboundEmailEntity::STATUS_ACTIVE,
|
||||
'useSmtp' => true,
|
||||
'smtpHost!=' => null,
|
||||
])
|
||||
->where(
|
||||
Condition::equal(
|
||||
Expression::lowerCase(
|
||||
Expression::column('emailAddress')
|
||||
),
|
||||
strtolower($emailAddress)
|
||||
)
|
||||
)
|
||||
->findOne();
|
||||
|
||||
if (!$entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->groupAccountFactory->create($entity->getId());
|
||||
} catch (Error) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a personal user account.
|
||||
*/
|
||||
public function getPersonal(User $user, ?string $emailAddress): ?Account
|
||||
{
|
||||
if (!$emailAddress) {
|
||||
$emailAddress = $user->getEmailAddress();
|
||||
}
|
||||
|
||||
if (!$emailAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$entity = $this->entityManager
|
||||
->getRDBRepositoryByClass(EmailAccountEntity::class)
|
||||
->select([Attribute::ID])
|
||||
->where([
|
||||
'assignedUserId' => $user->getId(),
|
||||
'status' => EmailAccountEntity::STATUS_ACTIVE,
|
||||
'useSmtp' => true,
|
||||
])
|
||||
->where(
|
||||
Condition::equal(
|
||||
Expression::lowerCase(
|
||||
Expression::column('emailAddress')
|
||||
),
|
||||
strtolower($emailAddress)
|
||||
)
|
||||
)
|
||||
->findOne();
|
||||
|
||||
if (!$entity) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->personalAccountFactory->create($entity->getId());
|
||||
} catch (Error) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a system account.
|
||||
*/
|
||||
public function getSystem(): ?Account
|
||||
{
|
||||
if (!$this->systemIsCached) {
|
||||
$this->loadSystem();
|
||||
|
||||
$this->systemIsCached = true;
|
||||
}
|
||||
|
||||
return $this->system;
|
||||
}
|
||||
|
||||
private function loadSystem(): void
|
||||
{
|
||||
$address = $this->configDataProvider->getSystemOutboundAddress();
|
||||
|
||||
if (!$address) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entity = $this->entityManager
|
||||
->getRDBRepositoryByClass(InboundEmailEntity::class)
|
||||
->where([
|
||||
'status' => InboundEmailEntity::STATUS_ACTIVE,
|
||||
'useSmtp' => true,
|
||||
])
|
||||
->where(
|
||||
Condition::equal(
|
||||
Expression::lowerCase(
|
||||
Expression::column('emailAddress')
|
||||
),
|
||||
strtolower($address)
|
||||
)
|
||||
)
|
||||
->findOne();
|
||||
|
||||
if (!$entity) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->system = $this->groupAccountFactory->create($entity->getId());
|
||||
} catch (Error) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
98
application/Espo/Core/Mail/Account/Storage.php
Normal file
98
application/Espo/Core/Mail/Account/Storage.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Field\DateTime;
|
||||
|
||||
interface Storage
|
||||
{
|
||||
/**
|
||||
* Set message flags.
|
||||
*
|
||||
* @param string[] $flags
|
||||
*/
|
||||
public function setFlags(int $id, array $flags): void;
|
||||
|
||||
/**
|
||||
* Get a message size.
|
||||
*/
|
||||
public function getSize(int $id): int;
|
||||
|
||||
/**
|
||||
* Get message raw content.
|
||||
*/
|
||||
public function getRawContent(int $id): string;
|
||||
|
||||
/**
|
||||
* Get a message unique ID.
|
||||
*/
|
||||
public function getUniqueId(int $id): string;
|
||||
|
||||
/**
|
||||
* Get IDs from unique ID.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function getIdsFromUniqueId(string $uniqueId): array;
|
||||
|
||||
/**
|
||||
* Get IDs since a specific date.
|
||||
*
|
||||
* @return int[]
|
||||
*/
|
||||
public function getIdsSinceDate(DateTime $since): array;
|
||||
|
||||
/**
|
||||
* Get only header and flags. Won't fetch the whole email.
|
||||
*
|
||||
* @return array{header: string, flags: string[]}
|
||||
*/
|
||||
public function getHeaderAndFlags(int $id): array;
|
||||
|
||||
/**
|
||||
* Close the resource.
|
||||
*/
|
||||
public function close(): void;
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getFolderNames(): array;
|
||||
|
||||
/**
|
||||
* Select a folder.
|
||||
*/
|
||||
public function selectFolder(string $name): void;
|
||||
|
||||
/**
|
||||
* Store a message.
|
||||
*/
|
||||
public function appendMessage(string $content, ?string $folder = null): void;
|
||||
}
|
||||
49
application/Espo/Core/Mail/Account/Storage/Flag.php
Normal file
49
application/Espo/Core/Mail/Account/Storage/Flag.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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;
|
||||
|
||||
class Flag
|
||||
{
|
||||
public const PASSED = 'Passed';
|
||||
|
||||
public const SEEN = '\Seen';
|
||||
|
||||
public const UNSEEN = '\Unseen';
|
||||
|
||||
public const ANSWERED = '\Answered';
|
||||
|
||||
public const FLAGGED = '\Flagged';
|
||||
|
||||
public const DELETED = '\Deleted';
|
||||
|
||||
public const DRAFT = '\Draft';
|
||||
|
||||
public const RECENT = '\Recent';
|
||||
}
|
||||
134
application/Espo/Core/Mail/Account/Storage/LaminasStorage.php
Normal file
134
application/Espo/Core/Mail/Account/Storage/LaminasStorage.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
130
application/Espo/Core/Mail/Account/Storage/Params.php
Normal file
130
application/Espo/Core/Mail/Account/Storage/Params.php
Normal file
@@ -0,0 +1,130 @@
|
||||
<?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 SensitiveParameter;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class Params
|
||||
{
|
||||
/** @var ?class-string<object> */
|
||||
private ?string $imapHandlerClassName;
|
||||
|
||||
/**
|
||||
* @param ?class-string<object> $imapHandlerClassName
|
||||
*/
|
||||
public function __construct(
|
||||
private ?string $host,
|
||||
private ?int $port,
|
||||
private ?string $username,
|
||||
private ?string $password,
|
||||
private ?string $security,
|
||||
?string $imapHandlerClassName,
|
||||
private ?string $id,
|
||||
private ?string $userId,
|
||||
private ?string $emailAddress
|
||||
) {
|
||||
$this->imapHandlerClassName = $imapHandlerClassName;
|
||||
}
|
||||
|
||||
public static function createBuilder(): ParamsBuilder
|
||||
{
|
||||
return new ParamsBuilder();
|
||||
}
|
||||
|
||||
public function getHost(): ?string
|
||||
{
|
||||
return $this->host;
|
||||
}
|
||||
|
||||
public function getPort(): ?int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getSecurity(): ?string
|
||||
{
|
||||
return $this->security;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string
|
||||
*/
|
||||
public function getImapHandlerClassName(): ?string
|
||||
{
|
||||
return $this->imapHandlerClassName;
|
||||
}
|
||||
|
||||
public function getId(): ?string
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getUserId(): ?string
|
||||
{
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
public function getEmailAddress(): ?string
|
||||
{
|
||||
return $this->emailAddress;
|
||||
}
|
||||
|
||||
public function withPassword(#[SensitiveParameter] ?string $password): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->password = $password;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?class-string $imapHandlerClassName
|
||||
*/
|
||||
public function withImapHandlerClassName(?string $imapHandlerClassName): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->imapHandlerClassName = $imapHandlerClassName;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
127
application/Espo/Core/Mail/Account/Storage/ParamsBuilder.php
Normal file
127
application/Espo/Core/Mail/Account/Storage/ParamsBuilder.php
Normal file
@@ -0,0 +1,127 @@
|
||||
<?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 SensitiveParameter;
|
||||
|
||||
class ParamsBuilder
|
||||
{
|
||||
private ?string $host = null;
|
||||
private ?int $port = null;
|
||||
private ?string $username = null;
|
||||
private ?string $password = null;
|
||||
private ?string $security = null;
|
||||
/** @var ?class-string<object> */
|
||||
private ?string $imapHandlerClassName = null;
|
||||
private ?string $id = null;
|
||||
private ?string $userId = null;
|
||||
private ?string $emailAddress = null;
|
||||
|
||||
public function build(): Params
|
||||
{
|
||||
return new Params(
|
||||
$this->host,
|
||||
$this->port,
|
||||
$this->username,
|
||||
$this->password,
|
||||
$this->security,
|
||||
$this->imapHandlerClassName,
|
||||
$this->id,
|
||||
$this->userId,
|
||||
$this->emailAddress
|
||||
);
|
||||
}
|
||||
|
||||
public function setHost(?string $host): self
|
||||
{
|
||||
$this->host = $host;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPort(?int $port): self
|
||||
{
|
||||
$this->port = $port;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUsername(?string $username): self
|
||||
{
|
||||
$this->username = $username;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPassword(#[SensitiveParameter] ?string $password): self
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setSecurity(?string $security): self
|
||||
{
|
||||
$this->security = $security;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?class-string<object> $imapHandlerClassName
|
||||
*/
|
||||
public function setImapHandlerClassName(?string $imapHandlerClassName): self
|
||||
{
|
||||
$this->imapHandlerClassName = $imapHandlerClassName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setId(?string $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUserId(?string $userId): self
|
||||
{
|
||||
$this->userId = $userId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setEmailAddress(?string $emailAddress): self
|
||||
{
|
||||
$this->emailAddress = $emailAddress;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
52
application/Espo/Core/Mail/Account/StorageFactory.php
Normal file
52
application/Espo/Core/Mail/Account/StorageFactory.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Mail\Account\Storage\Params;
|
||||
use Espo\Core\Mail\Exceptions\ImapError;
|
||||
use Espo\Core\Mail\Exceptions\NoImap;
|
||||
|
||||
interface StorageFactory
|
||||
{
|
||||
/**
|
||||
* Create an account.
|
||||
*
|
||||
* @throws NoImap
|
||||
* @throws ImapError
|
||||
*/
|
||||
public function create(Account $account): Storage;
|
||||
|
||||
/**
|
||||
* Create an account with parameters.
|
||||
*
|
||||
* @throws ImapError
|
||||
*/
|
||||
public function createWithParams(Params $params): Storage;
|
||||
}
|
||||
147
application/Espo/Core/Mail/Account/Util/NotificationHelper.php
Normal file
147
application/Espo/Core/Mail/Account/Util/NotificationHelper.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?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\Util;
|
||||
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Field\LinkParent;
|
||||
use Espo\Core\Mail\Account\Account;
|
||||
use Espo\Core\Utils\Language;
|
||||
use Espo\Entities\InboundEmail;
|
||||
use Espo\Entities\Notification;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
|
||||
class NotificationHelper
|
||||
{
|
||||
private const PERIOD = '1 day';
|
||||
private const WAIT_PERIOD = '10 minutes';
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Language $language
|
||||
) {}
|
||||
|
||||
public function processImapError(Account $account): void
|
||||
{
|
||||
$userId = $account->getUser()?->getId();
|
||||
$id = $account->getId();
|
||||
$entityType = $account->getEntityType();
|
||||
|
||||
if (!$id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
$account->getConnectedAt() &&
|
||||
DateTime::createNow()
|
||||
->modify('-' . self::WAIT_PERIOD)
|
||||
->isLessThan($account->getConnectedAt())
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$userIds = [];
|
||||
|
||||
if ($entityType === InboundEmail::ENTITY_TYPE) {
|
||||
$userIds = $this->getAdminUserIds();
|
||||
} else if ($userId) {
|
||||
$userIds[] = $userId;
|
||||
}
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
$this->processImapErrorForUser($entityType, $id, $userId);
|
||||
}
|
||||
}
|
||||
|
||||
private function exists(string $entityType, string $id, string $userId): bool
|
||||
{
|
||||
$one = $this->entityManager
|
||||
->getRDBRepositoryByClass(Notification::class)
|
||||
->where([
|
||||
'relatedId' => $id,
|
||||
'relatedType' => $entityType,
|
||||
'userId' => $userId,
|
||||
'createdAt>' => DateTime::createNow()->modify('-' . self::PERIOD)->toString(),
|
||||
])
|
||||
->findOne();
|
||||
|
||||
return $one !== null;
|
||||
}
|
||||
|
||||
private function getMessage(string $entityType, string $id): string
|
||||
{
|
||||
$message = $this->language->translateLabel('imapNotConnected', 'messages', $entityType);
|
||||
|
||||
return str_replace('{id}', $id, $message);
|
||||
}
|
||||
|
||||
private function processImapErrorForUser(string $entityType, string $id, string $userId): void
|
||||
{
|
||||
if ($this->exists($entityType, $id, $userId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$notification = $this->entityManager->getRDBRepositoryByClass(Notification::class)->getNew();
|
||||
|
||||
$message = $this->getMessage($entityType, $id);
|
||||
|
||||
$notification
|
||||
->setType(Notification::TYPE_MESSAGE)
|
||||
->setMessage($message)
|
||||
->setUserId($userId)
|
||||
->setRelated(LinkParent::create($entityType, $id));
|
||||
|
||||
$this->entityManager->saveEntity($notification);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function getAdminUserIds(): array
|
||||
{
|
||||
$users = $this->entityManager
|
||||
->getRDBRepositoryByClass(User::class)
|
||||
->select([Attribute::ID])
|
||||
->where([
|
||||
'isActive' => true,
|
||||
'type' => User::TYPE_ADMIN,
|
||||
])
|
||||
->find();
|
||||
|
||||
$ids = [];
|
||||
|
||||
foreach ($users as $user) {
|
||||
$ids[] = $user->getId();
|
||||
}
|
||||
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
49
application/Espo/Core/Mail/ConfigDataProvider.php
Normal file
49
application/Espo/Core/Mail/ConfigDataProvider.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Utils\Config;
|
||||
|
||||
class ConfigDataProvider
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
) {}
|
||||
|
||||
public function getSystemOutboundAddress(): ?string
|
||||
{
|
||||
return $this->config->get('outboundEmailFromAddress');
|
||||
}
|
||||
|
||||
public function isSystemOutboundAddressShared(): bool
|
||||
{
|
||||
return (bool) $this->config->get('outboundEmailIsShared');
|
||||
}
|
||||
}
|
||||
53
application/Espo/Core/Mail/EmailFactory.php
Normal file
53
application/Espo/Core/Mail/EmailFactory.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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;
|
||||
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Entities\Email;
|
||||
|
||||
/**
|
||||
* Creates email instances.
|
||||
*/
|
||||
class EmailFactory
|
||||
{
|
||||
public function __construct(private EntityManager $entityManager)
|
||||
{}
|
||||
|
||||
/**
|
||||
* Create an email instance.
|
||||
*/
|
||||
public function create(): Email
|
||||
{
|
||||
/** @var Email $email */
|
||||
$email = $this->entityManager->getNewEntity(Email::ENTITY_TYPE);
|
||||
|
||||
return $email;
|
||||
}
|
||||
}
|
||||
193
application/Espo/Core/Mail/EmailSender.php
Normal file
193
application/Espo/Core/Mail/EmailSender.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Binding\BindingContainerBuilder;
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Mail\Account\SendingAccountProvider;
|
||||
use Espo\Core\Name\Field;
|
||||
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.
|
||||
*/
|
||||
class EmailSender
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private SendingAccountProvider $accountProvider,
|
||||
private InjectableFactory $injectableFactory
|
||||
) {}
|
||||
|
||||
private function createSender(): Sender
|
||||
{
|
||||
return $this->injectableFactory->createWithBinding(
|
||||
Sender::class,
|
||||
BindingContainerBuilder
|
||||
::create()
|
||||
->bindInstance(SendingAccountProvider::class, $this->accountProvider)
|
||||
->build()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a builder.
|
||||
*/
|
||||
public function create(): Sender
|
||||
{
|
||||
return $this->createSender();
|
||||
}
|
||||
|
||||
/**
|
||||
* With parameters.
|
||||
*
|
||||
* @param SenderParams|array<string, mixed> $params
|
||||
*/
|
||||
public function withParams($params): Sender
|
||||
{
|
||||
return $this->createSender()->withParams($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* With specific SMTP parameters.
|
||||
*
|
||||
* @param SmtpParams|array<string, mixed> $params
|
||||
*/
|
||||
public function withSmtpParams($params): Sender
|
||||
{
|
||||
return $this->createSender()->withSmtpParams($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* With specific attachments.
|
||||
*
|
||||
* @param iterable<Attachment> $attachmentList
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function withAttachments(iterable $attachmentList): Sender
|
||||
{
|
||||
return $this->createSender()->withAttachments($attachmentList);
|
||||
}
|
||||
|
||||
/**
|
||||
* With an envelope from address.
|
||||
*
|
||||
* @since 9.1.0
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function withEnvelopeFromAddress(string $fromAddress): void
|
||||
{
|
||||
$this->createSender()->withEnvelopeFromAddress($fromAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* With envelope options.
|
||||
*
|
||||
* @param array{from: string} $options
|
||||
* @deprecated As of v9.1.
|
||||
* @todo Remove in v10.0. Use `withEnvelopeFromAddress`.
|
||||
*/
|
||||
public function withEnvelopeOptions(array $options): Sender
|
||||
{
|
||||
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.
|
||||
*
|
||||
* @param string $name A header name.
|
||||
* @param string $value A header value.
|
||||
* @since 9.1.0
|
||||
*/
|
||||
public function withAddedHeader(string $name, string $value): Sender
|
||||
{
|
||||
return $this->createSender()->withAddedHeader($name, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether system SMTP is configured.
|
||||
*/
|
||||
public function hasSystemSmtp(): bool
|
||||
{
|
||||
if ($this->config->get('smtpServer')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->accountProvider->getSystem()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email.
|
||||
*
|
||||
* @throws Exceptions\SendingError
|
||||
*/
|
||||
public function send(Email $email): void
|
||||
{
|
||||
$this->createSender()->send($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a message ID.
|
||||
*/
|
||||
static public function generateMessageId(Email $email): string
|
||||
{
|
||||
$rand = mt_rand(1000, 9999);
|
||||
|
||||
$messageId = $email->getParentType() && $email->getParentId() ?
|
||||
sprintf("%s/%s/%s/%s@espo", $email->getParentType(), $email->getParentId(), time(), $rand) :
|
||||
sprintf("%s/%s/%s@espo", md5($email->get(Field::NAME)), time(), $rand);
|
||||
|
||||
if ($email->get('isSystem')) {
|
||||
$messageId .= '-system';
|
||||
}
|
||||
|
||||
return $messageId;
|
||||
}
|
||||
}
|
||||
246
application/Espo/Core/Mail/Event/Event.php
Normal file
246
application/Espo/Core/Mail/Event/Event.php
Normal file
@@ -0,0 +1,246 @@
|
||||
<?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\Event;
|
||||
|
||||
use Espo\Core\Utils\DateTime as DateTimeUtil;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use RuntimeException;
|
||||
|
||||
class Event
|
||||
{
|
||||
private ?string $attendees = null;
|
||||
private ?string $organizer = null;
|
||||
private ?string $dateStart = null;
|
||||
private ?string $dateEnd = null;
|
||||
private ?string $location = null;
|
||||
private ?string $name = null;
|
||||
private ?string $description = null;
|
||||
private ?string $timezone = null;
|
||||
private ?string $uid = null;
|
||||
|
||||
private bool $isAllDay = false;
|
||||
|
||||
public function withAttendees(?string $attendees): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->attendees = $attendees;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withOrganizer(?string $organizer): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->organizer = $organizer;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withDateStart(?string $dateStart): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->dateStart = $dateStart;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withDateEnd(?string $dateEnd): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->dateEnd = $dateEnd;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withLocation(?string $location): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->location = $location;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withName(?string $name): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->name = $name;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withDescription(?string $description): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->description = $description;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withTimezone(?string $timezone): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->timezone = $timezone;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withUid(?string $uid): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->uid = $uid;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withIsAllDay(bool $isAllDay): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->isAllDay = $isAllDay;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function getUid(): ?string
|
||||
{
|
||||
return $this->uid;
|
||||
}
|
||||
|
||||
public function isAllDay(): bool
|
||||
{
|
||||
return $this->isAllDay;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getDateStart(): ?string
|
||||
{
|
||||
return $this->convertDate($this->dateStart);
|
||||
}
|
||||
|
||||
public function getDateEnd(): ?string
|
||||
{
|
||||
return $this->convertDate($this->dateEnd, true);
|
||||
}
|
||||
|
||||
public function getLocation(): ?string
|
||||
{
|
||||
return $this->location;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
private function convertDate(?string $value, bool $isEnd = false): ?string
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isAllDay) {
|
||||
$dt = DateTime::createFromFormat('Ymd', $value);
|
||||
|
||||
if ($dt === false) {
|
||||
throw new RuntimeException("Could not parse '{$value}'.");
|
||||
}
|
||||
|
||||
if ($isEnd) {
|
||||
$dt->modify('-1 day');
|
||||
}
|
||||
|
||||
return $dt->format(DateTimeUtil::SYSTEM_DATE_FORMAT);
|
||||
}
|
||||
|
||||
$timezone = $this->timezone ?? 'UTC';
|
||||
|
||||
$dt = DateTime::createFromFormat('Ymd\THis', $value, new DateTimeZone($timezone));
|
||||
|
||||
if ($dt === false) {
|
||||
throw new RuntimeException("Could not parse '{$value}'.");
|
||||
}
|
||||
|
||||
$dt->setTimezone(new DateTimeZone('UTC'));
|
||||
|
||||
return $dt->format(DateTimeUtil::SYSTEM_DATE_TIME_FORMAT);
|
||||
}
|
||||
|
||||
public function getOrganizerEmailAddress(): ?string
|
||||
{
|
||||
return $this->getEmailAddressFromAttendee($this->organizer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAttendeeEmailAddressList(): array
|
||||
{
|
||||
if ($this->attendees === null || $this->attendees === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach (explode(',', $this->attendees) as $item) {
|
||||
$emailAddress = $this->getEmailAddressFromAttendee($item);
|
||||
|
||||
if ($emailAddress === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$list[] = $emailAddress;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
private function getEmailAddressFromAttendee(?string $item): ?string
|
||||
{
|
||||
if ($item === null || $item === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (explode(':', $item)[0] !== 'MAILTO') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return explode(':', $item)[1] ?? null;
|
||||
}
|
||||
}
|
||||
70
application/Espo/Core/Mail/Event/EventFactory.php
Normal file
70
application/Espo/Core/Mail/Event/EventFactory.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?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\Event;
|
||||
|
||||
use ICal\Event as ICalEvent;
|
||||
use ICal\ICal as U01jmg3ICal;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class EventFactory
|
||||
{
|
||||
public static function createFromU01jmg3Ical(U01jmg3ICal $ical): Event
|
||||
{
|
||||
/* @var ?ICalEvent $event */
|
||||
$event = $ical->events()[0] ?? null;
|
||||
|
||||
if (!$event) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$dateStart = $event->dtstart_tz ?? null;
|
||||
$dateEnd = $event->dtend_tz ?? null;
|
||||
|
||||
$isAllDay = strlen($event->dtstart) === 8;
|
||||
|
||||
if ($isAllDay) {
|
||||
$dateStart = $event->dtstart ?? null;
|
||||
$dateEnd = $event->dtend ?? null;
|
||||
}
|
||||
|
||||
return Event::create()
|
||||
->withUid($event->uid ?? null)
|
||||
->withIsAllDay($isAllDay)
|
||||
->withDateStart($dateStart)
|
||||
->withDateEnd($dateEnd)
|
||||
->withName($event->summary ?? null)
|
||||
->withLocation($event->location ?? null)
|
||||
->withDescription($event->description ?? null)
|
||||
->withTimezone($ical->calendarTimeZone() ?? null) /** @phpstan-ignore-line */
|
||||
->withOrganizer($event->organizer ?? null)
|
||||
->withAttendees($event->attendee ?? null);
|
||||
}
|
||||
}
|
||||
32
application/Espo/Core/Mail/Exceptions/ImapError.php
Normal file
32
application/Espo/Core/Mail/Exceptions/ImapError.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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\Exceptions;
|
||||
|
||||
class ImapError extends \Exception {}
|
||||
32
application/Espo/Core/Mail/Exceptions/NoImap.php
Normal file
32
application/Espo/Core/Mail/Exceptions/NoImap.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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\Exceptions;
|
||||
|
||||
class NoImap extends \Exception {}
|
||||
32
application/Espo/Core/Mail/Exceptions/NoSmtp.php
Normal file
32
application/Espo/Core/Mail/Exceptions/NoSmtp.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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\Exceptions;
|
||||
|
||||
class NoSmtp extends SendingError {}
|
||||
32
application/Espo/Core/Mail/Exceptions/SendingError.php
Normal file
32
application/Espo/Core/Mail/Exceptions/SendingError.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?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\Exceptions;
|
||||
|
||||
class SendingError extends \Exception {}
|
||||
208
application/Espo/Core/Mail/FiltersMatcher.php
Normal file
208
application/Espo/Core/Mail/FiltersMatcher.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailFilter;
|
||||
|
||||
class FiltersMatcher
|
||||
{
|
||||
/**
|
||||
* @param iterable<EmailFilter> $filterList
|
||||
* @param bool $skipBody Not to match if the body-contains is not empty.
|
||||
*/
|
||||
public function findMatch(Email $email, $filterList, bool $skipBody = false): ?EmailFilter
|
||||
{
|
||||
foreach ($filterList as $filter) {
|
||||
if ($this->match($email, $filter, $skipBody)) {
|
||||
return $filter;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $skipBody Not to match if the body-contains is not empty.
|
||||
*/
|
||||
public function match(Email $email, EmailFilter $filter, bool $skipBody = false): bool
|
||||
{
|
||||
$filterCount = 0;
|
||||
|
||||
$from = $filter->getFrom();
|
||||
$subject = $filter->getSubject();
|
||||
|
||||
if ($from) {
|
||||
$filterCount++;
|
||||
|
||||
if (
|
||||
!$this->matchString(
|
||||
strtolower($from),
|
||||
strtolower($email->getFromAddress() ?? '')
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($filter->getTo()) {
|
||||
$filterCount++;
|
||||
|
||||
if (!$this->matchTo($email, $filter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($subject) {
|
||||
$filterCount++;
|
||||
|
||||
if (
|
||||
!$this->matchString($subject, $email->getSubject() ?? '')
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($filter->getBodyContains())) {
|
||||
$filterCount++;
|
||||
|
||||
if ($skipBody) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->matchBody($email, $filter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($filter->getBodyContainsAll())) {
|
||||
$filterCount++;
|
||||
|
||||
if ($skipBody) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->matchBodyAll($email, $filter)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($filterCount) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function matchTo(Email $email, EmailFilter $filter): bool
|
||||
{
|
||||
$filterTo = $filter->getTo();
|
||||
|
||||
if ($filterTo === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (count($email->getToAddressList())) {
|
||||
foreach ($email->getToAddressList() as $to) {
|
||||
if (
|
||||
$this->matchString(
|
||||
strtolower($filterTo),
|
||||
strtolower($to)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function matchBody(Email $email, EmailFilter $filter): bool
|
||||
{
|
||||
$phraseList = $filter->getBodyContains();
|
||||
$body = $email->getBody();
|
||||
$bodyPlain = $email->getBodyPlain();
|
||||
|
||||
foreach ($phraseList as $phrase) {
|
||||
if ($phrase === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($bodyPlain && stripos($bodyPlain, $phrase) !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($body && stripos($body, $phrase) !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function matchBodyAll(Email $email, EmailFilter $filter): bool
|
||||
{
|
||||
$phraseList = $filter->getBodyContainsAll();
|
||||
$body = $email->getBody() ?? $email->getBodyPlain() ?? '';
|
||||
|
||||
if ($phraseList === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($phraseList as $phrase) {
|
||||
if ($phrase === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (stripos($body, $phrase) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function matchString(string $pattern, string $value): bool
|
||||
{
|
||||
if ($pattern == $value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$pattern = preg_quote($pattern, '#');
|
||||
$pattern = str_replace('\*', '.*', $pattern) . '\z';
|
||||
|
||||
if (preg_match('#^' . $pattern . '#', $value)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
41
application/Espo/Core/Mail/Importer.php
Normal file
41
application/Espo/Core/Mail/Importer.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Mail\Importer\Data;
|
||||
use Espo\Entities\Email;
|
||||
|
||||
/**
|
||||
* Imports email messages. Handles duplicate checking, parent look-up.
|
||||
*/
|
||||
interface Importer
|
||||
{
|
||||
public function import(Message $message, Data $data): ?Email;
|
||||
}
|
||||
42
application/Espo/Core/Mail/Importer/AutoReplyDetector.php
Normal file
42
application/Espo/Core/Mail/Importer/AutoReplyDetector.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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\Importer;
|
||||
|
||||
use Espo\Core\Mail\Message;
|
||||
|
||||
/**
|
||||
* Detects if an email is auto-response.
|
||||
*
|
||||
* @since 9.2.0
|
||||
*/
|
||||
interface AutoReplyDetector
|
||||
{
|
||||
public function detect(Message $message): bool;
|
||||
}
|
||||
172
application/Espo/Core/Mail/Importer/Data.php
Normal file
172
application/Espo/Core/Mail/Importer/Data.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?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\Importer;
|
||||
|
||||
use Espo\Entities\EmailFilter;
|
||||
|
||||
/**
|
||||
* Immutable.
|
||||
*/
|
||||
class Data
|
||||
{
|
||||
private ?string $assignedUserId = null;
|
||||
/** @var string[] */
|
||||
private array $teamIdList = [];
|
||||
/** @var string[] */
|
||||
private array $userIdList = [];
|
||||
/** @var iterable<EmailFilter> */
|
||||
private iterable $filterList = [];
|
||||
private bool $fetchOnlyHeader = false;
|
||||
/** @var array<string, string> */
|
||||
private array $folderData = [];
|
||||
private ?string $groupEmailFolderId = null;
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function getAssignedUserId(): ?string
|
||||
{
|
||||
return $this->assignedUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTeamIdList(): array
|
||||
{
|
||||
return $this->teamIdList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getUserIdList(): array
|
||||
{
|
||||
return $this->userIdList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<EmailFilter>
|
||||
*/
|
||||
public function getFilterList(): iterable
|
||||
{
|
||||
return $this->filterList;
|
||||
}
|
||||
|
||||
public function fetchOnlyHeader(): bool
|
||||
{
|
||||
return $this->fetchOnlyHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function getFolderData(): array
|
||||
{
|
||||
return $this->folderData;
|
||||
}
|
||||
|
||||
public function getGroupEmailFolderId(): ?string
|
||||
{
|
||||
return $this->groupEmailFolderId;
|
||||
}
|
||||
|
||||
public function withAssignedUserId(?string $assignedUserId): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
|
||||
$obj->assignedUserId = $assignedUserId;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $teamIdList
|
||||
*/
|
||||
public function withTeamIdList(array $teamIdList): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
|
||||
$obj->teamIdList = $teamIdList;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $userIdList
|
||||
*/
|
||||
public function withUserIdList(array $userIdList): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->userIdList = $userIdList;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<EmailFilter> $filterList
|
||||
*/
|
||||
public function withFilterList(iterable $filterList): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->filterList = $filterList;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withFetchOnlyHeader(bool $fetchOnlyHeader = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->fetchOnlyHeader = $fetchOnlyHeader;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $folderData
|
||||
*/
|
||||
public function withFolderData(array $folderData): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->folderData = $folderData;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withGroupEmailFolderId(?string $groupEmailFolderId): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->groupEmailFolderId = $groupEmailFolderId;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?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\Importer;
|
||||
|
||||
use Espo\Core\Mail\Message;
|
||||
|
||||
class DefaultAutoReplyDetector implements AutoReplyDetector
|
||||
{
|
||||
public function detect(Message $message): bool
|
||||
{
|
||||
if ($message->getHeader('X-Autoreply')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($message->getHeader('X-Autorespond')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
$message->getHeader('Auto-Submitted') &&
|
||||
strtolower($message->getHeader('Auto-Submitted')) === 'auto-replied'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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\Importer;
|
||||
|
||||
use Espo\Core\Mail\Message;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
|
||||
class DefaultDuplicateFinder implements DuplicateFinder
|
||||
{
|
||||
public function __construct(private EntityManager $entityManager)
|
||||
{}
|
||||
|
||||
public function find(Email $email, Message $message): ?Email
|
||||
{
|
||||
if (!$email->getMessageId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->entityManager
|
||||
->getRDBRepositoryByClass(Email::class)
|
||||
->select([Attribute::ID, 'status'])
|
||||
->where([
|
||||
'messageId' => $email->getMessageId(),
|
||||
])
|
||||
->findOne();
|
||||
}
|
||||
}
|
||||
723
application/Espo/Core/Mail/Importer/DefaultImporter.php
Normal file
723
application/Espo/Core/Mail/Importer/DefaultImporter.php
Normal file
@@ -0,0 +1,723 @@
|
||||
<?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\Importer;
|
||||
|
||||
use Espo\Core\Field\DateTime as DateTimeField;
|
||||
use Espo\Core\Field\LinkMultiple;
|
||||
use Espo\Core\Field\LinkParent;
|
||||
use Espo\Core\Job\Job\Data as JobData;
|
||||
use Espo\Core\Job\JobSchedulerFactory;
|
||||
use Espo\Core\Mail\FiltersMatcher;
|
||||
use Espo\Core\Mail\Importer;
|
||||
use Espo\Core\Mail\Message;
|
||||
use Espo\Core\Mail\MessageWrapper;
|
||||
use Espo\Core\Mail\Parser;
|
||||
use Espo\Core\Mail\ParserFactory;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\Core\Notification\AssignmentNotificator;
|
||||
use Espo\Core\Notification\AssignmentNotificatorFactory;
|
||||
use Espo\Core\Notification\AssignmentNotificator\Params as AssignmentNotificatorParams;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\FieldProcessing\Relation\LinkMultipleSaver;
|
||||
use Espo\Core\FieldProcessing\Saver\Params as SaverParams;
|
||||
use Espo\Core\Job\QueueName;
|
||||
use Espo\Core\ORM\Entity as CoreEntity;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailFilter;
|
||||
use Espo\Entities\GroupEmailFolder;
|
||||
use Espo\Entities\Team;
|
||||
use Espo\Entities\User;
|
||||
use Espo\ORM\Name\Attribute;
|
||||
use Espo\ORM\Query\Part\Condition;
|
||||
use Espo\ORM\Query\Part\Expression;
|
||||
use Espo\ORM\Query\SelectBuilder;
|
||||
use Espo\Repositories\Email as EmailRepository;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Tools\Stream\Jobs\ProcessNoteAcl;
|
||||
|
||||
use DateTime;
|
||||
use Exception;
|
||||
|
||||
class DefaultImporter implements Importer
|
||||
{
|
||||
private const SUBJECT_MAX_LENGTH = 255;
|
||||
private const PROCESS_ACL_DELAY_PERIOD = '5 seconds';
|
||||
|
||||
/** @var AssignmentNotificator<Email> */
|
||||
private AssignmentNotificator $notificator;
|
||||
private FiltersMatcher $filtersMatcher;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Config $config,
|
||||
AssignmentNotificatorFactory $notificatorFactory,
|
||||
private ParserFactory $parserFactory,
|
||||
private LinkMultipleSaver $linkMultipleSaver,
|
||||
private DuplicateFinder $duplicateFinder,
|
||||
private JobSchedulerFactory $jobSchedulerFactory,
|
||||
private ParentFinder $parentFinder,
|
||||
private AutoReplyDetector $autoReplyDetector,
|
||||
) {
|
||||
$this->notificator = $notificatorFactory->createByClass(Email::class);
|
||||
$this->filtersMatcher = new FiltersMatcher();
|
||||
}
|
||||
|
||||
public function import(Message $message, Data $data): ?Email
|
||||
{
|
||||
$parser = $this->getParser($message);
|
||||
|
||||
$email = $this->entityManager->getRDBRepositoryByClass(Email::class)->getNew();
|
||||
$email->set('isBeingImported', true);
|
||||
|
||||
$subject = $this->getSubject($parser, $message);
|
||||
|
||||
$email
|
||||
->setSubject($subject)
|
||||
->setStatus(Email::STATUS_ARCHIVED)
|
||||
->setIsHtml(false)
|
||||
->setGroupFolderId($data->getGroupEmailFolderId())
|
||||
->setTeams(LinkMultiple::create()->withAddedIdList($data->getTeamIdList()));
|
||||
|
||||
if ($data->getAssignedUserId()) {
|
||||
$email->setAssignedUserId($data->getAssignedUserId());
|
||||
$email->addAssignedUserId($data->getAssignedUserId());
|
||||
}
|
||||
|
||||
foreach ($data->getUserIdList() as $uId) {
|
||||
$email->addUserId($uId);
|
||||
}
|
||||
|
||||
$this->setFromStrings($parser, $message, $email);
|
||||
$this->setAddresses($parser, $message, $email);
|
||||
|
||||
foreach ($data->getFolderData() as $uId => $folderId) {
|
||||
$email->setUserColumnFolderId($uId, $folderId);
|
||||
}
|
||||
|
||||
$toSkip = $this->processFilters($email, $data->getFilterList(), true);
|
||||
|
||||
if ($toSkip) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$isSystemEmail = $this->processMessageId($parser, $message, $email);
|
||||
|
||||
if ($isSystemEmail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->processDate($parser, $message, $email);
|
||||
|
||||
$duplicate = $this->findDuplicate($email, $message);
|
||||
|
||||
if ($duplicate && $duplicate->getStatus() !== Email::STATUS_BEING_IMPORTED) {
|
||||
$this->entityManager->refreshEntity($duplicate);
|
||||
|
||||
$this->processDuplicate($duplicate, $data, $email->getGroupFolder()?->getId());
|
||||
|
||||
return $duplicate;
|
||||
}
|
||||
|
||||
$email->setIsAutoReply($this->autoReplyDetector->detect($message));
|
||||
|
||||
$this->processDeliveryDate($parser, $message, $email);
|
||||
|
||||
if (!$email->getDateSent()) {
|
||||
$email->setDateSent(DateTimeField::createNow());
|
||||
}
|
||||
|
||||
$inlineAttachmentList = [];
|
||||
|
||||
if (!$data->fetchOnlyHeader()) {
|
||||
$inlineAttachmentList = $parser->getInlineAttachmentList($message, $email);
|
||||
|
||||
$toSkip = $this->processFilters($email, $data->getFilterList());
|
||||
|
||||
if ($toSkip) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
$email->setBody('Not fetched. The email size exceeds the limit.');
|
||||
$email->setIsHtml(false);
|
||||
}
|
||||
|
||||
$this->processInReplyTo($parser, $message, $email);
|
||||
|
||||
$parentFound = $this->parentFinder->find($email, $message);
|
||||
|
||||
if ($parentFound) {
|
||||
$email->setParent($parentFound);
|
||||
}
|
||||
|
||||
if (!$duplicate) {
|
||||
$this->entityManager->getLocker()->lockExclusive(Email::ENTITY_TYPE);
|
||||
|
||||
$duplicate = $this->findDuplicate($email, $message);
|
||||
|
||||
if ($duplicate) {
|
||||
$this->entityManager->getLocker()->rollback();
|
||||
|
||||
if ($duplicate->getStatus() !== Email::STATUS_BEING_IMPORTED) {
|
||||
$this->entityManager->refreshEntity($duplicate);
|
||||
|
||||
$this->processDuplicate($duplicate, $data, $email->getGroupFolder()?->getId());
|
||||
|
||||
return $duplicate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($duplicate) {
|
||||
$this->copyAttributesToDuplicate($email, $duplicate);
|
||||
$this->getEmailRepository()->fillAccount($duplicate);
|
||||
|
||||
$this->processDuplicate($duplicate, $data, $email->getGroupFolder()?->getId());
|
||||
|
||||
return $duplicate;
|
||||
}
|
||||
|
||||
if (!$email->getMessageId()) {
|
||||
$email->setDummyMessageId();
|
||||
}
|
||||
|
||||
$email->setStatus(Email::STATUS_BEING_IMPORTED);
|
||||
|
||||
$this->entityManager->saveEntity($email, [
|
||||
SaveOption::SKIP_ALL => true,
|
||||
SaveOption::KEEP_NEW => true,
|
||||
]);
|
||||
|
||||
$this->entityManager->getLocker()->commit();
|
||||
|
||||
if ($parentFound) {
|
||||
$this->processEmailWithParent($email);
|
||||
}
|
||||
|
||||
$email->setStatus(Email::STATUS_ARCHIVED);
|
||||
|
||||
$this->processFinalTransactionalSave($email);
|
||||
$this->processAttachmentSave($inlineAttachmentList, $email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
private function copyAttributesToDuplicate(Email $email, Email $duplicate): void
|
||||
{
|
||||
$duplicate->set([
|
||||
'from' => $email->get('from'),
|
||||
'to' => $email->get('to'),
|
||||
'cc' => $email->get('cc'),
|
||||
'bcc' => $email->get('bcc'),
|
||||
'replyTo' => $email->get('replyTo'),
|
||||
'name' => $email->get(Field::NAME),
|
||||
'dateSent' => $email->get('dateSent'),
|
||||
'body' => $email->get('body'),
|
||||
'bodyPlain' => $email->get('bodyPlain'),
|
||||
'parentType' => $email->get('parentType'),
|
||||
'parentId' => $email->get('parentId'),
|
||||
'isHtml' => $email->get('isHtml'),
|
||||
'messageId' => $email->get('messageId'),
|
||||
'fromString' => $email->get('fromString'),
|
||||
'replyToString' => $email->get('replyToString'),
|
||||
]);
|
||||
}
|
||||
|
||||
private function processEmailWithParent(Email $email): void
|
||||
{
|
||||
$parentType = $email->get(Field::PARENT . 'Type');
|
||||
$parentId = $email->get(Field::PARENT . 'Id');
|
||||
|
||||
if (!$parentId || !$parentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
$emailKeepParentTeamsEntityList = $this->config->get('emailKeepParentTeamsEntityList') ?? [];
|
||||
|
||||
if (
|
||||
!in_array($parentType, $emailKeepParentTeamsEntityList) ||
|
||||
!$this->entityManager->hasRepository($parentType)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$parent = $email->getParent();
|
||||
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$parent instanceof CoreEntity) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($parent->getLinkMultipleIdList(Field::TEAMS) as $parentTeamId) {
|
||||
$email->addTeamId($parentTeamId);
|
||||
}
|
||||
}
|
||||
|
||||
private function findDuplicate(Email $email, Message $message): ?Email
|
||||
{
|
||||
return $this->duplicateFinder->find($email, $message);
|
||||
}
|
||||
|
||||
private function processDuplicate(Email $email, Data $data, ?string $groupFolderId): void
|
||||
{
|
||||
$assignedUserId = $data->getAssignedUserId();
|
||||
|
||||
if ($email->getStatus() === Email::STATUS_ARCHIVED) {
|
||||
$this->getEmailRepository()->loadFromField($email);
|
||||
$this->getEmailRepository()->loadToField($email);
|
||||
}
|
||||
|
||||
$fetchedTeamIds = $email->getTeams()->getIdList();
|
||||
$fetchedUserIds = $email->getUsers()->getIdList();
|
||||
$fetchedAssignedUserIds = $email->getAssignedUsers()->getIdList();
|
||||
|
||||
$email->setLinkMultipleIdList('users', []);
|
||||
$email->setLinkMultipleIdList(Field::TEAMS, []);
|
||||
$email->setLinkMultipleIdList(Field::ASSIGNED_USERS, []);
|
||||
|
||||
$processNoteAcl = false;
|
||||
|
||||
if ($assignedUserId) {
|
||||
if (!in_array($assignedUserId, $fetchedUserIds)) {
|
||||
$processNoteAcl = true;
|
||||
|
||||
$email->addUserId($assignedUserId);
|
||||
}
|
||||
|
||||
if (!in_array($assignedUserId, $fetchedAssignedUserIds)) {
|
||||
$email->addAssignedUserId($assignedUserId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data->getUserIdList() as $uId) {
|
||||
if (!in_array($uId, $fetchedUserIds)) {
|
||||
$processNoteAcl = true;
|
||||
|
||||
$email->addUserId($uId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data->getFolderData() as $uId => $folderId) {
|
||||
if (!in_array($uId, $fetchedUserIds)) {
|
||||
$email->setUserColumnFolderId($uId, $folderId);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Can cause skip-notification bypass. @todo Revise.
|
||||
$this->entityManager
|
||||
->getRelation($email, 'users')
|
||||
->updateColumnsById($uId, [Email::USERS_COLUMN_FOLDER_ID => $folderId]);
|
||||
}
|
||||
|
||||
$email->set('isBeingImported', true);
|
||||
|
||||
$this->getEmailRepository()->applyUsersFilters($email);
|
||||
|
||||
if ($groupFolderId && !$email->getGroupFolder()) {
|
||||
$this->relateWithGroupFolder($email, $groupFolderId);
|
||||
|
||||
$addedFromFolder = $this->applyGroupFolder(
|
||||
$email,
|
||||
$groupFolderId,
|
||||
$fetchedUserIds,
|
||||
$fetchedTeamIds
|
||||
);
|
||||
|
||||
if ($addedFromFolder) {
|
||||
$processNoteAcl = true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($data->getTeamIdList() as $teamId) {
|
||||
if (!in_array($teamId, $fetchedTeamIds)) {
|
||||
$processNoteAcl = true;
|
||||
|
||||
$email->addTeamId($teamId);
|
||||
}
|
||||
}
|
||||
|
||||
$saverParams = SaverParams::create()->withRawOptions([
|
||||
'skipLinkMultipleRemove' => true,
|
||||
'skipLinkMultipleUpdate' => true,
|
||||
]);
|
||||
|
||||
$this->linkMultipleSaver->process($email, 'users', $saverParams);
|
||||
$this->linkMultipleSaver->process($email, Field::ASSIGNED_USERS, $saverParams);
|
||||
$this->linkMultipleSaver->process($email, Field::TEAMS, $saverParams);
|
||||
|
||||
if ($this->notificationsEnabled()) {
|
||||
$notificatorParams = AssignmentNotificatorParams::create()
|
||||
->withRawOptions([Email::SAVE_OPTION_IS_BEING_IMPORTED => true]);
|
||||
|
||||
$this->notificator->process($email, $notificatorParams);
|
||||
}
|
||||
|
||||
$email->set('isBeingImported', false);
|
||||
$email->clear('teamsIds');
|
||||
$email->clear('usersIds');
|
||||
$email->clear('assignedUsersIds');
|
||||
|
||||
$email->setAsFetched();
|
||||
|
||||
if ($email->getParentType() && $processNoteAcl) {
|
||||
$this->scheduleAclJob($email);
|
||||
}
|
||||
}
|
||||
|
||||
private function notificationsEnabled(): bool
|
||||
{
|
||||
return in_array(
|
||||
Email::ENTITY_TYPE,
|
||||
$this->config->get('assignmentNotificationsEntityList') ?? []
|
||||
);
|
||||
}
|
||||
|
||||
private function getSubject(Parser $parser, Message $message): string
|
||||
{
|
||||
$subject = '';
|
||||
|
||||
if ($parser->hasHeader($message, 'subject')) {
|
||||
$subject = $parser->getHeader($message, 'subject');
|
||||
}
|
||||
|
||||
if (!empty($subject)) {
|
||||
$subject = trim($subject);
|
||||
}
|
||||
|
||||
if ($subject !== '0' && empty($subject)) {
|
||||
$subject = '(No Subject)';
|
||||
}
|
||||
|
||||
if (strlen($subject) > self::SUBJECT_MAX_LENGTH) {
|
||||
$subject = substr($subject, 0, self::SUBJECT_MAX_LENGTH);
|
||||
}
|
||||
|
||||
return $subject;
|
||||
}
|
||||
|
||||
private function setFromStrings(Parser $parser, Message $message, Email $email): void
|
||||
{
|
||||
$fromAddressData = $parser->getAddressData($message, 'from');
|
||||
|
||||
if ($fromAddressData) {
|
||||
$namePart = ($fromAddressData->name ? ($fromAddressData->name . ' ') : '');
|
||||
|
||||
$email->set('fromString', "$namePart<$fromAddressData->address>");
|
||||
}
|
||||
|
||||
$replyToData = $parser->getAddressData($message, 'reply-To');
|
||||
|
||||
if ($replyToData) {
|
||||
$namePart = ($replyToData->name ? ($replyToData->name . ' ') : '');
|
||||
|
||||
$email->set('replyToString', "$namePart<$replyToData->address>");
|
||||
}
|
||||
}
|
||||
|
||||
private function setAddresses(Parser $parser, Message $message, Email $email): void
|
||||
{
|
||||
$from = $parser->getAddressList($message, 'from');
|
||||
$to = $parser->getAddressList($message, 'to');
|
||||
$cc = $parser->getAddressList($message, 'cc');
|
||||
$replyTo = $parser->getAddressList($message, 'reply-To');
|
||||
|
||||
$email->setFromAddress($from[0] ?? null);
|
||||
$email->setToAddressList($to);
|
||||
$email->setCcAddressList($cc);
|
||||
$email->setReplyToAddressList($replyTo);
|
||||
|
||||
$email->set('addressNameMap', $parser->getAddressNameMap($message));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool True if an email is system.
|
||||
*/
|
||||
private function processMessageId(Parser $parser, Message $message, Email $email): bool
|
||||
{
|
||||
if (!$parser->hasHeader($message, 'message-Id')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$messageId = $parser->getMessageId($message);
|
||||
|
||||
if (!$messageId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$email->setMessageId($messageId);
|
||||
|
||||
if ($parser->hasHeader($message, 'delivered-To')) {
|
||||
$deliveredTo = $parser->getHeader($message, 'delivered-To') ?? '';
|
||||
|
||||
$email->set('messageIdInternal', "$messageId-$deliveredTo");
|
||||
}
|
||||
|
||||
if (stripos($messageId, '@espo-system') !== false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processDate(Parser $parser, Message $message, Email $email): void
|
||||
{
|
||||
if (!$parser->hasHeader($message, 'date')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateString = $parser->getHeader($message, 'date') ?? 'now';
|
||||
|
||||
try {
|
||||
$dateSent = DateTimeField::fromDateTime(new DateTime($dateString));
|
||||
} catch (Exception) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email->setDateSent($dateSent);
|
||||
}
|
||||
|
||||
private function processDeliveryDate(Parser $parser, Message $message, Email $email): void
|
||||
{
|
||||
if (!$parser->hasHeader($message, 'delivery-Date')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$dateString = $parser->getHeader($message, 'delivery-Date') ?? 'now';
|
||||
|
||||
try {
|
||||
$deliveryDate = DateTimeField::fromDateTime(new DateTime($dateString));
|
||||
} catch (Exception) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email->setDeliveryDate($deliveryDate);
|
||||
}
|
||||
|
||||
private function processInReplyTo(Parser $parser, Message $message, Email $email): void
|
||||
{
|
||||
if (!$parser->hasHeader($message, 'in-Reply-To')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$stringValue = $parser->getHeader($message, 'in-Reply-To');
|
||||
|
||||
if (!$stringValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
$values = explode(' ', $stringValue);
|
||||
|
||||
$inReplyTo = $values[0] ?? null;
|
||||
|
||||
if (!$inReplyTo) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($inReplyTo[0] !== '<') {
|
||||
$inReplyTo = "<$inReplyTo>";
|
||||
}
|
||||
|
||||
$replied = $this->entityManager
|
||||
->getRDBRepositoryByClass(Email::class)
|
||||
->where(['messageId' => $inReplyTo])
|
||||
->findOne();
|
||||
|
||||
if (!$replied) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email->setReplied($replied);
|
||||
|
||||
foreach ($replied->getTeams()->getIdList() as $teamId) {
|
||||
$email->addTeamId($teamId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable<EmailFilter> $filterList
|
||||
* @return bool True if to skip.
|
||||
*/
|
||||
private function processFilters(Email $email, iterable $filterList, bool $skipBody = false): bool
|
||||
{
|
||||
$matchedFilter = $this->filtersMatcher->findMatch($email, $filterList, $skipBody);
|
||||
|
||||
if (!$matchedFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($matchedFilter->getAction() === EmailFilter::ACTION_SKIP) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
$matchedFilter->getAction() === EmailFilter::ACTION_MOVE_TO_GROUP_FOLDER &&
|
||||
$matchedFilter->getGroupEmailFolderId()
|
||||
) {
|
||||
$this->applyGroupFolder($email, $matchedFilter->getGroupEmailFolderId());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function processFinalTransactionalSave(Email $email): void
|
||||
{
|
||||
$this->entityManager->getTransactionManager()->start();
|
||||
|
||||
$this->entityManager
|
||||
->getRDBRepositoryByClass(Email::class)
|
||||
->forUpdate()
|
||||
->where([Attribute::ID => $email->getId()])
|
||||
->findOne();
|
||||
|
||||
$this->entityManager->saveEntity($email, [Email::SAVE_OPTION_IS_BEING_IMPORTED => true]);
|
||||
|
||||
$this->entityManager->getTransactionManager()->commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Attachment[] $inlineAttachmentList
|
||||
*/
|
||||
private function processAttachmentSave(array $inlineAttachmentList, Email $email): void
|
||||
{
|
||||
foreach ($inlineAttachmentList as $attachment) {
|
||||
$attachment->setTargetField('body');
|
||||
$attachment->setRelated(LinkParent::createFromEntity($email));
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
}
|
||||
}
|
||||
|
||||
private function getParser(Message $message): Parser
|
||||
{
|
||||
return $message instanceof MessageWrapper ?
|
||||
($message->getParser() ?? $this->parserFactory->create()) :
|
||||
$this->parserFactory->create();
|
||||
}
|
||||
|
||||
private function getEmailRepository(): EmailRepository
|
||||
{
|
||||
/** @var EmailRepository */
|
||||
return $this->entityManager->getRDBRepositoryByClass(Email::class);
|
||||
}
|
||||
|
||||
private function relateWithGroupFolder(Email $email, string $groupFolderId): void
|
||||
{
|
||||
$this->entityManager
|
||||
->getRelation($email, 'groupFolder')
|
||||
->relateById($groupFolderId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $fetchedUserIds
|
||||
* @param string[] $fetchedTeamIds
|
||||
*/
|
||||
private function applyGroupFolder(
|
||||
Email $email,
|
||||
string $groupFolderId,
|
||||
array $fetchedUserIds = [],
|
||||
array $fetchedTeamIds = [],
|
||||
): bool {
|
||||
|
||||
$email->setGroupFolderId($groupFolderId);
|
||||
|
||||
$groupFolder = $this->entityManager
|
||||
->getRDBRepositoryByClass(GroupEmailFolder::class)
|
||||
->getById($groupFolderId);
|
||||
|
||||
if (!$groupFolder || !$groupFolder->getTeams()->getCount()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$added = false;
|
||||
|
||||
foreach ($groupFolder->getTeams()->getIdList() as $teamId) {
|
||||
if (!in_array($teamId, $fetchedTeamIds)) {
|
||||
$added = true;
|
||||
|
||||
$email->addTeamId($teamId);
|
||||
}
|
||||
}
|
||||
|
||||
$users = $this->entityManager
|
||||
->getRDBRepositoryByClass(User::class)
|
||||
->select([Attribute::ID])
|
||||
->where([
|
||||
'type' => [User::TYPE_REGULAR, User::TYPE_ADMIN],
|
||||
'isActive' => true,
|
||||
Attribute::ID . '!=' => $fetchedUserIds,
|
||||
])
|
||||
->where(
|
||||
Condition::in(
|
||||
Expression::column(Attribute::ID),
|
||||
SelectBuilder::create()
|
||||
->from(Team::RELATIONSHIP_TEAM_USER)
|
||||
->select('userId')
|
||||
->where(['teamId' => $groupFolder->getTeams()->getIdList()])
|
||||
->build()
|
||||
)
|
||||
)
|
||||
->find();
|
||||
|
||||
foreach ($users as $user) {
|
||||
$added = true;
|
||||
|
||||
$email->addUserId($user->getId());
|
||||
}
|
||||
|
||||
return $added;
|
||||
}
|
||||
|
||||
private function scheduleAclJob(Email $email): void
|
||||
{
|
||||
// Need to update acl fields (users and teams)
|
||||
// of notes related to the duplicate email.
|
||||
// To grant access to the user who received the email.
|
||||
|
||||
$dt = new DateTime();
|
||||
$dt->modify('+' . self::PROCESS_ACL_DELAY_PERIOD);
|
||||
|
||||
$this->jobSchedulerFactory
|
||||
->create()
|
||||
->setClassName(ProcessNoteAcl::class)
|
||||
->setData(
|
||||
JobData::create(['notify' => true])
|
||||
->withTargetId($email->getId())
|
||||
->withTargetType(Email::ENTITY_TYPE)
|
||||
)
|
||||
->setQueue(QueueName::Q1)
|
||||
->setTime($dt)
|
||||
->schedule();
|
||||
}
|
||||
}
|
||||
271
application/Espo/Core/Mail/Importer/DefaultParentFinder.php
Normal file
271
application/Espo/Core/Mail/Importer/DefaultParentFinder.php
Normal file
@@ -0,0 +1,271 @@
|
||||
<?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\Importer;
|
||||
|
||||
use Espo\Core\Mail\Message;
|
||||
use Espo\Core\Name\Field;
|
||||
use Espo\Core\Templates\Entities\Company;
|
||||
use Espo\Core\Templates\Entities\Person;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Metadata;
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\EmailAddress;
|
||||
use Espo\Modules\Crm\Entities\Account;
|
||||
use Espo\Modules\Crm\Entities\Contact;
|
||||
use Espo\Modules\Crm\Entities\Lead;
|
||||
use Espo\ORM\Entity;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Repositories\EmailAddress as EmailAddressRepository;
|
||||
|
||||
class DefaultParentFinder implements ParentFinder
|
||||
{
|
||||
/** @var string[] */
|
||||
private array $entityTypeList;
|
||||
|
||||
public function __construct(
|
||||
private EntityManager $entityManager,
|
||||
private Config $config,
|
||||
private Metadata $metadata
|
||||
) {
|
||||
$this->entityTypeList = $this->entityManager
|
||||
->getDefs()
|
||||
->getEntity(Email::ENTITY_TYPE)
|
||||
->getField(Field::PARENT)
|
||||
->getParam('entityList') ?? [];
|
||||
}
|
||||
|
||||
public function find(Email $email, Message $message): ?Entity
|
||||
{
|
||||
return
|
||||
$this->getByReferences($message) ??
|
||||
$this->getFromReplied($email) ??
|
||||
$this->getByFromAddress($email) ??
|
||||
$this->getByReplyToAddress($email) ??
|
||||
$this->getByToAddress($email);
|
||||
}
|
||||
|
||||
private function isEntityTypeAllowed(string $entityType): bool
|
||||
{
|
||||
return in_array($entityType, $this->entityTypeList);
|
||||
}
|
||||
|
||||
private function getByFromAddress(Email $email): ?Entity
|
||||
{
|
||||
$from = $email->getFromAddress();
|
||||
|
||||
if (!$from) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getByAddress($from);
|
||||
}
|
||||
|
||||
private function getByReplyToAddress(Email $email): ?Entity
|
||||
{
|
||||
$list = $email->getReplyToAddressList();
|
||||
|
||||
if ($list === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getByAddress($list[0]);
|
||||
}
|
||||
|
||||
private function getByToAddress(Email $email): ?Entity
|
||||
{
|
||||
$list = $email->getToAddressList();
|
||||
|
||||
if ($list === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getByAddress($list[0]);
|
||||
}
|
||||
|
||||
private function getByAddress(string $emailAddress): ?Entity
|
||||
{
|
||||
$contact = $this->entityManager
|
||||
->getRDBRepositoryByClass(Contact::class)
|
||||
->where(['emailAddress' => $emailAddress])
|
||||
->findOne();
|
||||
|
||||
if ($contact) {
|
||||
if (
|
||||
!$this->config->get('b2cMode') &&
|
||||
$this->isEntityTypeAllowed(Account::ENTITY_TYPE) &&
|
||||
$contact->getAccount()
|
||||
) {
|
||||
return $contact->getAccount();
|
||||
}
|
||||
|
||||
if ($this->isEntityTypeAllowed(Contact::ENTITY_TYPE)) {
|
||||
return $contact;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isEntityTypeAllowed(Account::ENTITY_TYPE)) {
|
||||
$account = $this->entityManager
|
||||
->getRDBRepositoryByClass(Account::class)
|
||||
->where(['emailAddress' => $emailAddress])
|
||||
->findOne();
|
||||
|
||||
if ($account) {
|
||||
return $account;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isEntityTypeAllowed(Lead::ENTITY_TYPE)) {
|
||||
$lead = $this->entityManager
|
||||
->getRDBRepositoryByClass(Lead::class)
|
||||
->where(['emailAddress' => $emailAddress])
|
||||
->findOne();
|
||||
|
||||
if ($lead) {
|
||||
return $lead;
|
||||
}
|
||||
}
|
||||
|
||||
$entityTypeList = array_filter(
|
||||
$this->entityTypeList,
|
||||
function ($entityType) {
|
||||
return
|
||||
!in_array(
|
||||
$entityType,
|
||||
[Account::ENTITY_TYPE, Contact::ENTITY_TYPE, Lead::ENTITY_TYPE]
|
||||
) &&
|
||||
in_array(
|
||||
$this->metadata->get(['scopes', $entityType, 'type']),
|
||||
[Company::TEMPLATE_TYPE, Person::TEMPLATE_TYPE]
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/** @var EmailAddressRepository $emailAddressRepository */
|
||||
$emailAddressRepository = $this->entityManager->getRepository(EmailAddress::ENTITY_TYPE);
|
||||
|
||||
foreach ($entityTypeList as $entityType) {
|
||||
$entity = $emailAddressRepository->getEntityByAddress($emailAddress, $entityType);
|
||||
|
||||
if ($entity) {
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getFromReplied(Email $email): ?Entity
|
||||
{
|
||||
$replied = $email->getReplied();
|
||||
|
||||
return $replied?->getParent();
|
||||
}
|
||||
|
||||
private function getByReferences(Message $message): ?Entity
|
||||
{
|
||||
$references = $message->getHeader('References');
|
||||
|
||||
if (!$references) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$delimiter = strpos($references, '>,') ? ',' : ' ';
|
||||
|
||||
foreach (explode($delimiter, $references) as $reference) {
|
||||
$reference = str_replace(['/', '@'], ' ', trim(trim($reference), '<>'));
|
||||
|
||||
$parent = $this->getByReferencesItem($reference);
|
||||
|
||||
if ($parent) {
|
||||
return $parent;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getByReferencesItem(string $reference): ?Entity
|
||||
{
|
||||
$parentType = null;
|
||||
$parentId = null;
|
||||
$number = null;
|
||||
$emailSent = PHP_INT_MAX;
|
||||
|
||||
$n = sscanf($reference, '%s %s %d %d espo', $parentType, $parentId, $emailSent, $number);
|
||||
|
||||
if ($n !== 4) {
|
||||
$n = sscanf($reference, '%s %s %d %d espo-system', $parentType, $parentId, $emailSent, $number);
|
||||
}
|
||||
|
||||
if ($n !== 4 || $emailSent >= time()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$parentType || !$parentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!is_string($parentType) || !is_string($parentId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->entityManager->hasRepository($parentType)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $this->entityManager->getEntityById($parentType, $parentId);
|
||||
|
||||
if ($parent instanceof Lead) {
|
||||
return $this->getFromLead($parent) ?? $parent;
|
||||
}
|
||||
|
||||
return $parent;
|
||||
}
|
||||
|
||||
private function getFromLead(Lead $lead): ?Entity
|
||||
{
|
||||
if ($lead->getStatus() !== Lead::STATUS_CONVERTED) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($lead->getCreatedAccount()) {
|
||||
return $lead->getCreatedAccount();
|
||||
}
|
||||
|
||||
if (
|
||||
$this->config->get('b2cMode') &&
|
||||
$lead->getCreatedContact()
|
||||
) {
|
||||
return $lead->getCreatedContact();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
41
application/Espo/Core/Mail/Importer/DuplicateFinder.php
Normal file
41
application/Espo/Core/Mail/Importer/DuplicateFinder.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\Importer;
|
||||
|
||||
use Espo\Core\Mail\Message;
|
||||
use Espo\Entities\Email;
|
||||
|
||||
/**
|
||||
* Finds an existing duplicate of an email being imported.
|
||||
*/
|
||||
interface DuplicateFinder
|
||||
{
|
||||
public function find(Email $email, Message $message): ?Email;
|
||||
}
|
||||
42
application/Espo/Core/Mail/Importer/ParentFinder.php
Normal file
42
application/Espo/Core/Mail/Importer/ParentFinder.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?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\Importer;
|
||||
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Core\Mail\Message;
|
||||
use Espo\ORM\Entity;
|
||||
|
||||
/**
|
||||
* Finds a parent record for an email being imported.
|
||||
*/
|
||||
interface ParentFinder
|
||||
{
|
||||
public function find(Email $email, Message $message): ?Entity;
|
||||
}
|
||||
33
application/Espo/Core/Mail/Mail/Headers.php
Normal file
33
application/Espo/Core/Mail/Mail/Headers.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?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;
|
||||
|
||||
class Headers extends \Laminas\Mail\Headers
|
||||
{}
|
||||
81
application/Espo/Core/Mail/Mail/Storage/Imap.php
Normal file
81
application/Espo/Core/Mail/Mail/Storage/Imap.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?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,
|
||||
];
|
||||
}
|
||||
}
|
||||
77
application/Espo/Core/Mail/Message.php
Normal file
77
application/Espo/Core/Mail/Message.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Mail\Message\Part;
|
||||
|
||||
interface Message
|
||||
{
|
||||
/**
|
||||
* Whether has a specific header.
|
||||
*/
|
||||
public function hasHeader(string $name): bool;
|
||||
|
||||
/**
|
||||
* Get a specific header.
|
||||
*/
|
||||
public function getHeader(string $attribute): ?string;
|
||||
|
||||
/**
|
||||
* Get a raw header part.
|
||||
*/
|
||||
public function getRawHeader(): string;
|
||||
|
||||
/**
|
||||
* Get a raw content part.
|
||||
*/
|
||||
public function getRawContent(): string;
|
||||
|
||||
/**
|
||||
* Get a full raw message.
|
||||
*/
|
||||
public function getFullRawContent(): string;
|
||||
|
||||
/**
|
||||
* Get flags.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getFlags(): array;
|
||||
|
||||
/**
|
||||
* Whether contents is fetched.
|
||||
*/
|
||||
public function isFetched(): bool;
|
||||
|
||||
/**
|
||||
* @return Part[]
|
||||
*/
|
||||
public function getPartList(): array;
|
||||
}
|
||||
79
application/Espo/Core/Mail/Message/MailMimeParser/Part.php
Normal file
79
application/Espo/Core/Mail/Message/MailMimeParser/Part.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?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\Message\MailMimeParser;
|
||||
|
||||
use Espo\Core\Mail\Message\Part as PartInterface;
|
||||
|
||||
use ZBateson\MailMimeParser\Message\IMessagePart;
|
||||
|
||||
class Part implements PartInterface
|
||||
{
|
||||
private IMessagePart $part;
|
||||
|
||||
public function __construct(IMessagePart $part)
|
||||
{
|
||||
$this->part = $part;
|
||||
}
|
||||
|
||||
public function getContentType(): ?string
|
||||
{
|
||||
return $this->part->getContentType();
|
||||
}
|
||||
|
||||
public function hasContent(): bool
|
||||
{
|
||||
return $this->part->hasContent();
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->part->getContent();
|
||||
}
|
||||
|
||||
public function getContentId(): ?string
|
||||
{
|
||||
return $this->part->getContentId();
|
||||
}
|
||||
|
||||
public function getCharset(): ?string
|
||||
{
|
||||
return $this->part->getCharset();
|
||||
}
|
||||
|
||||
public function getContentDisposition(): ?string
|
||||
{
|
||||
return $this->part->getContentDisposition();
|
||||
}
|
||||
|
||||
public function getFilename(): ?string
|
||||
{
|
||||
return $this->part->getFilename();
|
||||
}
|
||||
}
|
||||
47
application/Espo/Core/Mail/Message/Part.php
Normal file
47
application/Espo/Core/Mail/Message/Part.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?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\Message;
|
||||
|
||||
interface Part
|
||||
{
|
||||
public function getContentType(): ?string;
|
||||
|
||||
public function hasContent(): bool;
|
||||
|
||||
public function getContent(): ?string;
|
||||
|
||||
public function getContentId(): ?string;
|
||||
|
||||
public function getCharset(): ?string;
|
||||
|
||||
public function getContentDisposition(): ?string;
|
||||
|
||||
public function getFilename(): ?string;
|
||||
}
|
||||
150
application/Espo/Core/Mail/MessageWrapper.php
Normal file
150
application/Espo/Core/Mail/MessageWrapper.php
Normal file
@@ -0,0 +1,150 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Mail\Account\Storage;
|
||||
use Espo\Core\Mail\Message\Part;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class MessageWrapper implements Message
|
||||
{
|
||||
private ?string $rawHeader = null;
|
||||
private ?string $rawContent = null;
|
||||
|
||||
/** @var ?string[] */
|
||||
private ?array $flagList = null;
|
||||
|
||||
public function __construct(
|
||||
private int $id,
|
||||
private ?Storage $storage = null,
|
||||
private ?Parser $parser = null,
|
||||
private ?string $fullRawContent = null
|
||||
) {
|
||||
if ($storage) {
|
||||
$data = $storage->getHeaderAndFlags($id);
|
||||
|
||||
$this->rawHeader = $data['header'];
|
||||
$this->flagList = $data['flags'];
|
||||
}
|
||||
|
||||
if (
|
||||
!$storage &&
|
||||
$this->fullRawContent
|
||||
) {
|
||||
$rawHeader = null;
|
||||
$rawBody = null;
|
||||
|
||||
if (str_contains($this->fullRawContent, "\r\n\r\n")) {
|
||||
[$rawHeader, $rawBody] = explode("\r\n\r\n", $this->fullRawContent, 2);
|
||||
} else if (str_contains($this->fullRawContent, "\n\n")) {
|
||||
[$rawHeader, $rawBody] = explode("\n\n", $this->fullRawContent, 2);
|
||||
}
|
||||
|
||||
$this->rawHeader = $rawHeader;
|
||||
$this->rawContent = $rawBody;
|
||||
}
|
||||
}
|
||||
|
||||
public function getRawHeader(): string
|
||||
{
|
||||
return $this->rawHeader ?? '';
|
||||
}
|
||||
|
||||
public function getParser(): ?Parser
|
||||
{
|
||||
return $this->parser;
|
||||
}
|
||||
|
||||
public function hasHeader(string $name): bool
|
||||
{
|
||||
if (!$this->parser) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return $this->parser->hasHeader($this, $name);
|
||||
}
|
||||
|
||||
public function getHeader(string $attribute): ?string
|
||||
{
|
||||
if (!$this->parser) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return $this->parser->getHeader($this, $attribute);
|
||||
}
|
||||
|
||||
public function getRawContent(): string
|
||||
{
|
||||
if (is_null($this->rawContent)) {
|
||||
if (!$this->storage) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$this->rawContent = $this->storage->getRawContent($this->id);
|
||||
}
|
||||
|
||||
return $this->rawContent ?? '';
|
||||
}
|
||||
|
||||
public function getFullRawContent(): string
|
||||
{
|
||||
if ($this->fullRawContent) {
|
||||
return $this->fullRawContent;
|
||||
}
|
||||
|
||||
return $this->getRawHeader() . "\n" . $this->getRawContent();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getFlags(): array
|
||||
{
|
||||
return $this->flagList ?? [];
|
||||
}
|
||||
|
||||
public function isFetched(): bool
|
||||
{
|
||||
return (bool) $this->rawHeader;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Part[]
|
||||
*/
|
||||
public function getPartList(): array
|
||||
{
|
||||
if (!$this->parser) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
return $this->parser->getPartList($this);
|
||||
}
|
||||
}
|
||||
67
application/Espo/Core/Mail/Parser.php
Normal file
67
application/Espo/Core/Mail/Parser.php
Normal file
@@ -0,0 +1,67 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\Core\Mail\Message\Part;
|
||||
|
||||
use stdClass;
|
||||
|
||||
interface Parser
|
||||
{
|
||||
public function hasHeader(Message $message, string $name): bool;
|
||||
|
||||
public function getHeader(Message $message, string $name): ?string;
|
||||
|
||||
public function getMessageId(Message $message): ?string;
|
||||
|
||||
public function getAddressNameMap(Message $message): stdClass;
|
||||
|
||||
/**
|
||||
* @return ?object{address: string, name: string}
|
||||
*/
|
||||
public function getAddressData(Message $message, string $type): ?object;
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAddressList(Message $message, string $type): array;
|
||||
|
||||
/**
|
||||
* @return Attachment[] A list of inline attachments.
|
||||
*/
|
||||
public function getInlineAttachmentList(Message $message, Email $email): array;
|
||||
|
||||
/**
|
||||
* @return Part[]
|
||||
*/
|
||||
public function getPartList(Message $message): array;
|
||||
}
|
||||
46
application/Espo/Core/Mail/ParserFactory.php
Normal file
46
application/Espo/Core/Mail/ParserFactory.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Mail\Parsers\MailMimeParser;
|
||||
|
||||
class ParserFactory
|
||||
{
|
||||
protected const DEFAULT_PARSER_CLASS_NAME = MailMimeParser::class;
|
||||
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
public function create(): Parser
|
||||
{
|
||||
return $this->injectableFactory->create(self::DEFAULT_PARSER_CLASS_NAME);
|
||||
}
|
||||
}
|
||||
464
application/Espo/Core/Mail/Parsers/MailMimeParser.php
Normal file
464
application/Espo/Core/Mail/Parsers/MailMimeParser.php
Normal file
@@ -0,0 +1,464 @@
|
||||
<?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\Parsers;
|
||||
|
||||
use Espo\Entities\Email;
|
||||
use Espo\Entities\Attachment;
|
||||
use Espo\ORM\EntityManager;
|
||||
use Espo\Core\Mail\Message;
|
||||
use Espo\Core\Mail\Parser;
|
||||
use Espo\Core\Mail\Message\Part;
|
||||
use Espo\Core\Mail\Message\MailMimeParser\Part as WrapperPart;
|
||||
|
||||
use ZBateson\MailMimeParser\Header\AddressHeader;
|
||||
use ZBateson\MailMimeParser\Header\HeaderConsts;
|
||||
use ZBateson\MailMimeParser\IMessage;
|
||||
use ZBateson\MailMimeParser\MailMimeParser as WrappeeParser;
|
||||
use ZBateson\MailMimeParser\Message\MessagePart;
|
||||
use ZBateson\MailMimeParser\Message\MimePart;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* An adapter for MailMimeParser library.
|
||||
*/
|
||||
class MailMimeParser implements Parser
|
||||
{
|
||||
/** @var array<string, string> */
|
||||
private array $extMimeTypeMap = [
|
||||
'jpg' => 'image/jpg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
];
|
||||
|
||||
private ?WrappeeParser $parser = null;
|
||||
|
||||
private const FIELD_BODY = 'body';
|
||||
private const FIELD_ATTACHMENTS = 'attachments';
|
||||
|
||||
private const DISPOSITION_INLINE = 'inline';
|
||||
|
||||
public const TYPE_MESSAGE_RFC822 = 'message/rfc822';
|
||||
public const TYPE_OCTET_STREAM = 'application/octet-stream';
|
||||
|
||||
/** @var array<string, IMessage> */
|
||||
private array $messageHash = [];
|
||||
|
||||
public function __construct(private EntityManager $entityManager)
|
||||
{}
|
||||
|
||||
private function getParser(): WrappeeParser
|
||||
{
|
||||
if (!$this->parser) {
|
||||
$this->parser = new WrappeeParser();
|
||||
}
|
||||
|
||||
return $this->parser;
|
||||
}
|
||||
|
||||
private function loadContent(Message $message): void
|
||||
{
|
||||
$raw = $message->getFullRawContent();
|
||||
|
||||
$key = spl_object_hash($message);
|
||||
|
||||
$this->messageHash[$key] = $this->getParser()->parse($raw, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IMessage
|
||||
*/
|
||||
private function getMessage(Message $message)
|
||||
{
|
||||
$key = spl_object_hash($message);
|
||||
|
||||
if (!array_key_exists($key, $this->messageHash)) {
|
||||
$raw = $message->getRawHeader();
|
||||
|
||||
if (!$raw) {
|
||||
$raw = $message->getFullRawContent();
|
||||
}
|
||||
|
||||
$this->messageHash[$key] = $this->getParser()->parse($raw, false);
|
||||
}
|
||||
|
||||
return $this->messageHash[$key];
|
||||
}
|
||||
|
||||
public function hasHeader(Message $message, string $name): bool
|
||||
{
|
||||
return $this->getMessage($message)->getHeaderValue($name) !== null;
|
||||
}
|
||||
|
||||
public function getHeader(Message $message, string $name): ?string
|
||||
{
|
||||
if (!$this->hasHeader($message, $name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->getMessage($message)->getHeaderValue($name);
|
||||
}
|
||||
|
||||
public function getMessageId(Message $message): ?string
|
||||
{
|
||||
$messageId = $this->getHeader($message, 'Message-ID');
|
||||
|
||||
if (!$messageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($messageId[0] !== '<') {
|
||||
$messageId = '<' . $messageId . '>';
|
||||
}
|
||||
|
||||
return $messageId;
|
||||
}
|
||||
|
||||
public function getAddressNameMap(Message $message): stdClass
|
||||
{
|
||||
$map = (object) [];
|
||||
|
||||
foreach (['from', 'to', 'cc', 'reply-To'] as $type) {
|
||||
$header = $this->getMessage($message)->getHeader($type);
|
||||
|
||||
if (!$header || !method_exists($header, 'getAddresses')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var AddressHeader $header */
|
||||
|
||||
$list = $header->getAddresses();
|
||||
|
||||
foreach ($list as $item) {
|
||||
$address = $item->getEmail();
|
||||
$name = $item->getName();
|
||||
|
||||
if ($name && $address) {
|
||||
$map->$address = $name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function getAddressData(Message $message, string $type): ?object
|
||||
{
|
||||
$header = $this->getMessage($message)->getHeader($type);
|
||||
|
||||
/** @var ?AddressHeader $header */
|
||||
|
||||
if ($header && method_exists($header, 'getAddresses')) {
|
||||
foreach ($header->getAddresses() as $item) {
|
||||
return (object) [
|
||||
'address' => $item->getEmail(),
|
||||
'name' => $item->getName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getAddressList(Message $message, string $type): array
|
||||
{
|
||||
$addressList = [];
|
||||
|
||||
$header = $this->getMessage($message)->getHeader($type);
|
||||
|
||||
/** @var ?AddressHeader $header */
|
||||
|
||||
if ($header && method_exists($header, 'getAddresses')) {
|
||||
$list = $header->getAddresses();
|
||||
|
||||
foreach ($list as $address) {
|
||||
$addressList[] = $address->getEmail();
|
||||
}
|
||||
}
|
||||
|
||||
return $addressList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Part[]
|
||||
*/
|
||||
public function getPartList(Message $message): array
|
||||
{
|
||||
$wrappeeList = $this->getMessage($message)->getChildParts();
|
||||
|
||||
$partList = [];
|
||||
|
||||
foreach ($wrappeeList as $wrappee) {
|
||||
$partList[] = new WrapperPart($wrappee);
|
||||
}
|
||||
|
||||
return $partList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Attachment[]
|
||||
*/
|
||||
public function getInlineAttachmentList(Message $message, Email $email): array
|
||||
{
|
||||
$inlineAttachmentList = [];
|
||||
|
||||
$this->loadContent($message);
|
||||
|
||||
$bodyPlain = '';
|
||||
$bodyHtml = '';
|
||||
|
||||
$htmlPartCount = $this->getMessage($message)->getHtmlPartCount();
|
||||
$textPartCount = $this->getMessage($message)->getTextPartCount();
|
||||
|
||||
if (!$htmlPartCount) {
|
||||
$bodyHtml = $this->getMessage($message)->getHtmlContent();
|
||||
}
|
||||
|
||||
if (!$textPartCount) {
|
||||
$bodyPlain = $this->getMessage($message)->getTextContent();
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $htmlPartCount; $i++) {
|
||||
if ($i) {
|
||||
$bodyHtml .= "<br>";
|
||||
}
|
||||
|
||||
$inlinePart = $this->getMessage($message)->getHtmlPart($i);
|
||||
|
||||
$bodyHtml .= $inlinePart?->getContent() ?? '';
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $textPartCount; $i++) {
|
||||
if ($i) {
|
||||
$bodyPlain .= "\n";
|
||||
}
|
||||
|
||||
$inlinePart = $this->getMessage($message)->getTextPart($i);
|
||||
|
||||
$bodyPlain .= $inlinePart?->getContent() ?? '';
|
||||
}
|
||||
|
||||
if ($bodyHtml) {
|
||||
$email->setIsHtml();
|
||||
$email->setBody($bodyHtml);
|
||||
|
||||
if ($bodyPlain) {
|
||||
$email->setBodyPlain($bodyPlain);
|
||||
}
|
||||
} else {
|
||||
$email->setIsHtml(false);
|
||||
$email->setBody($bodyPlain);
|
||||
$email->setBodyPlain($bodyPlain);
|
||||
}
|
||||
|
||||
if (!$email->getBody() && $email->hasBodyPlain()) {
|
||||
$email->setBody($email->getBodyPlain());
|
||||
}
|
||||
|
||||
$attachmentPartList = $this->getMessage($message)->getAllAttachmentParts();
|
||||
|
||||
$inlineAttachmentMap = [];
|
||||
|
||||
foreach ($attachmentPartList as $i => $attachmentPart) {
|
||||
if (!$attachmentPart instanceof MimePart) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attachment = $this->entityManager->getRDBRepositoryByClass(Attachment::class)->getNew();
|
||||
|
||||
$filename = $this->extractFileName($attachmentPart, $i);
|
||||
$contentType = $this->detectAttachmentContentType($attachmentPart, $filename);
|
||||
$disposition = $attachmentPart->getHeaderValue(HeaderConsts::CONTENT_DISPOSITION);
|
||||
|
||||
if ($contentType) {
|
||||
$contentType = strtolower($contentType);
|
||||
}
|
||||
|
||||
$attachment->setName($filename);
|
||||
$attachment->setType($contentType);
|
||||
|
||||
$content = '';
|
||||
|
||||
$binaryContentStream = $attachmentPart->getBinaryContentStream();
|
||||
|
||||
if ($binaryContentStream) {
|
||||
$content = $binaryContentStream->getContents();
|
||||
}
|
||||
|
||||
$contentId = $attachmentPart->getHeaderValue('Content-ID');
|
||||
|
||||
if ($contentId) {
|
||||
$contentId = trim($contentId, '<>');
|
||||
}
|
||||
|
||||
if ($disposition === self::DISPOSITION_INLINE) {
|
||||
$attachment->setRole(Attachment::ROLE_INLINE_ATTACHMENT);
|
||||
$attachment->setTargetField(self::FIELD_BODY);
|
||||
} else {
|
||||
$attachment->setRole(Attachment::ROLE_ATTACHMENT);
|
||||
$attachment->setTargetField(self::FIELD_ATTACHMENTS);
|
||||
}
|
||||
|
||||
$attachment->setContents($content);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
if ($attachment->getRole() === Attachment::ROLE_ATTACHMENT) {
|
||||
$email->addLinkMultipleId(self::FIELD_ATTACHMENTS, $attachment->getId());
|
||||
|
||||
if ($contentId) {
|
||||
$inlineAttachmentMap[$contentId] = $attachment;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Inline disposition.
|
||||
|
||||
if ($contentId) {
|
||||
$inlineAttachmentMap[$contentId] = $attachment;
|
||||
|
||||
$inlineAttachmentList[] = $attachment;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// No ID found, fallback to attachment.
|
||||
$attachment
|
||||
->setRole(Attachment::ROLE_ATTACHMENT)
|
||||
->setTargetField(self::FIELD_ATTACHMENTS);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
$email->addLinkMultipleId(self::FIELD_ATTACHMENTS, $attachment->getId());
|
||||
}
|
||||
|
||||
$body = $email->getBody();
|
||||
|
||||
if ($body) {
|
||||
foreach ($inlineAttachmentMap as $cid => $attachment) {
|
||||
if (str_contains($body, 'cid:' . $cid)) {
|
||||
$body = str_replace(
|
||||
'cid:' . $cid,
|
||||
'?entryPoint=attachment&id=' . $attachment->getId(),
|
||||
$body
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback to attachment.
|
||||
if ($attachment->getRole() === Attachment::ROLE_INLINE_ATTACHMENT) {
|
||||
$attachment
|
||||
->setRole(Attachment::ROLE_ATTACHMENT)
|
||||
->setTargetField(self::FIELD_ATTACHMENTS);
|
||||
|
||||
$this->entityManager->saveEntity($attachment);
|
||||
|
||||
$email->addLinkMultipleId(self::FIELD_ATTACHMENTS, $attachment->getId());
|
||||
}
|
||||
}
|
||||
|
||||
$email->setBody($body);
|
||||
}
|
||||
|
||||
/** @var ?MessagePart $textCalendarPart */
|
||||
$textCalendarPart =
|
||||
$this->getMessage($message)->getAllPartsByMimeType('text/calendar')[0] ??
|
||||
$this->getMessage($message)->getAllPartsByMimeType('application/ics')[0] ??
|
||||
null;
|
||||
|
||||
if ($textCalendarPart && $textCalendarPart->hasContent()) {
|
||||
$email->set('icsContents', $textCalendarPart->getContent());
|
||||
}
|
||||
|
||||
return $inlineAttachmentList;
|
||||
}
|
||||
|
||||
private function detectAttachmentContentType(MimePart $part, ?string $filename): ?string
|
||||
{
|
||||
$contentType = $part->getHeaderValue(HeaderConsts::CONTENT_TYPE);
|
||||
|
||||
if ($contentType && strtolower($contentType) !== self::TYPE_OCTET_STREAM) {
|
||||
return $contentType;
|
||||
}
|
||||
|
||||
if (!$filename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ext = $this->getAttachmentFilenameExtension($filename);
|
||||
|
||||
if (!$ext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->extMimeTypeMap[$ext] ?? null;
|
||||
}
|
||||
|
||||
private function getAttachmentFilenameExtension(string $filename): ?string
|
||||
{
|
||||
if (!$filename) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$ext = explode('.', $filename)[1] ?? null;
|
||||
|
||||
if (!$ext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return strtolower($ext);
|
||||
}
|
||||
|
||||
private function extractFileName(MimePart $attachmentPart, int $i): string
|
||||
{
|
||||
$filename = $attachmentPart->getHeaderParameter(HeaderConsts::CONTENT_DISPOSITION, 'filename');
|
||||
|
||||
if ($filename === null) {
|
||||
$filename = $attachmentPart->getHeaderParameter(HeaderConsts::CONTENT_TYPE, 'name');
|
||||
}
|
||||
|
||||
if ($filename === null && $attachmentPart->getContentType() === self::TYPE_MESSAGE_RFC822) {
|
||||
$filename = 'message-' . ($i + 1) . '.eml';
|
||||
}
|
||||
|
||||
if ($filename === null) {
|
||||
$filename = 'unnamed';
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
648
application/Espo/Core/Mail/Sender.php
Normal file
648
application/Espo/Core/Mail/Sender.php
Normal file
@@ -0,0 +1,648 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\FileStorage\Manager as FileStorageManager;
|
||||
use Espo\Core\Mail\Exceptions\NoSmtp;
|
||||
use Espo\Core\Mail\Sender\MessageContainer;
|
||||
use Espo\Core\Mail\Sender\TransportPreparatorFactory;
|
||||
use Espo\Core\ORM\Repository\Option\SaveOption;
|
||||
use Espo\ORM\EntityCollection;
|
||||
use Espo\Core\Field\DateTime;
|
||||
use Espo\Core\Utils\Config;
|
||||
use Espo\Core\Utils\Log;
|
||||
use Espo\Core\Mail\Account\SendingAccountProvider;
|
||||
use Espo\Core\Mail\Exceptions\SendingError;
|
||||
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;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email as Message;
|
||||
use Symfony\Component\Mime\Part\DataPart;
|
||||
|
||||
use Exception;
|
||||
use LogicException;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Sends emails. Builds parameters for sending. Should not be used directly.
|
||||
*/
|
||||
class Sender
|
||||
{
|
||||
private ?TransportInterface $transport = null;
|
||||
private bool $isGlobal = false;
|
||||
/** @var array<string, mixed> */
|
||||
private array $params = [];
|
||||
/** @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}[] */
|
||||
private array $headers = [];
|
||||
private ?MessageContainer $messageContainer = null;
|
||||
|
||||
private const ATTACHMENT_ATTR_CONTENTS = 'contents';
|
||||
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
private EntityManager $entityManager,
|
||||
private Log $log,
|
||||
private SendingAccountProvider $accountProvider,
|
||||
private FileStorageManager $fileStorageManager,
|
||||
private ConfigDataProvider $configDataProvider,
|
||||
private TransportPreparatorFactory $transportPreparatorFactory,
|
||||
) {
|
||||
$this->useGlobal();
|
||||
}
|
||||
|
||||
private function resetParams(): void
|
||||
{
|
||||
$this->params = [];
|
||||
$this->envelopeFromAddress = null;
|
||||
$this->laminasMessage = null;
|
||||
$this->attachmentList = null;
|
||||
$this->overrideParams = [];
|
||||
$this->headers = [];
|
||||
$this->messageContainer = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* With parameters.
|
||||
*
|
||||
* @param SenderParams|array<string, mixed> $params
|
||||
*/
|
||||
public function withParams($params): self
|
||||
{
|
||||
if ($params instanceof SenderParams) {
|
||||
$params = $params->toArray();
|
||||
} else if (!is_array($params)) {
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
|
||||
$paramList = [
|
||||
'fromAddress',
|
||||
'fromName',
|
||||
'replyToAddress',
|
||||
'replyToName',
|
||||
];
|
||||
|
||||
foreach (array_keys($params) as $key) {
|
||||
if (!in_array($key, $paramList)) {
|
||||
unset($params[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->overrideParams = array_merge($this->overrideParams, $params);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* With specific SMTP parameters.
|
||||
*
|
||||
* @param SmtpParams|array<string, mixed> $params
|
||||
*/
|
||||
public function withSmtpParams($params): self
|
||||
{
|
||||
if ($params instanceof SmtpParams) {
|
||||
$params = $params->toArray();
|
||||
} else if (!is_array($params)) {
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
return $this->useSmtp($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* With specific attachments.
|
||||
*
|
||||
* @param iterable<Attachment> $attachmentList
|
||||
*/
|
||||
public function withAttachments(iterable $attachmentList): self
|
||||
{
|
||||
$this->attachmentList = $attachmentList;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* With an envelope from address.
|
||||
*
|
||||
* @since 9.1.0
|
||||
*/
|
||||
public function withEnvelopeFromAddress(string $fromAddress): void
|
||||
{
|
||||
$this->envelopeFromAddress = $fromAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* With envelope options.
|
||||
*
|
||||
* @param array{from: string} $options
|
||||
* @deprecated As of v9.1.
|
||||
* @todo Remove in v10.0. Use `withEnvelopeFromAddress`.
|
||||
*/
|
||||
public function withEnvelopeOptions(array $options): self
|
||||
{
|
||||
/** @noinspection PhpDeprecationInspection */
|
||||
return $this->setEnvelopeOptions($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 9.2.0
|
||||
* @internal
|
||||
*/
|
||||
public function withMessageContainer(MessageContainer $messageContainer): self
|
||||
{
|
||||
$this->messageContainer = $messageContainer;
|
||||
|
||||
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.
|
||||
*
|
||||
* @param string $name A header name.
|
||||
* @param string $value A header value.
|
||||
* @since 9.1.0
|
||||
*/
|
||||
public function withAddedHeader(string $name, string $value): self
|
||||
{
|
||||
$this->headers[] = [$name, $value];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated As of v6.0. Use withParams.
|
||||
* @param array<string, mixed> $params
|
||||
* @todo Remove in v10.0.
|
||||
*/
|
||||
public function setParams(array $params = []): self
|
||||
{
|
||||
$this->params = array_merge($this->params, $params);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated As of 6.0. Use withSmtpParams.
|
||||
* @param array<string, mixed> $params
|
||||
* @todo Make private in v10.0.
|
||||
*/
|
||||
public function useSmtp(array $params = []): self
|
||||
{
|
||||
$this->isGlobal = false;
|
||||
|
||||
$this->applySmtp($params);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function useGlobal(): void
|
||||
{
|
||||
$this->params = [];
|
||||
$this->isGlobal = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
private function applySmtp(array $params = []): void
|
||||
{
|
||||
$this->params = $params;
|
||||
|
||||
$smtpParams = SmtpParams::fromArray($params);
|
||||
|
||||
$preparator = $this->transportPreparatorFactory->create($smtpParams);
|
||||
|
||||
$this->transport = $preparator->prepare($smtpParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
private function applyGlobal(): void
|
||||
{
|
||||
$systemAccount = $this->accountProvider->getSystem();
|
||||
|
||||
if (!$systemAccount) {
|
||||
throw new NoSmtp("No system SMTP settings.");
|
||||
}
|
||||
|
||||
$smtpParams = $systemAccount->getSmtpParams();
|
||||
|
||||
if (!$smtpParams) {
|
||||
throw new NoSmtp("No system SMTP settings.");
|
||||
}
|
||||
|
||||
$this->applySmtp($smtpParams->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an email.
|
||||
*
|
||||
* @throws SendingError
|
||||
*/
|
||||
public function send(Email $email): void
|
||||
{
|
||||
if ($this->isGlobal) {
|
||||
$this->applyGlobal();
|
||||
}
|
||||
|
||||
$message = new Message();
|
||||
|
||||
$params = array_merge($this->params, $this->overrideParams);
|
||||
|
||||
$this->applyHeaders($email, $message);
|
||||
$this->applyFrom($email, $message, $params);
|
||||
$this->addRecipientAddresses($email, $message);
|
||||
$this->applyReplyTo($email, $message, $params);
|
||||
$this->applySubject($email, $message);
|
||||
$this->applyBody($email, $message);
|
||||
$this->applyMessageId($email, $message);
|
||||
|
||||
$this->applyLaminasMessageHeaders($message);
|
||||
|
||||
if (!$this->transport) {
|
||||
throw new LogicException();
|
||||
}
|
||||
|
||||
$envelope = $this->prepareEnvelope($message);
|
||||
|
||||
if ($this->messageContainer) {
|
||||
$this->messageContainer->message = new Sender\Message($message);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->transport->send($message, $envelope);
|
||||
} catch (Exception|TransportExceptionInterface $e) {
|
||||
$this->resetParams();
|
||||
$this->useGlobal();
|
||||
|
||||
$this->handleException($e);
|
||||
}
|
||||
|
||||
$email
|
||||
->setStatus(Email::STATUS_SENT)
|
||||
->setDateSent(DateTime::createNow())
|
||||
->setSendAt(null);
|
||||
|
||||
$this->resetParams();
|
||||
$this->useGlobal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataPart[]
|
||||
*/
|
||||
private function getAttachmentParts(Email $email): array
|
||||
{
|
||||
/** @var EntityCollection<Attachment> $collection */
|
||||
$collection = $this->entityManager
|
||||
->getCollectionFactory()
|
||||
->create(Attachment::ENTITY_TYPE);
|
||||
|
||||
if (!$email->isNew()) {
|
||||
foreach ($email->getAttachments() as $attachment) {
|
||||
$collection[] = $attachment;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->attachmentList !== null) {
|
||||
foreach ($this->attachmentList as $attachment) {
|
||||
$collection[] = $attachment;
|
||||
}
|
||||
}
|
||||
|
||||
$list = [];
|
||||
|
||||
foreach ($collection as $attachment) {
|
||||
$contents = $attachment->has(self::ATTACHMENT_ATTR_CONTENTS) ?
|
||||
$attachment->get(self::ATTACHMENT_ATTR_CONTENTS) :
|
||||
$this->fileStorageManager->getContents($attachment);
|
||||
|
||||
$part = new DataPart(
|
||||
body: $contents,
|
||||
filename: $attachment->getName() ?? '',
|
||||
contentType: $attachment->getType(),
|
||||
);
|
||||
|
||||
$list[] = $part;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataPart[]
|
||||
*/
|
||||
private function getInlineAttachmentParts(Email $email): array
|
||||
{
|
||||
$list = [];
|
||||
|
||||
foreach ($email->getInlineAttachmentList() as $attachment) {
|
||||
$contents = $attachment->has(self::ATTACHMENT_ATTR_CONTENTS) ?
|
||||
$attachment->get(self::ATTACHMENT_ATTR_CONTENTS) :
|
||||
$this->fileStorageManager->getContents($attachment);
|
||||
|
||||
$part = (new DataPart($contents, null, $attachment->getType()))
|
||||
->asInline()
|
||||
->setContentId($attachment->getId() . '@espo');
|
||||
|
||||
$list[] = $part;
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws SendingError
|
||||
*/
|
||||
private function handleException(Exception|TransportExceptionInterface $e): never
|
||||
{
|
||||
if ($e instanceof TransportExceptionInterface) {
|
||||
$message = "unknownError";
|
||||
|
||||
if (
|
||||
stripos($e->getMessage(), 'password') !== false ||
|
||||
stripos($e->getMessage(), 'credentials') !== false ||
|
||||
stripos($e->getMessage(), '5.7.8') !== false ||
|
||||
stripos($e->getMessage(), '5.7.3') !== false
|
||||
) {
|
||||
$message = 'invalidCredentials';
|
||||
}
|
||||
|
||||
$this->log->error("Email sending error: " . $e->getMessage(), ['exception' => $e]);
|
||||
|
||||
throw new SendingError($message);
|
||||
}
|
||||
|
||||
throw new SendingError($e->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Since v9.1.0. Use EmailSender::generateMessageId.
|
||||
* @noinspection PhpUnused
|
||||
* @todo Remove in v10.0.
|
||||
*/
|
||||
static public function generateMessageId(Email $email): string
|
||||
{
|
||||
return EmailSender::generateMessageId($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated As of v6.0.
|
||||
*
|
||||
* @param array{from: string} $options
|
||||
* @todo Make private in v10.0. Use `withEnvelopeFromAddress`.
|
||||
*/
|
||||
public function setEnvelopeOptions(array $options): self
|
||||
{
|
||||
$this->envelopeFromAddress = $options['from'];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function addRecipientAddresses(Email $email, Message $message): void
|
||||
{
|
||||
$value = $email->get('to');
|
||||
|
||||
if ($value) {
|
||||
foreach (explode(';', $value) as $address) {
|
||||
$message->addTo(trim($address));
|
||||
}
|
||||
}
|
||||
|
||||
$value = $email->get('cc');
|
||||
|
||||
if ($value) {
|
||||
foreach (explode(';', $value) as $address) {
|
||||
$message->addCC(trim($address));
|
||||
}
|
||||
}
|
||||
|
||||
$value = $email->get('bcc');
|
||||
|
||||
if ($value) {
|
||||
foreach (explode(';', $value) as $address) {
|
||||
$message->addBCC(trim($address));
|
||||
}
|
||||
}
|
||||
|
||||
$value = $email->get('replyTo');
|
||||
|
||||
if ($value) {
|
||||
foreach (explode(';', $value) as $address) {
|
||||
$message->addReplyTo(trim($address));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
* @throws NoSmtp
|
||||
*/
|
||||
private function applyFrom(Email $email, Message $message, array $params): void
|
||||
{
|
||||
$fromName = $params['fromName'] ?? $this->config->get('outboundEmailFromName');
|
||||
|
||||
$fromAddress = $email->get('from');
|
||||
|
||||
if ($fromAddress) {
|
||||
$fromAddress = trim($fromAddress);
|
||||
} else {
|
||||
if (
|
||||
empty($params['fromAddress']) &&
|
||||
!$this->configDataProvider->getSystemOutboundAddress()
|
||||
) {
|
||||
throw new NoSmtp('outboundEmailFromAddress is not specified in config.');
|
||||
}
|
||||
|
||||
$fromAddress = $params['fromAddress'] ?? $this->configDataProvider->getSystemOutboundAddress();
|
||||
|
||||
$email->setFromAddress($fromAddress);
|
||||
}
|
||||
|
||||
$message->addFrom(new Address($fromAddress, $fromName ?? ''));
|
||||
|
||||
$fromString = '<' . $fromAddress . '>';
|
||||
|
||||
if ($fromName) {
|
||||
$fromString = $fromName . ' ' . $fromString;
|
||||
}
|
||||
|
||||
$email->set('fromString', $fromString);
|
||||
|
||||
$message->sender($fromAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
private function applyReplyTo(Email $email, Message $message, array $params): void
|
||||
{
|
||||
$address = $params['replyToAddress'] ?? null;
|
||||
$name = $params['replyToName'] ?? null;
|
||||
|
||||
if (!$address) {
|
||||
return;
|
||||
}
|
||||
|
||||
$message->replyTo(new Address($address, $name ?? ''));
|
||||
|
||||
$email->setReplyToAddressList([$address]);
|
||||
}
|
||||
|
||||
private function applyMessageId(Email $email, Message $message): void
|
||||
{
|
||||
$messageId = $email->getMessageId();
|
||||
|
||||
if (
|
||||
!$messageId ||
|
||||
strlen($messageId) < 4 ||
|
||||
str_starts_with($messageId, 'dummy:')
|
||||
) {
|
||||
$messageId = EmailSender::generateMessageId($email);
|
||||
|
||||
$email->setMessageId('<' . $messageId . '>');
|
||||
|
||||
if ($email->hasId()) {
|
||||
$this->entityManager->saveEntity($email, [SaveOption::SILENT => true]);
|
||||
}
|
||||
} else {
|
||||
$messageId = substr($messageId, 1, strlen($messageId) - 2);
|
||||
}
|
||||
|
||||
$message->getHeaders()->addIdHeader('Message-ID', $messageId);
|
||||
}
|
||||
|
||||
private function applyBody(Email $email, Message $message): void
|
||||
{
|
||||
$message->text($email->getBodyPlainForSending());
|
||||
|
||||
if ($email->isHtml()) {
|
||||
$message->html($email->getBodyForSending());
|
||||
}
|
||||
|
||||
foreach ($this->getAttachmentParts($email) as $part) {
|
||||
$message->addPart($part);
|
||||
}
|
||||
|
||||
foreach ($this->getInlineAttachmentParts($email) as $part) {
|
||||
$message->addPart($part);
|
||||
}
|
||||
}
|
||||
|
||||
private function applySubject(Email $email, Message $message): void
|
||||
{
|
||||
$message->subject($email->getSubject() ?? '');
|
||||
}
|
||||
|
||||
private function applyHeaders(Email $email, Message $message): void
|
||||
{
|
||||
foreach ($this->headers as $item) {
|
||||
$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');
|
||||
}
|
||||
}
|
||||
|
||||
private function prepareEnvelope(Message $message): ?Envelope
|
||||
{
|
||||
if (!$this->envelopeFromAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$recipients = [
|
||||
...$message->getTo(),
|
||||
...$message->getCc(),
|
||||
...$message->getBcc(),
|
||||
];
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
118
application/Espo/Core/Mail/Sender/DefaultTransportPreparator.php
Normal file
118
application/Espo/Core/Mail/Sender/DefaultTransportPreparator.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?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\Sender;
|
||||
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Espo\Core\Utils\Config;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Mailer\Transport\Dsn;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Auth\CramMd5Authenticator;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Auth\LoginAuthenticator;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Auth\PlainAuthenticator;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\Auth\XOAuth2Authenticator;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
|
||||
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransportFactory;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
|
||||
class DefaultTransportPreparator implements TransportPreparator
|
||||
{
|
||||
public function __construct(
|
||||
private Config $config,
|
||||
) {}
|
||||
|
||||
public function prepare(SmtpParams $smtpParams): TransportInterface
|
||||
{
|
||||
$localHostName = $this->config->get('smtpLocalHostName', gethostname());
|
||||
|
||||
// 'SSL' is treated as implicit SSL/TLS. 'TLS' is treated as STARTTLS.
|
||||
// STARTTLS is the most common method.
|
||||
$scheme = $smtpParams->getSecurity() === 'SSL' ? 'smtps' : 'smtp';
|
||||
|
||||
if ($smtpParams->getSecurity() === '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(
|
||||
scheme: $scheme,
|
||||
host: $smtpParams->getServer(),
|
||||
port: $smtpParams->getPort(),
|
||||
)
|
||||
);
|
||||
|
||||
if (!$transport instanceof EsmtpTransport) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$transport->setLocalDomain($localHostName);
|
||||
|
||||
$authMechanism = null;
|
||||
|
||||
// @todo For xoauth, set authMechanism, username, password in handlers.
|
||||
$connectionOptions = $smtpParams->getConnectionOptions() ?? [];
|
||||
$authString = $connectionOptions['authString'] ?? null;
|
||||
|
||||
if ($authString) {
|
||||
$decodedAuthString = base64_decode($authString);
|
||||
|
||||
/** @noinspection RegExpRedundantEscape */
|
||||
if (preg_match("/user=(.*?)\\\1auth=Bearer (.*?)\\\1\\\1/", $decodedAuthString, $matches) !== false) {
|
||||
$username = $matches[1];
|
||||
$token = $matches[2];
|
||||
|
||||
$transport->setUsername($username);
|
||||
$transport->setPassword($token);
|
||||
}
|
||||
|
||||
$authMechanism = SmtpParams::AUTH_MECHANISM_XOAUTH;
|
||||
} else if ($smtpParams->useAuth()) {
|
||||
$authMechanism = $smtpParams->getAuthMechanism() ?: SmtpParams::AUTH_MECHANISM_LOGIN;
|
||||
|
||||
$transport->setUsername($smtpParams->getUsername() ?? '');
|
||||
$transport->setPassword($smtpParams->getPassword() ?? '');
|
||||
}
|
||||
|
||||
if ($authMechanism === SmtpParams::AUTH_MECHANISM_LOGIN) {
|
||||
$transport->setAuthenticators([new LoginAuthenticator()]);
|
||||
} else if ($authMechanism === SmtpParams::AUTH_MECHANISM_CRAMMD5) {
|
||||
$transport->setAuthenticators([new CramMd5Authenticator()]);
|
||||
} else if ($authMechanism === SmtpParams::AUTH_MECHANISM_PLAIN) {
|
||||
$transport->setAuthenticators([new PlainAuthenticator()]);
|
||||
} else if ($authMechanism === SmtpParams::AUTH_MECHANISM_XOAUTH) {
|
||||
$transport->setAuthenticators([new XOAuth2Authenticator()]);
|
||||
}
|
||||
|
||||
return $transport;
|
||||
}
|
||||
}
|
||||
48
application/Espo/Core/Mail/Sender/Message.php
Normal file
48
application/Espo/Core/Mail/Sender/Message.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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\Sender;
|
||||
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
/**
|
||||
* @since 9.2.0
|
||||
* @internal
|
||||
*/
|
||||
class Message
|
||||
{
|
||||
public function __construct(
|
||||
private Email $email,
|
||||
) {}
|
||||
|
||||
public function toString(): string
|
||||
{
|
||||
return $this->email->toString();
|
||||
}
|
||||
}
|
||||
41
application/Espo/Core/Mail/Sender/MessageContainer.php
Normal file
41
application/Espo/Core/Mail/Sender/MessageContainer.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\Sender;
|
||||
|
||||
/**
|
||||
* @since 9.2.0
|
||||
* @internal
|
||||
*/
|
||||
class MessageContainer
|
||||
{
|
||||
public function __construct(
|
||||
public ?Message $message = null,
|
||||
) {}
|
||||
}
|
||||
41
application/Espo/Core/Mail/Sender/TransportPreparator.php
Normal file
41
application/Espo/Core/Mail/Sender/TransportPreparator.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?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\Sender;
|
||||
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
use Symfony\Component\Mailer\Transport\TransportInterface;
|
||||
|
||||
/**
|
||||
* @since 9.1.0
|
||||
*/
|
||||
interface TransportPreparator
|
||||
{
|
||||
public function prepare(SmtpParams $smtpParams): TransportInterface;
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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\Sender;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
|
||||
class TransportPreparatorFactory
|
||||
{
|
||||
public function __construct(
|
||||
private InjectableFactory $injectableFactory,
|
||||
) {}
|
||||
|
||||
public function create(SmtpParams $smtpParams): TransportPreparator
|
||||
{
|
||||
$className = $smtpParams->getTransportPreparatorClassName() ?? DefaultTransportPreparator::class;
|
||||
|
||||
return $this->injectableFactory->create($className);
|
||||
}
|
||||
}
|
||||
140
application/Espo/Core/Mail/SenderParams.php
Normal file
140
application/Espo/Core/Mail/SenderParams.php
Normal file
@@ -0,0 +1,140 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Sender parameters.
|
||||
*
|
||||
* Immutable.
|
||||
*/
|
||||
class SenderParams
|
||||
{
|
||||
private ?string $fromAddress = null;
|
||||
private ?string $fromName = null;
|
||||
private ?string $replyToAddress = null;
|
||||
private ?string $replyToName = null;
|
||||
|
||||
/** @var string[] */
|
||||
private $paramList = [
|
||||
'fromAddress',
|
||||
'fromName',
|
||||
'replyToAddress',
|
||||
'replyToName',
|
||||
];
|
||||
|
||||
public static function create(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
foreach ($this->paramList as $name) {
|
||||
if ($this->$name !== null) {
|
||||
$params[$name] = $this->$name;
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public static function fromArray(array $params): self
|
||||
{
|
||||
$obj = new self();
|
||||
|
||||
foreach ($obj->paramList as $name) {
|
||||
if (array_key_exists($name, $params)) {
|
||||
$obj->$name = $params[$name];
|
||||
}
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function getFromAddress(): ?string
|
||||
{
|
||||
return $this->fromAddress;
|
||||
}
|
||||
|
||||
public function getFromName(): ?string
|
||||
{
|
||||
return $this->fromName;
|
||||
}
|
||||
|
||||
public function getReplyToAddress(): ?string
|
||||
{
|
||||
return $this->replyToAddress;
|
||||
}
|
||||
|
||||
public function getReplyToName(): ?string
|
||||
{
|
||||
return $this->replyToName;
|
||||
}
|
||||
|
||||
public function withFromAddress(?string $fromAddress): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->fromAddress = $fromAddress;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withFromName(?string $fromName): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->fromName = $fromName;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withReplyToAddress(?string $replyToAddress): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->replyToAddress = $replyToAddress;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withReplyToName(?string $replyToName): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->replyToName = $replyToName;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
}
|
||||
37
application/Espo/Core/Mail/Smtp/Handler.php
Normal file
37
application/Espo/Core/Mail/Smtp/Handler.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\Smtp;
|
||||
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
|
||||
interface Handler
|
||||
{
|
||||
public function handle(SmtpParams $params, ?string $id): SmtpParams;
|
||||
}
|
||||
61
application/Espo/Core/Mail/Smtp/HandlerProcessor.php
Normal file
61
application/Espo/Core/Mail/Smtp/HandlerProcessor.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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\Smtp;
|
||||
|
||||
use Espo\Core\InjectableFactory;
|
||||
use Espo\Core\Mail\SmtpParams;
|
||||
|
||||
class HandlerProcessor
|
||||
{
|
||||
public function __construct(private InjectableFactory $injectableFactory)
|
||||
{}
|
||||
|
||||
/**
|
||||
* @param class-string<object> $className
|
||||
*/
|
||||
public function handle(string $className, SmtpParams $params, ?string $id): SmtpParams
|
||||
{
|
||||
$handler = $this->injectableFactory->create($className);
|
||||
|
||||
if ($handler instanceof Handler) {
|
||||
return $handler->handle($params, $id);
|
||||
}
|
||||
|
||||
if (method_exists($handler, 'applyParams')) {
|
||||
$raw = $params->toArray();
|
||||
|
||||
$handler->applyParams($id, $raw);
|
||||
|
||||
return SmtpParams::fromArray($raw);
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
276
application/Espo/Core/Mail/SmtpParams.php
Normal file
276
application/Espo/Core/Mail/SmtpParams.php
Normal file
@@ -0,0 +1,276 @@
|
||||
<?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;
|
||||
|
||||
use Espo\Core\Mail\Sender\TransportPreparator;
|
||||
use RuntimeException;
|
||||
use SensitiveParameter;
|
||||
|
||||
/**
|
||||
* SMTP parameters.
|
||||
*
|
||||
* Immutable.
|
||||
*/
|
||||
class SmtpParams
|
||||
{
|
||||
private ?string $fromAddress = null;
|
||||
private ?string $fromName = null;
|
||||
/** @var ?array<string, mixed> */
|
||||
private ?array $connectionOptions = null;
|
||||
private bool $auth = false;
|
||||
private ?string $authMechanism = null;
|
||||
private ?string $username = null;
|
||||
private ?string $password = null;
|
||||
private ?string $security = null;
|
||||
/** @var ?class-string<TransportPreparator> */
|
||||
private ?string $transportPreparatorClassName = null;
|
||||
|
||||
public const AUTH_MECHANISM_LOGIN = 'login';
|
||||
public const AUTH_MECHANISM_CRAMMD5 = 'crammd5';
|
||||
public const AUTH_MECHANISM_PLAIN = 'plain';
|
||||
public const AUTH_MECHANISM_XOAUTH = 'xoauth';
|
||||
|
||||
/** @var string[] */
|
||||
private array $paramList = [
|
||||
'server',
|
||||
'port',
|
||||
'fromAddress',
|
||||
'fromName',
|
||||
'connectionOptions',
|
||||
'auth',
|
||||
'authMechanism',
|
||||
'username',
|
||||
'password',
|
||||
'security',
|
||||
'transportPreparatorClassName',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private string $server,
|
||||
private int $port
|
||||
) {}
|
||||
|
||||
public static function create(string $server, int $port): self
|
||||
{
|
||||
return new self($server, $port);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$params = [];
|
||||
|
||||
foreach ($this->paramList as $name) {
|
||||
if ($this->$name !== null) {
|
||||
$params[$name] = $this->$name;
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $params
|
||||
*/
|
||||
public static function fromArray(array $params): self
|
||||
{
|
||||
$server = $params['server'] ?? null;
|
||||
$port = $params['port'] ?? null;
|
||||
$auth = $params['auth'] ?? false;
|
||||
|
||||
if ($server === null) {
|
||||
throw new RuntimeException("Empty server.");
|
||||
}
|
||||
|
||||
if ($port === null) {
|
||||
throw new RuntimeException("Empty port.");
|
||||
}
|
||||
|
||||
$obj = new self($server, $port);
|
||||
|
||||
$obj->auth = $auth;
|
||||
|
||||
foreach ($obj->paramList as $name) {
|
||||
if ($obj->$name !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (array_key_exists($name, $params)) {
|
||||
$obj->$name = $params[$name];
|
||||
}
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function getServer(): string
|
||||
{
|
||||
return $this->server;
|
||||
}
|
||||
|
||||
public function getPort(): int
|
||||
{
|
||||
return $this->port;
|
||||
}
|
||||
|
||||
public function getFromAddress(): ?string
|
||||
{
|
||||
return $this->fromAddress;
|
||||
}
|
||||
|
||||
public function getFromName(): ?string
|
||||
{
|
||||
return $this->fromName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?array<string, mixed>
|
||||
*/
|
||||
public function getConnectionOptions(): ?array
|
||||
{
|
||||
return $this->connectionOptions;
|
||||
}
|
||||
|
||||
public function useAuth(): bool
|
||||
{
|
||||
return $this->auth;
|
||||
}
|
||||
|
||||
public function getAuthMechanism(): ?string
|
||||
{
|
||||
return $this->authMechanism;
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function getSecurity(): ?string
|
||||
{
|
||||
return $this->security;
|
||||
}
|
||||
|
||||
public function withFromAddress(?string $fromAddress): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->fromAddress = $fromAddress;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withFromName(?string $fromName): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->fromName = $fromName;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?array<string, mixed> $connectionOptions
|
||||
*/
|
||||
public function withConnectionOptions(?array $connectionOptions): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->connectionOptions = $connectionOptions;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withAuth(bool $auth = true): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->auth = $auth;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withAuthMechanism(?string $authMechanism): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->authMechanism = $authMechanism;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withUsername(?string $username): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->username = $username;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withPassword(#[SensitiveParameter] ?string $password): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->password = $password;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function withSecurity(?string $security): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->security = $security;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?class-string<TransportPreparator> $transportPreparatorClassName
|
||||
* @since 9.1.0.
|
||||
* @noinspection PhpUnused
|
||||
*/
|
||||
public function withTransportPreparatorClassName(?string $transportPreparatorClassName): self
|
||||
{
|
||||
$obj = clone $this;
|
||||
$obj->transportPreparatorClassName = $transportPreparatorClassName;
|
||||
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?class-string<TransportPreparator>
|
||||
* @since 9.1.0.
|
||||
*/
|
||||
public function getTransportPreparatorClassName(): ?string
|
||||
{
|
||||
return $this->transportPreparatorClassName;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user