Bridge
5 days ago
Release
2 years ago
AuthorizedEmailsController.php
2 months ago
AuthorizedSenderDomainController.php
5 months ago
Bridge.php
4 months ago
CongratulatoryMssEmailController.php
4 months ago
SubscribersCountReporter.php
3 years ago
Validator.php
3 years ago
index.php
3 years ago
AuthorizedEmailsController.php
286 lines
| 1 | <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing |
| 2 | |
| 3 | namespace MailPoet\Services; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use MailPoet\Entities\NewsletterEntity; |
| 9 | use MailPoet\Mailer\Mailer; |
| 10 | use MailPoet\Mailer\MailerError; |
| 11 | use MailPoet\Mailer\MailerLog; |
| 12 | use MailPoet\Newsletter\NewslettersRepository; |
| 13 | use MailPoet\Services\Bridge\API; |
| 14 | use MailPoet\Settings\SettingsController; |
| 15 | use MailPoet\Util\Helpers; |
| 16 | use MailPoet\WP\Functions as WPFunctions; |
| 17 | |
| 18 | class AuthorizedEmailsController { |
| 19 | const AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING = 'authorized_emails_addresses_check'; |
| 20 | |
| 21 | const AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_AUTHORIZED = 'authorized'; |
| 22 | const AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_PENDING = 'pending'; |
| 23 | const AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_ALL = 'all'; |
| 24 | const AUTHORIZED_EMAIL_ERROR_ALREADY_AUTHORIZED = 'Email address is already authorized'; |
| 25 | const AUTHORIZED_EMAIL_ERROR_PENDING_CONFIRMATION = 'Email address is pending confirmation'; |
| 26 | |
| 27 | const AUTHORIZED_EMAILS_KEY = 'mailpoet_authorized_email_addresses'; |
| 28 | |
| 29 | /** @var Bridge */ |
| 30 | private $bridge; |
| 31 | |
| 32 | /** @var SettingsController */ |
| 33 | private $settings; |
| 34 | |
| 35 | /** @var NewslettersRepository */ |
| 36 | private $newslettersRepository; |
| 37 | |
| 38 | /** @var AuthorizedSenderDomainController */ |
| 39 | private $senderDomainController; |
| 40 | |
| 41 | /** @var WPFunctions */ |
| 42 | private $wp; |
| 43 | |
| 44 | private $automaticEmailTypes = [ |
| 45 | NewsletterEntity::TYPE_WELCOME, |
| 46 | NewsletterEntity::TYPE_NOTIFICATION, |
| 47 | NewsletterEntity::TYPE_AUTOMATIC, |
| 48 | NewsletterEntity::TYPE_AUTOMATION, |
| 49 | NewsletterEntity::TYPE_AUTOMATION_TRANSACTIONAL, |
| 50 | ]; |
| 51 | |
| 52 | public function __construct( |
| 53 | SettingsController $settingsController, |
| 54 | Bridge $bridge, |
| 55 | NewslettersRepository $newslettersRepository, |
| 56 | AuthorizedSenderDomainController $senderDomainController, |
| 57 | WPFunctions $wp |
| 58 | ) { |
| 59 | $this->settings = $settingsController; |
| 60 | $this->bridge = $bridge; |
| 61 | $this->newslettersRepository = $newslettersRepository; |
| 62 | $this->senderDomainController = $senderDomainController; |
| 63 | $this->wp = $wp; |
| 64 | } |
| 65 | |
| 66 | public function setFromEmailAddress(string $address) { |
| 67 | // Bundled subscription users who are small senders skip authorization |
| 68 | if ($this->senderDomainController->shouldSkipAuthorization()) { |
| 69 | $this->settings->set('sender.address', $address); |
| 70 | $this->settings->set(self::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING, null); |
| 71 | return; |
| 72 | } |
| 73 | |
| 74 | $authorizedEmails = $this->getAuthorizedEmailAddresses() ?: []; |
| 75 | $verifiedDomains = $this->senderDomainController->getVerifiedSenderDomainsIgnoringCache(); |
| 76 | $isAuthorized = $this->validateAuthorizedEmail($authorizedEmails, $address); |
| 77 | |
| 78 | $emailDomainIsVerified = $this->validateEmailDomainIsVerified($verifiedDomains, $address); |
| 79 | |
| 80 | if (!$emailDomainIsVerified && !$isAuthorized) { |
| 81 | throw new \InvalidArgumentException("Email address '$address' is not authorized"); |
| 82 | } |
| 83 | |
| 84 | // update FROM address in settings & all scheduled and active emails |
| 85 | $this->settings->set('sender.address', $address); |
| 86 | $result = $this->validateAddressesInScheduledAndAutomaticEmails($authorizedEmails, $verifiedDomains); |
| 87 | foreach ($result['invalid_senders_in_newsletters'] ?? [] as $item) { |
| 88 | $newsletter = $this->newslettersRepository->findOneById((int)$item['newsletter_id']); |
| 89 | if ($newsletter) { |
| 90 | $newsletter->setSenderAddress($address); |
| 91 | } |
| 92 | } |
| 93 | $this->newslettersRepository->flush(); |
| 94 | $this->settings->set(self::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING, null); |
| 95 | } |
| 96 | |
| 97 | public function getAuthorizedEmailAddresses(string $type = 'authorized'): array { |
| 98 | $data = $this->bridge->getAuthorizedEmailAddresses(); |
| 99 | if (empty($data)) { |
| 100 | // API is potentially down, fallback to cache |
| 101 | $data = $this->wp->getTransient(self::AUTHORIZED_EMAILS_KEY); |
| 102 | } else { |
| 103 | $this->wp->setTransient(self::AUTHORIZED_EMAILS_KEY, $data, WEEK_IN_SECONDS); |
| 104 | } |
| 105 | |
| 106 | if ($data && $type === self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_ALL) { |
| 107 | return $data; |
| 108 | } |
| 109 | |
| 110 | return $data[$type] ?? []; |
| 111 | } |
| 112 | |
| 113 | public function createAuthorizedEmailAddress(string $email): array { |
| 114 | $allEmails = $this->getAuthorizedEmailAddresses(self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_ALL); |
| 115 | |
| 116 | $authorizedEmails = isset($allEmails[self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_AUTHORIZED]) ? $allEmails[self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_AUTHORIZED] : []; |
| 117 | $isAuthorized = $this->validateAuthorizedEmail($authorizedEmails, $email); |
| 118 | |
| 119 | if ($isAuthorized) { |
| 120 | throw new \InvalidArgumentException(self::AUTHORIZED_EMAIL_ERROR_ALREADY_AUTHORIZED); |
| 121 | } |
| 122 | |
| 123 | $pendingEmails = isset($allEmails[self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_PENDING]) ? $allEmails[self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_PENDING] : []; |
| 124 | $isPending = $this->validateAuthorizedEmail($pendingEmails, $email); |
| 125 | |
| 126 | if ($isPending) { |
| 127 | throw new \InvalidArgumentException(self::AUTHORIZED_EMAIL_ERROR_PENDING_CONFIRMATION); |
| 128 | } |
| 129 | |
| 130 | $response = $this->bridge->createAuthorizedEmailAddress($email); |
| 131 | if ($response['status'] === API::RESPONSE_STATUS_ERROR) { |
| 132 | throw new \InvalidArgumentException($response['message']); |
| 133 | } |
| 134 | |
| 135 | return $response; |
| 136 | } |
| 137 | |
| 138 | public function isEmailAddressAuthorized(string $email): bool { |
| 139 | $authorizedEmails = $this->getAuthorizedEmailAddresses() ?: []; |
| 140 | return $this->validateAuthorizedEmail($authorizedEmails, $email); |
| 141 | } |
| 142 | |
| 143 | public function checkAuthorizedEmailAddresses() { |
| 144 | if (!$this->bridge->isMailpoetSendingServiceEnabled()) { |
| 145 | $this->settings->set(self::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING, null); |
| 146 | $this->updateMailerLog(); |
| 147 | return null; |
| 148 | } |
| 149 | |
| 150 | // Bundled subscription users who are small senders skip authorization |
| 151 | if ($this->senderDomainController->shouldSkipAuthorization()) { |
| 152 | $this->settings->set(self::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING, null); |
| 153 | $this->updateMailerLog(); |
| 154 | return null; |
| 155 | } |
| 156 | |
| 157 | $authorizedEmails = $this->getAuthorizedEmailAddresses(); |
| 158 | // Keep previous check result for an invalid response from API |
| 159 | if (!$authorizedEmails) { |
| 160 | return null; |
| 161 | } |
| 162 | $authorizedEmails = array_map('strtolower', $authorizedEmails); |
| 163 | |
| 164 | $verifiedDomains = $this->senderDomainController->getVerifiedSenderDomainsIgnoringCache(); |
| 165 | |
| 166 | $result = []; |
| 167 | $result = $this->validateAddressesInSettings($authorizedEmails, $verifiedDomains, $result); |
| 168 | $result = $this->validateAddressesInScheduledAndAutomaticEmails($authorizedEmails, $verifiedDomains, $result); |
| 169 | $this->settings->set(self::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING, $result ?: null); |
| 170 | $this->updateMailerLog($result); |
| 171 | return $result; |
| 172 | } |
| 173 | |
| 174 | public function onSettingsSave($settings): ?array { |
| 175 | $senderAddressSet = !empty($settings['sender']['address']); |
| 176 | $mailpoetSendingMethodSet = ($settings[Mailer::MAILER_CONFIG_SETTING_NAME]['method'] ?? null) === Mailer::METHOD_MAILPOET; |
| 177 | if ($senderAddressSet || $mailpoetSendingMethodSet) { |
| 178 | return $this->checkAuthorizedEmailAddresses(); |
| 179 | } |
| 180 | return null; |
| 181 | } |
| 182 | |
| 183 | public function onNewsletterSenderAddressUpdate(NewsletterEntity $newsletter, ?string $oldSenderAddress = null) { |
| 184 | if ($newsletter->getSenderAddress() === $oldSenderAddress) { |
| 185 | return; |
| 186 | } |
| 187 | if ($newsletter->getType() === NewsletterEntity::TYPE_STANDARD && $newsletter->getStatus() === NewsletterEntity::STATUS_SCHEDULED) { |
| 188 | $this->checkAuthorizedEmailAddresses(); |
| 189 | } |
| 190 | if (in_array($newsletter->getType(), $this->automaticEmailTypes, true) && $newsletter->getStatus() === NewsletterEntity::STATUS_ACTIVE) { |
| 191 | $this->checkAuthorizedEmailAddresses(); |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | public function isSenderAddressValid(NewsletterEntity $newsletter, string $context = 'activation'): bool { |
| 196 | if (!in_array($newsletter->getType(), NewsletterEntity::CAMPAIGN_TYPES)) { |
| 197 | return true; |
| 198 | } |
| 199 | |
| 200 | $isAuthorizedDomainRequired = $context === 'activation' |
| 201 | ? $this->senderDomainController->isAuthorizedDomainRequiredForNewCampaigns() |
| 202 | : $this->senderDomainController->isAuthorizedDomainRequiredForExistingCampaigns(); |
| 203 | |
| 204 | if (!$isAuthorizedDomainRequired) { |
| 205 | return true; |
| 206 | } |
| 207 | |
| 208 | $verifiedDomains = $context === 'activation' |
| 209 | ? $this->senderDomainController->getVerifiedSenderDomainsIgnoringCache() |
| 210 | : $this->senderDomainController->getVerifiedSenderDomains(); |
| 211 | |
| 212 | // The shop is not returning data, so we allow sending and let the Sending Service block the campaign if needed. |
| 213 | if ($context === 'sending' && empty($verifiedDomains) && !$this->senderDomainController->isCacheAvailable()) { |
| 214 | return true; |
| 215 | } |
| 216 | |
| 217 | return $this->validateEmailDomainIsVerified($verifiedDomains, $newsletter->getSenderAddress()); |
| 218 | } |
| 219 | |
| 220 | private function validateAddressesInSettings($authorizedEmails, $verifiedDomains, $result = []) { |
| 221 | $defaultSenderAddress = $this->settings->get('sender.address'); |
| 222 | |
| 223 | if ($this->validateEmailDomainIsVerified($verifiedDomains, $defaultSenderAddress)) { |
| 224 | // allow sending from any email address in a verified domain |
| 225 | return $result; |
| 226 | } |
| 227 | |
| 228 | if (!$this->validateAuthorizedEmail($authorizedEmails, $defaultSenderAddress)) { |
| 229 | $result['invalid_sender_address'] = $defaultSenderAddress; |
| 230 | } |
| 231 | |
| 232 | return $result; |
| 233 | } |
| 234 | |
| 235 | private function validateAddressesInScheduledAndAutomaticEmails($authorizedEmails, $verifiedDomains, $result = []) { |
| 236 | $newsletters = $this->newslettersRepository->getScheduledStandardEmailsAndActiveAutomaticEmails($this->automaticEmailTypes); |
| 237 | |
| 238 | $invalidSendersInNewsletters = []; |
| 239 | foreach ($newsletters as $newsletter) { |
| 240 | if ($this->validateAuthorizedEmail($authorizedEmails, $newsletter->getSenderAddress())) { |
| 241 | continue; |
| 242 | } |
| 243 | if ($this->validateEmailDomainIsVerified($verifiedDomains, $newsletter->getSenderAddress())) { |
| 244 | // allow sending from any email address in a verified domain |
| 245 | continue; |
| 246 | } |
| 247 | $invalidSendersInNewsletters[] = [ |
| 248 | 'newsletter_id' => $newsletter->getId(), |
| 249 | 'subject' => $newsletter->getSubject(), |
| 250 | 'sender_address' => $newsletter->getSenderAddress(), |
| 251 | ]; |
| 252 | } |
| 253 | |
| 254 | if (!count($invalidSendersInNewsletters)) { |
| 255 | return $result; |
| 256 | } |
| 257 | |
| 258 | $result['invalid_senders_in_newsletters'] = $invalidSendersInNewsletters; |
| 259 | return $result; |
| 260 | } |
| 261 | |
| 262 | /** |
| 263 | * @param array|null $error |
| 264 | */ |
| 265 | private function updateMailerLog(?array $error = null) { |
| 266 | if ($error) { |
| 267 | return; |
| 268 | } |
| 269 | $mailerLogError = MailerLog::getError(); |
| 270 | if ($mailerLogError && $mailerLogError['operation'] === MailerError::OPERATION_AUTHORIZATION) { |
| 271 | MailerLog::resumeSending(); |
| 272 | } |
| 273 | } |
| 274 | |
| 275 | private function validateAuthorizedEmail($authorizedEmails = [], $email = '') { |
| 276 | $lowercaseAuthorizedEmails = array_map('strtolower', $authorizedEmails); |
| 277 | return in_array(strtolower($email), $lowercaseAuthorizedEmails, true); |
| 278 | } |
| 279 | |
| 280 | private function validateEmailDomainIsVerified(array $verifiedDomains = [], string $email = ''): bool { |
| 281 | $lowercaseVerifiedDomains = array_map('strtolower', $verifiedDomains); |
| 282 | $emailDomain = Helpers::extractEmailDomain($email); |
| 283 | return in_array($emailDomain, $lowercaseVerifiedDomains, true); |
| 284 | } |
| 285 | } |
| 286 |