This commit is contained in:
Yurii
2025-12-04 11:14:18 +02:00
parent 52f17733f2
commit 889059c8d9
5 changed files with 211 additions and 139 deletions
@@ -0,0 +1,146 @@
<?php
namespace Espo\Core\Mail\Account;
use Espo\Core\InjectableFactory;
use Espo\Core\Mail\Account\Storage\Handler;
use Espo\Core\Mail\Account\Storage\LaminasStorage;
use Espo\Core\Mail\Account\Storage\Params;
use Espo\Core\Mail\Exceptions\ImapError;
use Espo\Core\Mail\Mail\Storage\Imap;
use Espo\Core\Utils\Log;
use Laminas\Mail\Protocol\Exception\ExceptionInterface;
use Laminas\Mail\Protocol\Exception\RuntimeException as ProtocolRuntimeException;
use Laminas\Mail\Protocol\Imap as ImapProtocol;
use Laminas\Mail\Storage\Exception\InvalidArgumentException;
use Laminas\Mail\Storage\Exception\RuntimeException as LaminasRuntimeException;
use RuntimeException;
/**
* @since 9.3.0
*/
class CommonStorageFactory
{
public function __construct(
private InjectableFactory $injectableFactory,
private Log $log,
) {}
/**
* @throws ImapError
*/
public function create(Params $params): LaminasStorage
{
$handlerClassName = $params->getImapHandlerClassName();
$handler = null;
$isHandled = false;
if ($handlerClassName && $params->getId()) {
$handler = $this->injectableFactory->create($handlerClassName);
if ($handler instanceof Handler || method_exists($handler, 'handle')) {
$params = $handler->handle($params, $params->getId());
$isHandled = true;
}
}
if ($params->getAuthMechanism() === Params::AUTH_MECHANISM_XOAUTH) {
$imapParams = $this->prepareXoauthProtocol($params);
} else {
$rawParams = [
'host' => $params->getHost(),
'port' => $params->getPort(),
'username' => $params->getUsername(),
'password' => $params->getPassword(),
'id' => $params->getId(),
];
if ($params->getSecurity()) {
$rawParams['security'] = $params->getSecurity();
}
$imapParams = null;
// For bc.
if (!$isHandled && $handler && $params->getId() && method_exists($handler, 'prepareProtocol')) {
$imapParams = $handler->prepareProtocol($params->getId(), $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 (LaminasRuntimeException|InvalidArgumentException|ProtocolRuntimeException $e) {
throw new ImapError($e->getMessage(), 0, $e);
}
return new LaminasStorage($storage);
}
private function prepareXoauthProtocol(Params $params): ?ImapProtocol
{
$username = $params->getUsername();
$accessToken = $params->getPassword();
$host = $params->getHost() ?? throw new RuntimeException("No IMAP host.");
$port = $params->getPort();
$ssl = $params->getSecurity() ?: false;
try {
$protocol = new ImapProtocol($host, $port, $ssl);
} catch (ExceptionInterface $e) {
throw new RuntimeException($e->getMessage(), previous: $e);
}
$authString = base64_encode("user=$username\1auth=Bearer $accessToken\1\1");
$authenticateParams = ['XOAUTH2', $authString];
$protocol->sendRequest('AUTHENTICATE', $authenticateParams);
$i = 0;
while (true) {
if ($i === 10) {
return null;
}
$response = '';
$isPlus = $protocol->readLine($response, '+', true);
if ($isPlus) {
$this->log->warning("Imap XOauth: Extra server challenge: " . var_export($response, true));
$protocol->sendRequest('');
} else {
if (
is_string($response) &&
(preg_match('/^NO /i', $response) || preg_match('/^BAD /i', $response))
) {
$this->log->error("Imap XOauth: Failure: " . var_export($response, true));
return null;
}
if (is_string($response) && preg_match("/^OK /i", $response)) {
break;
}
}
$i++;
}
return $protocol;
}
}
@@ -29,28 +29,17 @@
namespace Espo\Core\Mail\Account\GroupAccount;
use Espo\Core\Mail\Account\CommonStorageFactory;
use Espo\Core\Mail\Account\Storage\Params;
use Espo\Core\Mail\Account\Account;
use Espo\Core\Mail\Account\StorageFactory as StorageFactoryInterface;
use Espo\Core\Mail\Account\Storage\LaminasStorage;
use Espo\Core\Mail\Exceptions\ImapError;
use Espo\Core\Mail\Exceptions\NoImap;
use Espo\Core\Mail\Mail\Storage\Imap;
use Espo\Core\Utils\Log;
use Espo\Core\InjectableFactory;
use Espo\ORM\Name\Attribute;
use Laminas\Mail\Storage\Exception\RuntimeException;
use Laminas\Mail\Storage\Exception\InvalidArgumentException;
use Laminas\Mail\Protocol\Exception\RuntimeException as ProtocolRuntimeException;
use Throwable;
class StorageFactory implements StorageFactoryInterface
{
public function __construct(
private Log $log,
private InjectableFactory $injectableFactory
private CommonStorageFactory $commonStorageFactory,
) {}
public function create(Account $account): LaminasStorage
@@ -76,60 +65,6 @@ class StorageFactory implements StorageFactoryInterface
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);
return $this->commonStorageFactory->create($params);
}
}
@@ -29,29 +29,19 @@
namespace Espo\Core\Mail\Account\PersonalAccount;
use Espo\Core\Mail\Account\CommonStorageFactory;
use Espo\Core\Mail\Account\Storage\Params;
use Espo\Core\Mail\Account\StorageFactory as StorageFactoryInterface;
use Espo\Core\Mail\Account\Account;
use Espo\Core\Mail\Exceptions\ImapError;
use Espo\Core\Mail\Exceptions\NoImap;
use Espo\Core\Mail\Mail\Storage\Imap;
use Espo\Core\Mail\Account\Storage\LaminasStorage;
use Espo\Core\Utils\Log;
use Espo\Core\InjectableFactory;
use Espo\ORM\Name\Attribute;
use Laminas\Mail\Protocol\Exception\RuntimeException as ProtocolRuntimeException;
use Laminas\Mail\Storage\Exception\InvalidArgumentException;
use Laminas\Mail\Storage\Exception\RuntimeException;
use LogicException;
use Throwable;
class StorageFactory implements StorageFactoryInterface
{
public function __construct(
private Log $log,
private InjectableFactory $injectableFactory,
private CommonStorageFactory $commonStorageFactory,
) {}
public function create(Account $account): LaminasStorage
@@ -87,63 +77,6 @@ class StorageFactory implements StorageFactoryInterface
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);
return $this->commonStorageFactory->create($params);
}
}
@@ -0,0 +1,14 @@
<?php
namespace Espo\Core\Mail\Account\Storage;
/**
* Handle storage parameters.
* To be used by extensions.
*
* @since 9.3.0
*/
interface Handler
{
public function handle(Params $params, string $id): Params;
}
@@ -39,8 +39,19 @@ class Params
/** @var ?class-string<object> */
private ?string $imapHandlerClassName;
/**
* @since 9.3.0
*/
public const string AUTH_MECHANISM_PLAIN = 'plain';
/**
* @since 9.3.0
*/
public const string AUTH_MECHANISM_XOAUTH = 'xoauth';
/**
* @param ?class-string<object> $imapHandlerClassName
* @param self::AUTH_MECHANISM_* $authMechanism
*/
public function __construct(
private ?string $host,
@@ -51,7 +62,8 @@ class Params
?string $imapHandlerClassName,
private ?string $id,
private ?string $userId,
private ?string $emailAddress
private ?string $emailAddress,
private string $authMechanism = self::AUTH_MECHANISM_PLAIN,
) {
$this->imapHandlerClassName = $imapHandlerClassName;
}
@@ -109,6 +121,38 @@ class Params
return $this->emailAddress;
}
/**
* @return self::AUTH_MECHANISM_*
* @since 9.3.0
*/
public function getAuthMechanism(): string
{
return $this->authMechanism;
}
/**
* @param self::AUTH_MECHANISM_* $authMechanism
* @since 9.3.0
*/
public function withAuthMechanism(string $authMechanism): self
{
$obj = clone $this;
$obj->authMechanism = $authMechanism;
return $obj;
}
/**
* @since 9.3.0
*/
public function withUsername(?string $username): self
{
$obj = clone $this;
$obj->username = $username;
return $obj;
}
public function withPassword(#[SensitiveParameter] ?string $password): self
{
$obj = clone $this;