diff --git a/lib/API/JSON/v1/Settings.php b/lib/API/JSON/v1/Settings.php index c074af2658..7b805cfefe 100644 --- a/lib/API/JSON/v1/Settings.php +++ b/lib/API/JSON/v1/Settings.php @@ -75,6 +75,25 @@ class Settings extends APIEndpoint { } } + public function setAuthorizedFromAddress($data = []) { + $address = $data['address'] ?? null; + if (!$address) { + return $this->badRequest([ + APIError::BAD_REQUEST => WPFunctions::get()->__('No email address specified.', 'mailpoet'), + ]); + } + $address = trim($address); + + try { + $this->authorizedEmailsController->setFromEmailAddress($address); + } catch (\InvalidArgumentException $e) { + return $this->badRequest([ + APIError::UNAUTHORIZED => WPFunctions::get()->__('Can’t use this email yet! Please authorize it first.', 'mailpoet'), + ]); + } + return $this->successResponse(); + } + private function onSettingsChange($oldSettings, $newSettings) { // Recalculate inactive subscribers $oldInactivationInterval = $oldSettings['deactivate_subscriber_after_inactive_days']; diff --git a/lib/Mailer/Mailer.php b/lib/Mailer/Mailer.php index 2f59f11e11..856d1e4824 100644 --- a/lib/Mailer/Mailer.php +++ b/lib/Mailer/Mailer.php @@ -2,6 +2,7 @@ namespace MailPoet\Mailer; +use MailPoet\DI\ContainerWrapper; use MailPoet\Mailer\Methods\AmazonSES; use MailPoet\Mailer\Methods\ErrorMappers\AmazonSESMapper; use MailPoet\Mailer\Methods\ErrorMappers\MailPoetMapper; @@ -13,7 +14,6 @@ use MailPoet\Mailer\Methods\PHPMail; use MailPoet\Mailer\Methods\SendGrid; use MailPoet\Mailer\Methods\SMTP; use MailPoet\Services\AuthorizedEmailsController; -use MailPoet\Services\Bridge; use MailPoet\Settings\SettingsController; class Mailer { @@ -74,7 +74,7 @@ class Mailer { $this->sender, $this->replyTo, new MailPoetMapper(), - new AuthorizedEmailsController($this->settings, new Bridge) + ContainerWrapper::getInstance()->get(AuthorizedEmailsController::class) ); break; case self::METHOD_SENDGRID: diff --git a/lib/Services/AuthorizedEmailsController.php b/lib/Services/AuthorizedEmailsController.php index 52180a41bd..9fc36b2fbb 100644 --- a/lib/Services/AuthorizedEmailsController.php +++ b/lib/Services/AuthorizedEmailsController.php @@ -2,9 +2,11 @@ namespace MailPoet\Services; +use MailPoet\Mailer\Mailer; use MailPoet\Mailer\MailerError; use MailPoet\Mailer\MailerLog; use MailPoet\Models\Newsletter; +use MailPoet\Newsletter\NewslettersRepository; use MailPoet\Settings\SettingsController; class AuthorizedEmailsController { @@ -16,15 +18,43 @@ class AuthorizedEmailsController { /** @var SettingsController */ private $settings; + /** @var NewslettersRepository */ + private $newslettersRepository; + private $automaticEmailTypes = [ Newsletter::TYPE_WELCOME, Newsletter::TYPE_NOTIFICATION, Newsletter::TYPE_AUTOMATIC, ]; - public function __construct(SettingsController $settingsController, Bridge $bridge) { + public function __construct( + SettingsController $settingsController, + Bridge $bridge, + NewslettersRepository $newslettersRepository + ) { $this->settings = $settingsController; $this->bridge = $bridge; + $this->newslettersRepository = $newslettersRepository; + } + + public function setFromEmailAddress(string $address) { + $authorizedEmails = array_map('strtolower', $this->bridge->getAuthorizedEmailAddresses() ?: []); + $isAuthorized = $this->validateAuthorizedEmail($authorizedEmails, $address); + if (!$isAuthorized) { + throw new \InvalidArgumentException("Email address '$address' is not authorized"); + } + + // update FROM address in settings & all scheduled and active emails + $this->settings->set('sender.address', $address); + $result = $this->validateAddressesInScheduledAndAutomaticEmails($authorizedEmails); + foreach ($result['invalid_senders_in_newsletters'] ?? [] as $item) { + $newsletter = $this->newslettersRepository->findOneById((int)$item['newsletter_id']); + if ($newsletter) { + $newsletter->setSenderAddress($address); + } + } + $this->newslettersRepository->flush(); + $this->settings->set(self::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING, null); } public function checkAuthorizedEmailAddresses() { diff --git a/tests/integration/Services/AuthorizedEmailsControllerTest.php b/tests/integration/Services/AuthorizedEmailsControllerTest.php index e95922ab0a..4660c760c8 100644 --- a/tests/integration/Services/AuthorizedEmailsControllerTest.php +++ b/tests/integration/Services/AuthorizedEmailsControllerTest.php @@ -3,10 +3,13 @@ namespace MailPoet\Test\Services; use Codeception\Stub\Expected; +use InvalidArgumentException; +use MailPoet\Entities\NewsletterEntity; use MailPoet\Mailer\Mailer; use MailPoet\Mailer\MailerError; use MailPoet\Mailer\MailerLog; use MailPoet\Models\Newsletter; +use MailPoet\Newsletter\NewslettersRepository; use MailPoet\Services\AuthorizedEmailsController; use MailPoet\Services\Bridge; use MailPoet\Settings\SettingsController; @@ -155,6 +158,77 @@ class AuthorizedEmailsControllerTest extends \MailPoetTest { expect($error['invalid_senders_in_newsletters'][0]['subject'])->equals('Subject'); } + public function testItSetsFromAddressInSettings() { + $this->settings->set('sender.address', ''); + $controller = $this->getController(['authorized@email.com']); + $controller->setFromEmailAddress('authorized@email.com'); + expect($this->settings->get('sender.address'))->same('authorized@email.com'); + } + + public function testItSetsFromAddressInScheduledEmails() { + $newsletter = new NewsletterEntity(); + $newsletter->setSubject('Subject'); + $newsletter->setType(NewsletterEntity::TYPE_STANDARD); + $newsletter->setStatus(NewsletterEntity::STATUS_SCHEDULED); + $newsletter->setSenderAddress('invalid@email.com'); + $this->entityManager->persist($newsletter); + $this->entityManager->flush(); + + $this->settings->set('sender.address', ''); + $controller = $this->getController(['authorized@email.com']); + $controller->setFromEmailAddress('authorized@email.com'); + expect($newsletter->getSenderAddress())->same('authorized@email.com'); + + // refresh from DB and check again + $this->entityManager->refresh($newsletter); + expect($newsletter->getSenderAddress())->same('authorized@email.com'); + } + + public function testItSetsFromAddressInAutomaticEmails() { + $newsletter = new NewsletterEntity(); + $newsletter->setSubject('Subject'); + $newsletter->setType(NewsletterEntity::TYPE_AUTOMATIC); + $newsletter->setStatus(NewsletterEntity::STATUS_ACTIVE); + $newsletter->setSenderAddress('invalid@email.com'); + $this->entityManager->persist($newsletter); + $this->entityManager->flush(); + + $this->settings->set('sender.address', ''); + $controller = $this->getController(['authorized@email.com']); + $controller->setFromEmailAddress('authorized@email.com'); + expect($newsletter->getSenderAddress())->same('authorized@email.com'); + + // refresh from DB and check again + $this->entityManager->refresh($newsletter); + expect($newsletter->getSenderAddress())->same('authorized@email.com'); + } + + public function testItDoesntSetFromAddressForSentEmails() { + $newsletter = new NewsletterEntity(); + $newsletter->setSubject('Subject'); + $newsletter->setType(NewsletterEntity::TYPE_STANDARD); + $newsletter->setStatus(NewsletterEntity::STATUS_SENT); + $newsletter->setSenderAddress('invalid@email.com'); + $this->entityManager->persist($newsletter); + $this->entityManager->flush(); + + $this->settings->set('sender.address', ''); + $controller = $this->getController(['authorized@email.com']); + $controller->setFromEmailAddress('authorized@email.com'); + expect($newsletter->getSenderAddress())->same('invalid@email.com'); + + // refresh from DB and check again + $this->entityManager->refresh($newsletter); + expect($newsletter->getSenderAddress())->same('invalid@email.com'); + } + + public function testSetsFromAddressThrowsForUnauthorizedEmail() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Email address 'invalid@email.com' is not authorized"); + $controller = $this->getController(['authorized@email.com']); + $controller->setFromEmailAddress('invalid@email.com'); + } + private function setMailPoetSendingMethod() { $this->settings->set( Mailer::MAILER_CONFIG_SETTING_NAME, @@ -172,7 +246,8 @@ class AuthorizedEmailsControllerTest extends \MailPoetTest { $getEmailsExpectaton = Expected::once($authorizedEmails); } $bridgeMock = $this->make(Bridge::class, ['getAuthorizedEmailAddresses' => $getEmailsExpectaton]); - return new AuthorizedEmailsController($this->settings, $bridgeMock); + $newslettersRepository = $this->diContainer->get(NewslettersRepository::class); + return new AuthorizedEmailsController($this->settings, $bridgeMock, $newslettersRepository); } public function _after() {