From 11c6b79a4dea8bdd51297bbceedca95a1cd3c332 Mon Sep 17 00:00:00 2001 From: Yuri Kuznetsov Date: Sat, 12 Apr 2025 13:00:22 +0300 Subject: [PATCH] remove system email account --- .../Mail/Account/SendingAccountProvider.php | 9 - .../Mail/Account/SystemSettingsAccount.php | 221 ------------------ .../Upgrades/Migrations/V9_1/AfterUpgrade.php | 73 ++++++ .../Espo/Resources/i18n/en_US/Settings.json | 11 +- .../layouts/Settings/outboundEmails.json | 9 - .../views/email-account/fields/test-send.js | 111 ++++++++- .../views/outbound-email/fields/test-send.js | 175 -------------- tests/unit/testData/Utils/Config/config.php | 30 +-- 8 files changed, 196 insertions(+), 443 deletions(-) delete mode 100644 application/Espo/Core/Mail/Account/SystemSettingsAccount.php delete mode 100644 client/src/views/outbound-email/fields/test-send.js diff --git a/application/Espo/Core/Mail/Account/SendingAccountProvider.php b/application/Espo/Core/Mail/Account/SendingAccountProvider.php index 80e8ad42e9..b4f616ed36 100644 --- a/application/Espo/Core/Mail/Account/SendingAccountProvider.php +++ b/application/Espo/Core/Mail/Account/SendingAccountProvider.php @@ -36,7 +36,6 @@ 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\Core\Utils\Config; use Espo\Entities\EmailAccount as EmailAccountEntity; use Espo\Entities\InboundEmail as InboundEmailEntity; use Espo\Entities\User; @@ -53,11 +52,9 @@ class SendingAccountProvider public function __construct( private EntityManager $entityManager, - private Config $config, private GroupAccountFactory $groupAccountFactory, private PersonalAccountFactory $personalAccountFactory, private AclManager $aclManager, - private SystemSettingsAccount $systemSettingsAccount, private ConfigDataProvider $configDataProvider, ) {} @@ -222,12 +219,6 @@ class SendingAccountProvider return; } - if ($this->config->get('smtpServer')) { - $this->system = $this->systemSettingsAccount; - - return; - } - $entity = $this->entityManager ->getRDBRepositoryByClass(InboundEmailEntity::class) ->where([ diff --git a/application/Espo/Core/Mail/Account/SystemSettingsAccount.php b/application/Espo/Core/Mail/Account/SystemSettingsAccount.php deleted file mode 100644 index f4709a9216..0000000000 --- a/application/Espo/Core/Mail/Account/SystemSettingsAccount.php +++ /dev/null @@ -1,221 +0,0 @@ -. - * - * 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\ConfigDataProvider; -use Espo\Core\Mail\Exceptions\NoSmtp; -use Espo\Core\Mail\SmtpParams; -use Espo\Core\Utils\Config; -use Espo\Entities\Email; -use Espo\Entities\Settings; - -class SystemSettingsAccount implements Account -{ - public function __construct( - private Config $config, - private ConfigDataProvider $configDataProvider, - ) {} - - public function updateFetchData(FetchData $fetchData): void {} - - public function getConnectedAt(): ?DateTime - { - return null; - } - - public function updateConnectedAt(): void - {} - - public function relateEmail(Email $email): void {} - - public function getPortionLimit(): int - { - return 0; - } - - public function isAvailableForFetching(): bool - { - return false; - } - - public function getEmailAddress(): ?string - { - return $this->configDataProvider->getSystemOutboundAddress(); - } - - public function getAssignedUser(): ?Link - { - return null; - } - - public function getUser(): ?Link - { - return null; - } - - public function getUsers(): LinkMultiple - { - return LinkMultiple::create(); - } - - public function getTeams(): LinkMultiple - { - return LinkMultiple::create(); - } - - public function keepFetchedEmailsUnread(): bool - { - return false; - } - - public function getFetchData(): FetchData - { - return FetchData::fromRaw((object) []); - } - - public function getFetchSince(): ?Date - { - return null; - } - - public function getEmailFolder(): ?Link - { - return null; - } - - public function getGroupEmailFolder(): ?Link - { - return null; - } - - public function getMonitoredFolderList(): array - { - return []; - } - - public function getId(): ?string - { - return null; - } - - public function getEntityType(): string - { - return Settings::ENTITY_TYPE; - } - - public function getHost(): ?string - { - return null; - } - - public function getPort(): ?int - { - return null; - } - - public function getUsername(): ?string - { - return null; - } - - public function getPassword(): ?string - { - return null; - } - - public function getSecurity(): ?string - { - return null; - } - - /** - * @return ?class-string - */ - public function getImapHandlerClassName(): ?string - { - return null; - } - - public function getSentFolder(): ?string - { - return null; - } - - public function isAvailableForSending(): bool - { - return (bool) $this->config->get('smtpServer'); - } - - public function storeSentEmails(): bool - { - return false; - } - - /** - * @throws NoSmtp - */ - public function getSmtpParams(): ?SmtpParams - { - $host = $this->config->get('smtpServer'); - $port = $this->config->get('smtpPort'); - - if (!$host) { - throw new NoSmtp("No system SMTP settings."); - } - - if (!$port) { - throw new NoSmtp("No system SMTP port."); - } - - $params = SmtpParams::create($host, $port) - ->withSecurity($this->config->get('smtpSecurity')) - ->withAuth($this->config->get('smtpAuth')); - - if ($params->useAuth()) { - $password = $this->config->get('smtpPassword'); - - $params = $params - ->withUsername($this->config->get('smtpUsername')) - ->withPassword($password) - ->withAuthMechanism($this->config->get('smtpAuthMechanism') ?? 'login'); - } - - return $params; - } - - public function getImapParams(): ?ImapParams - { - return null; - } -} diff --git a/application/Espo/Core/Upgrades/Migrations/V9_1/AfterUpgrade.php b/application/Espo/Core/Upgrades/Migrations/V9_1/AfterUpgrade.php index 87a661e7ac..ea83608b4b 100644 --- a/application/Espo/Core/Upgrades/Migrations/V9_1/AfterUpgrade.php +++ b/application/Espo/Core/Upgrades/Migrations/V9_1/AfterUpgrade.php @@ -31,10 +31,16 @@ namespace Espo\Core\Upgrades\Migrations\V9_1; use Espo\Core\ORM\Repository\Option\SaveOption; use Espo\Core\Upgrades\Migration\Script; +use Espo\Core\Utils\Config; +use Espo\Core\Utils\Crypt; use Espo\Core\Utils\Metadata; use Espo\Core\Utils\ObjectUtil; +use Espo\Core\Utils\SystemUser; +use Espo\Entities\InboundEmail; use Espo\Modules\Crm\Entities\KnowledgeBaseArticle; use Espo\ORM\EntityManager; +use Espo\ORM\Query\Part\Condition; +use Espo\ORM\Query\Part\Expression; use Espo\Tools\Email\Util; use stdClass; @@ -43,12 +49,17 @@ class AfterUpgrade implements Script public function __construct( private EntityManager $entityManager, private Metadata $metadata, + private Config $config, + private Config\ConfigWriter $configWriter, + private Crypt $crypt, + private SystemUser $systemUser, ) {} public function run(): void { $this->processKbArticles(); $this->processDynamicLogicMetadata(); + $this->processGroupEmailAccount(); } private function processKbArticles(): void @@ -104,4 +115,66 @@ class AfterUpgrade implements Script $this->metadata->saveCustom('clientDefs', $scope, $customClientDefs); } } + + private function processGroupEmailAccount(): void + { + if (!$this->config->get('smtpServer')) { + return; + } + + $outboundEmailFromAddress = $this->config->get('outboundEmailFromAddress'); + + if (!$outboundEmailFromAddress) { + return; + } + + $groupAccount = $this->entityManager + ->getRDBRepositoryByClass(InboundEmail::class) + ->where([ + 'status' => InboundEmail::STATUS_ACTIVE, + 'useSmtp' => true, + ]) + ->where( + Condition::equal( + Expression::lowerCase( + Expression::column('emailAddress') + ), + strtolower($outboundEmailFromAddress) + ) + ) + ->findOne(); + + $this->configWriter->set('smtpServer', null); + + if ($groupAccount) { + $this->configWriter->save(); + + return; + } + + $password = $this->config->get('smtpPassword'); + + $groupAccount = $this->entityManager->getRDBRepositoryByClass(InboundEmail::class)->getNew(); + + $groupAccount->setMultiple([ + 'emailAddress' => $outboundEmailFromAddress, + 'name' => $outboundEmailFromAddress . ' (system)', + 'useImap' => false, + 'useSmtp' => true, + 'smtpHost' => $this->config->get('smtpServer'), + 'smtpPort' => $this->config->get('smtpPort'), + 'smtpAuth' => $this->config->get('smtpAuth'), + 'smtpAuthMechanism' => $this->config->get('smtpAuthMechanism') ?? 'login', + 'fromName' => $this->config->get('outboundEmailFromName'), + 'smtpUsername' => $this->config->get('smtpUsername'), + 'smtpPassword' => $password !== null ? $this->crypt->encrypt($password) : null, + ]); + + $this->entityManager->saveEntity($groupAccount, [ + SaveOption::SKIP_HOOKS => true, + SaveOption::CREATED_BY_ID => $this->systemUser->getId(), + ]); + + $this->configWriter->save(); + } } diff --git a/application/Espo/Resources/i18n/en_US/Settings.json b/application/Espo/Resources/i18n/en_US/Settings.json index ecb50444fb..be738d7744 100644 --- a/application/Espo/Resources/i18n/en_US/Settings.json +++ b/application/Espo/Resources/i18n/en_US/Settings.json @@ -15,15 +15,9 @@ "companyLogo": "Company Logo", "smsProvider": "SMS Provider", "outboundSmsFromNumber": "SMS From Number", - "smtpServer": "Server", - "smtpPort": "Port", - "smtpAuth": "Auth", - "smtpSecurity": "Security", - "smtpUsername": "Username", "emailAddress": "Email", - "smtpPassword": "Password", "outboundEmailFromName": "From Name", - "outboundEmailFromAddress": "From Address", + "outboundEmailFromAddress": "System Email Address", "outboundEmailIsShared": "Is Shared", "emailAddressLookupEntityTypeList": "Email address look-up scopes", "emailAddressSelectEntityTypeList": "Email address select scopes", @@ -230,8 +224,7 @@ "emailAddressLookupEntityTypeList": "For email address autocomplete.", "emailAddressSelectEntityTypeList": "Entity types available when searching for an email address from a modal.", "emailNotificationsDelay": "A message can be edited within the specified timeframe before the notification is sent.", - "outboundEmailFromAddress": "The system email address.", - "smtpServer": "If empty, then Group Email Account with the corresponding email address will be used.", + "outboundEmailFromAddress": "System emails will be sent from this email address. A [group email account](#InboundEmail) with the same email address must be set up and properly configured to send emails.", "busyRangesEntityList": "What will be taken into account when showing busy time ranges in scheduler & timeline.", "massEmailVerp": "Variable envelope return path. For better handling of bounced messages. Make sure that your SMTP provider supports it.", "recordsPerPage": "Number of records initially displayed in list views.", diff --git a/application/Espo/Resources/layouts/Settings/outboundEmails.json b/application/Espo/Resources/layouts/Settings/outboundEmails.json index 4034e639b9..69b90a5524 100644 --- a/application/Espo/Resources/layouts/Settings/outboundEmails.json +++ b/application/Espo/Resources/layouts/Settings/outboundEmails.json @@ -8,15 +8,6 @@ [false, {"name": "emailAddressSelectEntityTypeList"}] ] }, - { - "label": "SMTP", - "rows": [ - [{"name": "smtpServer"}, {"name": "smtpPort"}], - [{"name": "smtpAuth"}, {"name": "smtpSecurity"}], - [{"name": "smtpUsername"}, {"name": "testSend", "customLabel": null, "view": "views/outbound-email/fields/test-send"}], - [{"name": "smtpPassword"}, false] - ] - }, { "label": "Mass Email", "rows": [ diff --git a/client/src/views/email-account/fields/test-send.js b/client/src/views/email-account/fields/test-send.js index f6e8aeed95..b4abe8c6ed 100644 --- a/client/src/views/email-account/fields/test-send.js +++ b/client/src/views/email-account/fields/test-send.js @@ -26,9 +26,26 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -import TestSendView from 'views/outbound-email/fields/test-send'; +import BaseFieldView from 'views/fields/base'; -export default class extends TestSendView { +export default class EmailAccountTestSendFieldView extends BaseFieldView { + + templateContent = ` + + ` + + setup() { + super.setup(); + + this.addActionHandler('sendTestEmail', () => this.send()); + } + + fetch() { + return {}; + } checkAvailability() { if (this.model.get('smtpHost')) { @@ -48,6 +65,96 @@ export default class extends TestSendView { }); } + /** + * @protected + */ + enableButton() { + this.$el.find('button').removeClass('disabled').removeAttr('disabled'); + } + + /** + * @protected + */ + disabledButton() { + this.$el.find('button').addClass('disabled').attr('disabled', 'disabled'); + } + + /** + * @private + */ + send() { + const data = this.getSmtpData(); + + this.createView('popup', 'views/outbound-email/modals/test-send', { + emailAddress: this.getUser().get('emailAddress'), + }).then(view => { + view.render(); + + this.listenToOnce(view, 'send', (emailAddress) => { + this.disabledButton(); + + data.emailAddress = emailAddress; + + Espo.Ui.notify(this.translate('Sending...')); + + view.close(); + + Espo.Ajax.postRequest('Email/sendTest', data) + .then(() => { + this.enableButton(); + + Espo.Ui.success(this.translate('testEmailSent', 'messages', 'Email')); + }) + .catch(xhr => { + let reason = xhr.getResponseHeader('X-Status-Reason') || ''; + + reason = reason + .replace(/ $/, '') + .replace(/,$/, ''); + + let msg = this.translate('Error'); + + if (xhr.status !== 200) { + msg += ' ' + xhr.status; + } + + if (xhr.responseText) { + try { + const data = /** @type {Record} */JSON.parse(xhr.responseText); + + if (data.messageTranslation) { + this.enableButton(); + + return; + } + + reason = data.message || reason; + } + catch (e) { + this.enableButton(); + + console.error('Could not parse error response body.'); + + return; + } + } + + if (reason) { + msg += ': ' + reason; + } + + Espo.Ui.error(msg, true); + console.error(msg); + + xhr.errorIsHandled = true; + + this.enableButton(); + } + ); + }); + }); + } + getSmtpData() { return { 'server': this.model.get('smtpHost'), diff --git a/client/src/views/outbound-email/fields/test-send.js b/client/src/views/outbound-email/fields/test-send.js deleted file mode 100644 index 993345ec00..0000000000 --- a/client/src/views/outbound-email/fields/test-send.js +++ /dev/null @@ -1,175 +0,0 @@ -/************************************************************************ - * This file is part of EspoCRM. - * - * EspoCRM – Open Source CRM application. - * Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko - * 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 . - * - * 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. - ************************************************************************/ - -import BaseFieldView from 'views/fields/base'; - -export default class extends BaseFieldView { - - templateContent = - '' - - setup() { - super.setup(); - - this.addActionHandler('sendTestEmail', () => this.send()); - } - - fetch() { - return {}; - } - - /** - * @protected - */ - checkAvailability() { - if (this.model.get('smtpServer')) { - this.$el.find('button').removeClass('hidden'); - } else { - this.$el.find('button').addClass('hidden'); - } - } - - afterRender() { - this.checkAvailability(); - - this.stopListening(this.model, 'change:smtpServer'); - - this.listenTo(this.model, 'change:smtpServer', () => { - this.checkAvailability(); - }); - } - - /** - * @protected - * @return {Record} - */ - getSmtpData() { - return { - 'server': this.model.get('smtpServer'), - 'port': this.model.get('smtpPort'), - 'auth': this.model.get('smtpAuth'), - 'security': this.model.get('smtpSecurity'), - 'username': this.model.get('smtpUsername'), - 'password': this.model.get('smtpPassword') || null, - 'fromName': this.model.get('outboundEmailFromName'), - 'fromAddress': this.model.get('outboundEmailFromAddress'), - 'type': 'outboundEmail', - }; - } - - /** - * @protected - */ - enableButton() { - this.$el.find('button').removeClass('disabled').removeAttr('disabled'); - } - - /** - * @protected - */ - disabledButton() { - this.$el.find('button').addClass('disabled').attr('disabled', 'disabled'); - } - - /** - * @private - */ - send() { - const data = this.getSmtpData(); - - this.createView('popup', 'views/outbound-email/modals/test-send', { - emailAddress: this.getUser().get('emailAddress') - }, (view) => { - view.render(); - - this.listenToOnce(view, 'send', (emailAddress) => { - this.disabledButton(); - - data.emailAddress = emailAddress; - - this.notify('Sending...'); - - view.close(); - - Espo.Ajax.postRequest('Email/sendTest', data) - .then(() => { - this.enableButton(); - - Espo.Ui.success(this.translate('testEmailSent', 'messages', 'Email')); - }) - .catch(xhr => { - let reason = xhr.getResponseHeader('X-Status-Reason') || ''; - - reason = reason - .replace(/ $/, '') - .replace(/,$/, ''); - - let msg = this.translate('Error'); - - if (xhr.status !== 200) { - msg += ' ' + xhr.status; - } - - if (xhr.responseText) { - try { - const data = /** @type {Record} */JSON.parse(xhr.responseText); - - if (data.messageTranslation) { - this.enableButton(); - - return; - } - - reason = data.message || reason; - } - catch (e) { - this.enableButton(); - - console.error('Could not parse error response body.'); - - return; - } - } - - if (reason) { - msg += ': ' + reason; - } - - Espo.Ui.error(msg, true); - console.error(msg); - - xhr.errorIsHandled = true; - - this.enableButton(); - } - ); - }); - }); - } -} diff --git a/tests/unit/testData/Utils/Config/config.php b/tests/unit/testData/Utils/Config/config.php index 846ec89553..09abde57bd 100644 --- a/tests/unit/testData/Utils/Config/config.php +++ b/tests/unit/testData/Utils/Config/config.php @@ -5,7 +5,7 @@ return array ( 'removeOption' => 'Test', 'testOption' => 'Another Wrong Value', 'testOption2' => 'Test2', - 'database' => + 'database' => array ( 'driver' => 'pdo_mysql', 'host' => 'localhost', @@ -24,16 +24,16 @@ return array ( 'weekStart' => 1, 'thousandSeparator' => ',', 'decimalMark' => '.', - 'currencyList' => + 'currencyList' => array ( 0 => 'USD', 1 => 'EUR', ), 'defaultCurrency' => 'USD', - 'currency' => + 'currency' => array ( 'base' => 'USD', - 'rate' => + 'rate' => array ( 'EUR' => '1.37', ), @@ -41,38 +41,32 @@ return array ( 'outboundEmailIsShared' => true, 'outboundEmailFromName' => 'EspoCRM', 'outboundEmailFromAddress' => '', - 'smtpServer' => '', - 'smtpPort' => 25, - 'smtpAuth' => true, - 'smtpSecurity' => '', - 'smtpUsername' => '', - 'smtpPassword' => '', - 'languageList' => + 'languageList' => array ( 0 => 'en_US', ), 'language' => 'en_US', - 'logger' => + 'logger' => array ( 'path' => 'data/logs/espo.log', 'level' => 'INFO', 'isRotate' => true, 'maxRotateFiles' => 5, ), - 'defaultPermissions' => + 'defaultPermissions' => array ( 'dir' => '0775', 'file' => '0664', 'user' => '', 'group' => '', ), - 'cron' => + 'cron' => array ( 'maxJobNumber' => 15, 'jobPeriod' => 7800, 'minExecutionTime' => 50, ), - 'globalSearchEntityList' => + 'globalSearchEntityList' => array ( 0 => 'Account', 1 => 'Contact', @@ -80,7 +74,7 @@ return array ( 3 => 'Prospect', 4 => 'Opportunity', ), - 'tabList' => + 'tabList' => array ( 0 => 'Contact', 1 => 'Account', @@ -93,7 +87,7 @@ return array ( 8 => 'Case', 9 => 'Prospect', ), - 'quickCreateList' => + 'quickCreateList' => array ( 0 => 'Account', 1 => 'Contact', @@ -108,4 +102,4 @@ return array ( 'isInstalled' => true, ); -?> \ No newline at end of file +?>