remove system email account

This commit is contained in:
Yuri Kuznetsov
2025-04-12 13:00:22 +03:00
parent b2ce89754f
commit 11c6b79a4d
8 changed files with 196 additions and 443 deletions

View File

@@ -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([

View File

@@ -1,221 +0,0 @@
<?php
/************************************************************************
* 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 <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\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<object>
*/
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;
}
}

View File

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

View File

@@ -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.",

View File

@@ -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": [

View File

@@ -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 = `
<button
class="btn btn-default hidden"
data-action="sendTestEmail"
>{{translate 'Send Test Email' scope='Email'}}</button>
`
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'),

View File

@@ -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 <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.
************************************************************************/
import BaseFieldView from 'views/fields/base';
export default class extends BaseFieldView {
templateContent =
'<button class="btn btn-default hidden" data-action="sendTestEmail">'+
'{{translate \'Send Test Email\' scope=\'Email\'}}</button>'
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();
}
);
});
});
}
}

View File

@@ -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,
);
?>
?>