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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user