From 889059c8d932a0072680bb88abae12ae6fc85feb Mon Sep 17 00:00:00 2001 From: Yurii Date: Thu, 4 Dec 2025 11:14:18 +0200 Subject: [PATCH] imap ref --- .../Mail/Account/CommonStorageFactory.php | 146 ++++++++++++++++++ .../Account/GroupAccount/StorageFactory.php | 71 +-------- .../PersonalAccount/StorageFactory.php | 73 +-------- .../Core/Mail/Account/Storage/Handler.php | 14 ++ .../Espo/Core/Mail/Account/Storage/Params.php | 46 +++++- 5 files changed, 211 insertions(+), 139 deletions(-) create mode 100644 application/Espo/Core/Mail/Account/CommonStorageFactory.php create mode 100644 application/Espo/Core/Mail/Account/Storage/Handler.php diff --git a/application/Espo/Core/Mail/Account/CommonStorageFactory.php b/application/Espo/Core/Mail/Account/CommonStorageFactory.php new file mode 100644 index 0000000000..d2031d422f --- /dev/null +++ b/application/Espo/Core/Mail/Account/CommonStorageFactory.php @@ -0,0 +1,146 @@ +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; + } +} diff --git a/application/Espo/Core/Mail/Account/GroupAccount/StorageFactory.php b/application/Espo/Core/Mail/Account/GroupAccount/StorageFactory.php index c118452892..7068c74395 100644 --- a/application/Espo/Core/Mail/Account/GroupAccount/StorageFactory.php +++ b/application/Espo/Core/Mail/Account/GroupAccount/StorageFactory.php @@ -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); } } diff --git a/application/Espo/Core/Mail/Account/PersonalAccount/StorageFactory.php b/application/Espo/Core/Mail/Account/PersonalAccount/StorageFactory.php index ccd740f047..cd3b30048d 100644 --- a/application/Espo/Core/Mail/Account/PersonalAccount/StorageFactory.php +++ b/application/Espo/Core/Mail/Account/PersonalAccount/StorageFactory.php @@ -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); } } diff --git a/application/Espo/Core/Mail/Account/Storage/Handler.php b/application/Espo/Core/Mail/Account/Storage/Handler.php new file mode 100644 index 0000000000..d4a4b42697 --- /dev/null +++ b/application/Espo/Core/Mail/Account/Storage/Handler.php @@ -0,0 +1,14 @@ + */ 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 $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;