Initial commit

This commit is contained in:
root
2026-01-19 17:44:46 +01:00
commit 823af8b11d
8721 changed files with 1130846 additions and 0 deletions

View 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');
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View 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);
}
}

View File

@@ -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);
}
}