init
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Services;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Entities\NewsletterEntity;
|
||||
use MailPoet\Mailer\Mailer;
|
||||
use MailPoet\Mailer\MailerError;
|
||||
use MailPoet\Mailer\MailerLog;
|
||||
use MailPoet\Newsletter\NewslettersRepository;
|
||||
use MailPoet\Services\Bridge\API;
|
||||
use MailPoet\Settings\SettingsController;
|
||||
use MailPoet\Util\Helpers;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class AuthorizedEmailsController {
|
||||
const AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING = 'authorized_emails_addresses_check';
|
||||
|
||||
const AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_AUTHORIZED = 'authorized';
|
||||
const AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_PENDING = 'pending';
|
||||
const AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_ALL = 'all';
|
||||
const AUTHORIZED_EMAIL_ERROR_ALREADY_AUTHORIZED = 'Email address is already authorized';
|
||||
const AUTHORIZED_EMAIL_ERROR_PENDING_CONFIRMATION = 'Email address is pending confirmation';
|
||||
|
||||
const AUTHORIZED_EMAILS_KEY = 'mailpoet_authorized_email_addresses';
|
||||
|
||||
/** @var Bridge */
|
||||
private $bridge;
|
||||
|
||||
/** @var SettingsController */
|
||||
private $settings;
|
||||
|
||||
/** @var NewslettersRepository */
|
||||
private $newslettersRepository;
|
||||
|
||||
/** @var AuthorizedSenderDomainController */
|
||||
private $senderDomainController;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
private $automaticEmailTypes = [
|
||||
NewsletterEntity::TYPE_WELCOME,
|
||||
NewsletterEntity::TYPE_NOTIFICATION,
|
||||
NewsletterEntity::TYPE_AUTOMATIC,
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
SettingsController $settingsController,
|
||||
Bridge $bridge,
|
||||
NewslettersRepository $newslettersRepository,
|
||||
AuthorizedSenderDomainController $senderDomainController,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->settings = $settingsController;
|
||||
$this->bridge = $bridge;
|
||||
$this->newslettersRepository = $newslettersRepository;
|
||||
$this->senderDomainController = $senderDomainController;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
public function setFromEmailAddress(string $address) {
|
||||
$authorizedEmails = $this->getAuthorizedEmailAddresses() ?: [];
|
||||
$verifiedDomains = $this->senderDomainController->getVerifiedSenderDomainsIgnoringCache();
|
||||
$isAuthorized = $this->validateAuthorizedEmail($authorizedEmails, $address);
|
||||
|
||||
$emailDomainIsVerified = $this->validateEmailDomainIsVerified($verifiedDomains, $address);
|
||||
|
||||
if (!$emailDomainIsVerified && !$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, $verifiedDomains);
|
||||
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 getAuthorizedEmailAddresses(string $type = 'authorized'): array {
|
||||
$data = $this->bridge->getAuthorizedEmailAddresses();
|
||||
if (empty($data)) {
|
||||
// API is potentially down, fallback to cache
|
||||
$data = $this->wp->getTransient(self::AUTHORIZED_EMAILS_KEY);
|
||||
} else {
|
||||
$this->wp->setTransient(self::AUTHORIZED_EMAILS_KEY, $data, WEEK_IN_SECONDS);
|
||||
}
|
||||
|
||||
if ($data && $type === self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_ALL) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
return $data[$type] ?? [];
|
||||
}
|
||||
|
||||
public function createAuthorizedEmailAddress(string $email): array {
|
||||
$allEmails = $this->getAuthorizedEmailAddresses(self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_ALL);
|
||||
|
||||
$authorizedEmails = isset($allEmails[self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_AUTHORIZED]) ? $allEmails[self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_AUTHORIZED] : [];
|
||||
$isAuthorized = $this->validateAuthorizedEmail($authorizedEmails, $email);
|
||||
|
||||
if ($isAuthorized) {
|
||||
throw new \InvalidArgumentException(self::AUTHORIZED_EMAIL_ERROR_ALREADY_AUTHORIZED);
|
||||
}
|
||||
|
||||
$pendingEmails = isset($allEmails[self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_PENDING]) ? $allEmails[self::AUTHORIZED_EMAIL_ADDRESSES_API_TYPE_PENDING] : [];
|
||||
$isPending = $this->validateAuthorizedEmail($pendingEmails, $email);
|
||||
|
||||
if ($isPending) {
|
||||
throw new \InvalidArgumentException(self::AUTHORIZED_EMAIL_ERROR_PENDING_CONFIRMATION);
|
||||
}
|
||||
|
||||
$response = $this->bridge->createAuthorizedEmailAddress($email);
|
||||
if ($response['status'] === API::RESPONSE_STATUS_ERROR) {
|
||||
throw new \InvalidArgumentException($response['message']);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function isEmailAddressAuthorized(string $email): bool {
|
||||
$authorizedEmails = $this->getAuthorizedEmailAddresses() ?: [];
|
||||
return $this->validateAuthorizedEmail($authorizedEmails, $email);
|
||||
}
|
||||
|
||||
public function checkAuthorizedEmailAddresses() {
|
||||
if (!$this->bridge->isMailpoetSendingServiceEnabled()) {
|
||||
$this->settings->set(self::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING, null);
|
||||
$this->updateMailerLog();
|
||||
return null;
|
||||
}
|
||||
|
||||
$authorizedEmails = $this->getAuthorizedEmailAddresses();
|
||||
// Keep previous check result for an invalid response from API
|
||||
if (!$authorizedEmails) {
|
||||
return null;
|
||||
}
|
||||
$authorizedEmails = array_map('strtolower', $authorizedEmails);
|
||||
|
||||
$verifiedDomains = $this->senderDomainController->getVerifiedSenderDomainsIgnoringCache();
|
||||
|
||||
$result = [];
|
||||
$result = $this->validateAddressesInSettings($authorizedEmails, $verifiedDomains, $result);
|
||||
$result = $this->validateAddressesInScheduledAndAutomaticEmails($authorizedEmails, $verifiedDomains, $result);
|
||||
$this->settings->set(self::AUTHORIZED_EMAIL_ADDRESSES_ERROR_SETTING, $result ?: null);
|
||||
$this->updateMailerLog($result);
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function onSettingsSave($settings): ?array {
|
||||
$senderAddressSet = !empty($settings['sender']['address']);
|
||||
$mailpoetSendingMethodSet = ($settings[Mailer::MAILER_CONFIG_SETTING_NAME]['method'] ?? null) === Mailer::METHOD_MAILPOET;
|
||||
if ($senderAddressSet || $mailpoetSendingMethodSet) {
|
||||
return $this->checkAuthorizedEmailAddresses();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function onNewsletterSenderAddressUpdate(NewsletterEntity $newsletter, string $oldSenderAddress = null) {
|
||||
if ($newsletter->getSenderAddress() === $oldSenderAddress) {
|
||||
return;
|
||||
}
|
||||
if ($newsletter->getType() === NewsletterEntity::TYPE_STANDARD && $newsletter->getStatus() === NewsletterEntity::STATUS_SCHEDULED) {
|
||||
$this->checkAuthorizedEmailAddresses();
|
||||
}
|
||||
if (in_array($newsletter->getType(), $this->automaticEmailTypes, true) && $newsletter->getStatus() === NewsletterEntity::STATUS_ACTIVE) {
|
||||
$this->checkAuthorizedEmailAddresses();
|
||||
}
|
||||
}
|
||||
|
||||
public function isSenderAddressValid(NewsletterEntity $newsletter, string $context = 'activation'): bool {
|
||||
if (!in_array($newsletter->getType(), NewsletterEntity::CAMPAIGN_TYPES)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$isAuthorizedDomainRequired = $context === 'activation'
|
||||
? $this->senderDomainController->isAuthorizedDomainRequiredForNewCampaigns()
|
||||
: $this->senderDomainController->isAuthorizedDomainRequiredForExistingCampaigns();
|
||||
|
||||
if (!$isAuthorizedDomainRequired) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$verifiedDomains = $context === 'activation'
|
||||
? $this->senderDomainController->getVerifiedSenderDomainsIgnoringCache()
|
||||
: $this->senderDomainController->getVerifiedSenderDomains();
|
||||
|
||||
// The shop is not returning data, so we allow sending and let the Sending Service block the campaign if needed.
|
||||
if ($context === 'sending' && empty($verifiedDomains) && !$this->senderDomainController->isCacheAvailable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->validateEmailDomainIsVerified($verifiedDomains, $newsletter->getSenderAddress());
|
||||
}
|
||||
|
||||
private function validateAddressesInSettings($authorizedEmails, $verifiedDomains, $result = []) {
|
||||
$defaultSenderAddress = $this->settings->get('sender.address');
|
||||
|
||||
if ($this->validateEmailDomainIsVerified($verifiedDomains, $defaultSenderAddress)) {
|
||||
// allow sending from any email address in a verified domain
|
||||
return $result;
|
||||
}
|
||||
|
||||
if (!$this->validateAuthorizedEmail($authorizedEmails, $defaultSenderAddress)) {
|
||||
$result['invalid_sender_address'] = $defaultSenderAddress;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function validateAddressesInScheduledAndAutomaticEmails($authorizedEmails, $verifiedDomains, $result = []) {
|
||||
$newsletters = $this->newslettersRepository->getScheduledStandardEmailsAndActiveAutomaticEmails($this->automaticEmailTypes);
|
||||
|
||||
$invalidSendersInNewsletters = [];
|
||||
foreach ($newsletters as $newsletter) {
|
||||
if ($this->validateAuthorizedEmail($authorizedEmails, $newsletter->getSenderAddress())) {
|
||||
continue;
|
||||
}
|
||||
if ($this->validateEmailDomainIsVerified($verifiedDomains, $newsletter->getSenderAddress())) {
|
||||
// allow sending from any email address in a verified domain
|
||||
continue;
|
||||
}
|
||||
$invalidSendersInNewsletters[] = [
|
||||
'newsletter_id' => $newsletter->getId(),
|
||||
'subject' => $newsletter->getSubject(),
|
||||
'sender_address' => $newsletter->getSenderAddress(),
|
||||
];
|
||||
}
|
||||
|
||||
if (!count($invalidSendersInNewsletters)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['invalid_senders_in_newsletters'] = $invalidSendersInNewsletters;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|null $error
|
||||
*/
|
||||
private function updateMailerLog(array $error = null) {
|
||||
if ($error) {
|
||||
return;
|
||||
}
|
||||
$mailerLogError = MailerLog::getError();
|
||||
if ($mailerLogError && $mailerLogError['operation'] === MailerError::OPERATION_AUTHORIZATION) {
|
||||
MailerLog::resumeSending();
|
||||
}
|
||||
}
|
||||
|
||||
private function validateAuthorizedEmail($authorizedEmails = [], $email = '') {
|
||||
$lowercaseAuthorizedEmails = array_map('strtolower', $authorizedEmails);
|
||||
return in_array(strtolower($email), $lowercaseAuthorizedEmails, true);
|
||||
}
|
||||
|
||||
private function validateEmailDomainIsVerified(array $verifiedDomains = [], string $email = ''): bool {
|
||||
$lowercaseVerifiedDomains = array_map('strtolower', $verifiedDomains);
|
||||
$emailDomain = Helpers::extractEmailDomain($email);
|
||||
return in_array($emailDomain, $lowercaseVerifiedDomains, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Services;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Mailer\Mailer;
|
||||
use MailPoet\Newsletter\Statistics\NewsletterStatisticsRepository;
|
||||
use MailPoet\Services\Bridge\API;
|
||||
use MailPoet\Settings\SettingsController;
|
||||
use MailPoet\Util\License\Features\Subscribers;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class AuthorizedSenderDomainController {
|
||||
const DOMAIN_STATUS_VERIFIED = 'verified';
|
||||
const DOMAIN_STATUS_PARTIALLY_VERIFIED = 'partially-verified';
|
||||
const DOMAIN_STATUS_UNVERIFIED = 'unverified';
|
||||
|
||||
const AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_CREATED = 'Sender domain exist';
|
||||
const AUTHORIZED_SENDER_DOMAIN_ERROR_NOT_CREATED = 'Sender domain does not exist';
|
||||
const AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_VERIFIED = 'Sender domain already verified';
|
||||
|
||||
const LOWER_LIMIT = 100;
|
||||
const UPPER_LIMIT = 200;
|
||||
|
||||
const INSTALLED_AFTER_NEW_RESTRICTIONS_OPTION = 'installed_after_new_domain_restrictions';
|
||||
|
||||
const SENDER_DOMAINS_KEY = 'mailpoet_sender_domains';
|
||||
|
||||
/** @var Bridge */
|
||||
private $bridge;
|
||||
|
||||
/** @var NewsletterStatisticsRepository */
|
||||
private $newsletterStatisticsRepository;
|
||||
|
||||
/** @var SettingsController */
|
||||
private $settingsController;
|
||||
|
||||
/** @var null|array Cached response for with authorized domains */
|
||||
private $currentRecords = null;
|
||||
|
||||
/** @var null|array */
|
||||
private $currentRawData = null;
|
||||
|
||||
/** @var Subscribers */
|
||||
private $subscribers;
|
||||
|
||||
/** @var WPFunctions */
|
||||
private $wp;
|
||||
|
||||
public function __construct(
|
||||
Bridge $bridge,
|
||||
NewsletterStatisticsRepository $newsletterStatisticsRepository,
|
||||
SettingsController $settingsController,
|
||||
Subscribers $subscribers,
|
||||
WPFunctions $wp
|
||||
) {
|
||||
$this->bridge = $bridge;
|
||||
$this->newsletterStatisticsRepository = $newsletterStatisticsRepository;
|
||||
$this->settingsController = $settingsController;
|
||||
$this->subscribers = $subscribers;
|
||||
$this->wp = $wp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get record of Bridge::getAuthorizedSenderDomains
|
||||
*/
|
||||
public function getDomainRecords(string $domain = ''): array {
|
||||
$records = $this->getAllRecords();
|
||||
if ($domain) {
|
||||
return $records[$domain] ?? [];
|
||||
}
|
||||
return $records;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Authorized Sender Domains
|
||||
*
|
||||
* Note: This includes both verified and unverified domains
|
||||
*/
|
||||
public function getAllSenderDomains(): array {
|
||||
return $this->returnAllDomains($this->getAllRecords());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Verified Sender Domains.
|
||||
*
|
||||
* Note: This includes partially or fully verified domains.
|
||||
*/
|
||||
public function getVerifiedSenderDomains(): array {
|
||||
return $this->getFullyOrPartiallyVerifiedSenderDomains(true);
|
||||
}
|
||||
|
||||
public function getVerifiedSenderDomainsIgnoringCache(): array {
|
||||
$this->reloadCache();
|
||||
return $this->getFullyOrPartiallyVerifiedSenderDomains(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new Sender Domain
|
||||
*
|
||||
* Throws an InvalidArgumentException if domain already exist
|
||||
*
|
||||
* returns an Array of DNS response or array of error
|
||||
*/
|
||||
public function createAuthorizedSenderDomain(string $domain): array {
|
||||
$allDomains = $this->getAllSenderDomains();
|
||||
|
||||
$alreadyExist = in_array($domain, $allDomains);
|
||||
|
||||
if ($alreadyExist) {
|
||||
// sender domain already created. skip making new request
|
||||
throw new \InvalidArgumentException(self::AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_CREATED);
|
||||
}
|
||||
|
||||
$response = $this->bridge->createAuthorizedSenderDomain($domain);
|
||||
|
||||
if (isset($response['status']) && $response['status'] === API::RESPONSE_STATUS_ERROR) {
|
||||
throw new \InvalidArgumentException($response['message']);
|
||||
}
|
||||
|
||||
// Reset cached value since a new domain was added
|
||||
$this->currentRecords = null;
|
||||
$this->reloadCache();
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getRewrittenEmailAddress(string $email): string {
|
||||
return sprintf('%s@replies.sendingservice.net', str_replace('@', '=', $email));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Sender Domain
|
||||
*
|
||||
* Throws an InvalidArgumentException if domain does not exist or domain is already verified
|
||||
*
|
||||
* * returns [ok: bool, dns: array] if domain verification is successful
|
||||
* * or [ok: bool, error: string, dns: array] if domain verification failed
|
||||
* * or [error: string, status: bool] for other errors
|
||||
*/
|
||||
public function verifyAuthorizedSenderDomain(string $domain): array {
|
||||
$records = $this->bridge->getAuthorizedSenderDomains();
|
||||
|
||||
$allDomains = $this->returnAllDomains($records);
|
||||
$alreadyExist = in_array($domain, $allDomains);
|
||||
|
||||
if (!$alreadyExist) {
|
||||
// can't verify a domain that does not exist
|
||||
throw new \InvalidArgumentException(self::AUTHORIZED_SENDER_DOMAIN_ERROR_NOT_CREATED);
|
||||
}
|
||||
|
||||
$verifiedDomains = $this->getFullyVerifiedSenderDomains(true);
|
||||
$alreadyVerified = in_array($domain, $verifiedDomains);
|
||||
|
||||
if ($alreadyVerified) {
|
||||
// no need to reverify an already verified domain
|
||||
throw new \InvalidArgumentException(self::AUTHORIZED_SENDER_DOMAIN_ERROR_ALREADY_VERIFIED);
|
||||
}
|
||||
|
||||
$response = $this->bridge->verifyAuthorizedSenderDomain($domain);
|
||||
|
||||
// API response contains status, but we need to check that dns array is not included
|
||||
if ($response['status'] === API::RESPONSE_STATUS_ERROR && !isset($response['dns'])) {
|
||||
throw new \InvalidArgumentException($response['message']);
|
||||
}
|
||||
|
||||
$this->currentRecords = null;
|
||||
$this->reloadCache();
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function getSenderDomainsByStatus(array $status): array {
|
||||
return array_filter($this->getAllRawData(), function(array $senderDomainData) use ($status) {
|
||||
return in_array($senderDomainData['domain_status'] ?? null, $status);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sender domains that have all required records, including DMARC.
|
||||
*/
|
||||
public function getFullyVerifiedSenderDomains($domainsOnly = false): array {
|
||||
$domainData = $this->getSenderDomainsByStatus([self::DOMAIN_STATUS_VERIFIED]);
|
||||
return $domainsOnly ? $this->extractDomains($domainData) : $domainData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns sender domains that were verified before DMARC record was required.
|
||||
*/
|
||||
public function getPartiallyVerifiedSenderDomains($domainsOnly = false): array {
|
||||
$domainData = $this->getSenderDomainsByStatus([self::DOMAIN_STATUS_PARTIALLY_VERIFIED]);
|
||||
return $domainsOnly ? $this->extractDomains($domainData) : $domainData;
|
||||
}
|
||||
|
||||
public function getUnverifiedSenderDomains($domainsOnly = false): array {
|
||||
$domainData = $this->getSenderDomainsByStatus([self::DOMAIN_STATUS_UNVERIFIED]);
|
||||
return $domainsOnly ? $this->extractDomains($domainData) : $domainData;
|
||||
}
|
||||
|
||||
public function getFullyOrPartiallyVerifiedSenderDomains($domainsOnly = false): array {
|
||||
$domainData = $this->getSenderDomainsByStatus([self::DOMAIN_STATUS_PARTIALLY_VERIFIED, self::DOMAIN_STATUS_VERIFIED]);
|
||||
return $domainsOnly ? $this->extractDomains($domainData) : $domainData;
|
||||
}
|
||||
|
||||
private function extractDomains(array $domainData): array {
|
||||
$extractedDomains = [];
|
||||
foreach ($domainData as $data) {
|
||||
$extractedDomains[] = $this->domainExtractor($data);
|
||||
}
|
||||
return $extractedDomains;
|
||||
}
|
||||
|
||||
private function domainExtractor(array $domainData): string {
|
||||
return $domainData['domain'] ?? '';
|
||||
}
|
||||
|
||||
public function getSenderDomainsGroupedByStatus(): array {
|
||||
$groupedDomains = [];
|
||||
foreach ($this->getAllRawData() as $senderDomainData) {
|
||||
$status = $senderDomainData['domain_status'] ?? 'unknown';
|
||||
if (!isset($groupedDomains[$status])) {
|
||||
$groupedDomains[$status] = [];
|
||||
}
|
||||
$groupedDomains[$status][] = $senderDomainData;
|
||||
}
|
||||
return $groupedDomains;
|
||||
}
|
||||
|
||||
/**
|
||||
* Little helper function to return All Domains. alias to `array_keys`
|
||||
*
|
||||
* The domain is the key returned from the Bridge::getAuthorizedSenderDomains
|
||||
*/
|
||||
private function returnAllDomains(array $records): array {
|
||||
$domains = array_keys($records);
|
||||
return $domains;
|
||||
}
|
||||
|
||||
private function reloadCache() {
|
||||
$currentRawData = $this->bridge->getRawSenderDomainData();
|
||||
if (!$currentRawData) return; // Do not modify cache if there is no data from the API
|
||||
|
||||
$this->currentRawData = $currentRawData;
|
||||
$this->wp->setTransient(self::SENDER_DOMAINS_KEY, $this->currentRawData, 60 * 60 * 24 * 7);
|
||||
}
|
||||
|
||||
public function isCacheAvailable(): bool {
|
||||
return is_array($this->wp->getTransient(self::SENDER_DOMAINS_KEY));
|
||||
}
|
||||
|
||||
private function getAllRawData(): array {
|
||||
if ($this->currentRawData === null) {
|
||||
$currentData = $this->wp->getTransient(self::SENDER_DOMAINS_KEY);
|
||||
if (is_array($currentData)) {
|
||||
$this->currentRawData = $currentData;
|
||||
} else {
|
||||
$this->reloadCache();
|
||||
}
|
||||
}
|
||||
return is_array($this->currentRawData) ? $this->currentRawData : [];
|
||||
}
|
||||
|
||||
private function getAllRecords(): array {
|
||||
if ($this->currentRecords === null) {
|
||||
$this->currentRecords = $this->bridge->getAuthorizedSenderDomains();
|
||||
}
|
||||
return $this->currentRecords;
|
||||
}
|
||||
|
||||
public function isNewUser(): bool {
|
||||
$installedVersion = $this->settingsController->get('version');
|
||||
|
||||
// Setup wizard has not been completed
|
||||
if ($installedVersion === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$installedAfterNewDomainRestrictions = $this->settingsController->get(self::INSTALLED_AFTER_NEW_RESTRICTIONS_OPTION, false);
|
||||
|
||||
if ($installedAfterNewDomainRestrictions) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->newsletterStatisticsRepository->countBy([]) === 0;
|
||||
}
|
||||
|
||||
public function isSmallSender(): bool {
|
||||
return $this->subscribers->getSubscribersCount() <= self::LOWER_LIMIT;
|
||||
}
|
||||
|
||||
public function isBigSender(): bool {
|
||||
return $this->subscribers->getSubscribersCount() > self::UPPER_LIMIT;
|
||||
}
|
||||
|
||||
public function isAuthorizedDomainRequiredForNewCampaigns(): bool {
|
||||
return $this->settingsController->get('mta.method') === Mailer::METHOD_MAILPOET && !$this->isSmallSender();
|
||||
}
|
||||
|
||||
public function isAuthorizedDomainRequiredForExistingCampaigns(): bool {
|
||||
return $this->settingsController->get('mta.method') === Mailer::METHOD_MAILPOET && $this->isBigSender();
|
||||
}
|
||||
|
||||
public function getContextData(): array {
|
||||
return [
|
||||
'verifiedSenderDomains' => $this->getFullyVerifiedSenderDomains(true),
|
||||
'partiallyVerifiedSenderDomains' => $this->getPartiallyVerifiedSenderDomains(true),
|
||||
'allSenderDomains' => $this->getAllSenderDomains(),
|
||||
'senderRestrictions' => [
|
||||
'lowerLimit' => self::LOWER_LIMIT,
|
||||
'alwaysRewrite' => false,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getContextDataForAutomations(): array {
|
||||
$data = $this->getContextData();
|
||||
$data['senderRestrictions']['alwaysRewrite'] = true;
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Services;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Mailer\Mailer;
|
||||
use MailPoet\Services\Bridge\API;
|
||||
use MailPoet\Settings\SettingsController;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class Bridge {
|
||||
const API_KEY_SETTING_NAME = 'mta.mailpoet_api_key';
|
||||
const API_KEY_STATE_SETTING_NAME = 'mta.mailpoet_api_key_state';
|
||||
const SUBSCRIPTION_TYPE_SETTING_NAME = 'mta.mailpoet_subscription_type';
|
||||
const MANUAL_SUBSCRIPTION_TYPE = 'MANUAL';
|
||||
const STRIPE_SUBSCRIPTION_TYPE = 'STRIPE';
|
||||
const WCCOM_SUBSCRIPTION_TYPE = 'WCCOM';
|
||||
const WPCOM_SUBSCRIPTION_TYPE = 'WPCOM';
|
||||
const WPCOM_BUNDLE_SUBSCRIPTION_TYPE = 'WPCOM_BUNDLE';
|
||||
const SUBSCRIPTION_TYPES = [
|
||||
self::MANUAL_SUBSCRIPTION_TYPE,
|
||||
self::STRIPE_SUBSCRIPTION_TYPE,
|
||||
self::WCCOM_SUBSCRIPTION_TYPE,
|
||||
self::WPCOM_SUBSCRIPTION_TYPE,
|
||||
self::WPCOM_BUNDLE_SUBSCRIPTION_TYPE,
|
||||
];
|
||||
|
||||
const PREMIUM_KEY_SETTING_NAME = 'premium.premium_key';
|
||||
const PREMIUM_KEY_STATE_SETTING_NAME = 'premium.premium_key_state';
|
||||
|
||||
const KEY_ACCESS_INSUFFICIENT_PRIVILEGES = 'insufficient_privileges';
|
||||
const KEY_ACCESS_EMAIL_VOLUME_LIMIT = 'email_volume_limit_reached';
|
||||
const KEY_ACCESS_SUBSCRIBERS_LIMIT = 'subscribers_limit_reached';
|
||||
|
||||
const PREMIUM_KEY_VALID = 'valid'; // for backwards compatibility until version 3.0.0
|
||||
const KEY_VALID = 'valid';
|
||||
const KEY_INVALID = 'invalid';
|
||||
const KEY_EXPIRING = 'expiring';
|
||||
const KEY_ALREADY_USED = 'already_used';
|
||||
const KEY_VALID_UNDERPRIVILEGED = 'valid_underprivileged';
|
||||
|
||||
const KEY_CHECK_ERROR = 'check_error';
|
||||
|
||||
const CHECK_ERROR_UNAVAILABLE = 503;
|
||||
const CHECK_ERROR_UNKNOWN = 'unknown';
|
||||
|
||||
const BRIDGE_URL = 'https://bridge.mailpoet.com';
|
||||
|
||||
/** @var API|null */
|
||||
public $api;
|
||||
|
||||
/** @var SettingsController */
|
||||
private $settings;
|
||||
|
||||
public function __construct(
|
||||
SettingsController $settingsController = null
|
||||
) {
|
||||
if ($settingsController === null) {
|
||||
$settingsController = SettingsController::getInstance();
|
||||
}
|
||||
$this->settings = $settingsController;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
* @deprecated Use non-static function isMailpoetSendingServiceEnabled instead
|
||||
*/
|
||||
public static function isMPSendingServiceEnabled() {
|
||||
try {
|
||||
$mailerConfig = SettingsController::getInstance()->get(Mailer::MAILER_CONFIG_SETTING_NAME);
|
||||
return !empty($mailerConfig['method'])
|
||||
&& $mailerConfig['method'] === Mailer::METHOD_MAILPOET;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isMailpoetSendingServiceEnabled() {
|
||||
try {
|
||||
$mailerConfig = SettingsController::getInstance()->get(Mailer::MAILER_CONFIG_SETTING_NAME);
|
||||
return !empty($mailerConfig['method'])
|
||||
&& $mailerConfig['method'] === Mailer::METHOD_MAILPOET;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static function isMSSKeySpecified() {
|
||||
$settings = SettingsController::getInstance();
|
||||
$key = $settings->get(self::API_KEY_SETTING_NAME);
|
||||
return !empty($key);
|
||||
}
|
||||
|
||||
public static function isPremiumKeySpecified() {
|
||||
$settings = SettingsController::getInstance();
|
||||
$key = $settings->get(self::PREMIUM_KEY_SETTING_NAME);
|
||||
return !empty($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|\WP_Error
|
||||
*/
|
||||
public function pingBridge() {
|
||||
$params = [
|
||||
'blocking' => true,
|
||||
'timeout' => 10,
|
||||
];
|
||||
$wp = new WPFunctions();
|
||||
return $wp->wpRemoteGet(self::BRIDGE_URL, $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function validateBridgePingResponse($response) {
|
||||
$wp = new WPFunctions();
|
||||
return $wp->wpRemoteRetrieveResponseCode($response) === 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return API
|
||||
*/
|
||||
public function initApi($apiKey) {
|
||||
if ($this->api instanceof API) {
|
||||
$this->api->setKey($apiKey);
|
||||
} else {
|
||||
$this->api = new Bridge\API($apiKey);
|
||||
}
|
||||
return $this->api;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return API
|
||||
*/
|
||||
public function getApi($key) {
|
||||
return $this->initApi($key);
|
||||
}
|
||||
|
||||
public function getAuthorizedEmailAddresses(): ?array {
|
||||
return $this
|
||||
->getApi($this->settings->get(self::API_KEY_SETTING_NAME))
|
||||
->getAuthorizedEmailAddresses();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Authorized Email Address
|
||||
*/
|
||||
public function createAuthorizedEmailAddress(string $emailAddress) {
|
||||
return $this
|
||||
->getApi($this->settings->get(self::API_KEY_SETTING_NAME))
|
||||
->createAuthorizedEmailAddress($emailAddress);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of sender domains
|
||||
* returns an assoc array of [domainName => Array(DNS responses)]
|
||||
* pass in the domain arg to return only the DNS response for the domain
|
||||
* For format see @see https://github.com/mailpoet/services-bridge#sender-domains
|
||||
*/
|
||||
public function getAuthorizedSenderDomains($domain = 'all'): array {
|
||||
$domain = strtolower($domain);
|
||||
|
||||
$allSenderDomains = [];
|
||||
$data = $this->getRawSenderDomainData();
|
||||
if ($data === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($data as $subarray) {
|
||||
if (isset($subarray['domain'])) {
|
||||
$allSenderDomains[strtolower($subarray['domain'])] = $subarray['dns'] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
if ($domain !== 'all') {
|
||||
// return an empty array if the provided domain can not be found
|
||||
return $allSenderDomains[$domain] ?? [];
|
||||
}
|
||||
|
||||
return $allSenderDomains;
|
||||
}
|
||||
|
||||
public function getRawSenderDomainData(): ?array {
|
||||
return $this
|
||||
->getApi($this->settings->get(self::API_KEY_SETTING_NAME))
|
||||
->getAuthorizedSenderDomains();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Sender domain record
|
||||
* returns an Array of DNS response or array of error
|
||||
* @see https://github.com/mailpoet/services-bridge#verify-a-sender-domain for response format
|
||||
*/
|
||||
public function createAuthorizedSenderDomain(string $domain): array {
|
||||
$data = $this
|
||||
->getApi($this->settings->get(self::API_KEY_SETTING_NAME))
|
||||
->createAuthorizedSenderDomain($domain);
|
||||
|
||||
return $data['dns'] ?? $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Sender Domain records
|
||||
* returns an Array of DNS response or an array of error
|
||||
* @see https://github.com/mailpoet/services-bridge#verify-a-sender-domain
|
||||
*/
|
||||
public function verifyAuthorizedSenderDomain(string $domain): array {
|
||||
return $this
|
||||
->getApi($this->settings->get(self::API_KEY_SETTING_NAME))
|
||||
->verifyAuthorizedSenderDomain($domain);
|
||||
}
|
||||
|
||||
public function checkMSSKey($apiKey) {
|
||||
$result = $this
|
||||
->getApi($apiKey)
|
||||
->checkMSSKey();
|
||||
return $this->processKeyCheckResult($result);
|
||||
}
|
||||
|
||||
private function storeSubscriptionType(?string $subscriptionType): void {
|
||||
if (in_array($subscriptionType, self::SUBSCRIPTION_TYPES, true)) {
|
||||
$this->settings->set(
|
||||
self::SUBSCRIPTION_TYPE_SETTING_NAME,
|
||||
$subscriptionType
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function storeMSSKeyAndState($key, $state) {
|
||||
return $this->storeKeyAndState(API::KEY_CHECK_TYPE_MSS, $key, $state);
|
||||
}
|
||||
|
||||
public function checkPremiumKey($key) {
|
||||
$result = $this
|
||||
->getApi($key)
|
||||
->checkPremiumKey();
|
||||
return $this->processKeyCheckResult($result);
|
||||
}
|
||||
|
||||
private function processKeyCheckResult(array $result) {
|
||||
$stateMap = [
|
||||
200 => self::KEY_VALID,
|
||||
401 => self::KEY_INVALID,
|
||||
402 => self::KEY_ALREADY_USED,
|
||||
403 => self::KEY_VALID_UNDERPRIVILEGED,
|
||||
];
|
||||
|
||||
if (!empty($result['code']) && isset($stateMap[$result['code']])) {
|
||||
if (
|
||||
$stateMap[$result['code']] == self::KEY_VALID
|
||||
&& !empty($result['data']['expire_at'])
|
||||
) {
|
||||
$keyState = self::KEY_EXPIRING;
|
||||
} else {
|
||||
$keyState = $stateMap[$result['code']];
|
||||
}
|
||||
} else {
|
||||
$keyState = self::KEY_CHECK_ERROR;
|
||||
}
|
||||
|
||||
// Map of access error messages.
|
||||
// The message is set by shop when a subscription has limited access to the feature.
|
||||
// Insufficient privileges - is the default state if the plan doesn't include the feature.
|
||||
// If the bridge returns 403 and there is a message set by the shop it returns the message.
|
||||
$accessRestrictionsMap = [
|
||||
API::ERROR_MESSAGE_INSUFFICIENT_PRIVILEGES => self::KEY_ACCESS_INSUFFICIENT_PRIVILEGES,
|
||||
API::ERROR_MESSAGE_SUBSCRIBERS_LIMIT_REACHED => self::KEY_ACCESS_SUBSCRIBERS_LIMIT,
|
||||
API::ERROR_MESSAGE_EMAIL_VOLUME_LIMIT_REACHED => self::KEY_ACCESS_EMAIL_VOLUME_LIMIT,
|
||||
];
|
||||
|
||||
$accessRestriction = null;
|
||||
if (!empty($result['code']) && $result['code'] === 403 && !empty($result['error_message'])) {
|
||||
$accessRestriction = $accessRestrictionsMap[$result['error_message']] ?? null;
|
||||
}
|
||||
|
||||
return $this->buildKeyState(
|
||||
$keyState,
|
||||
$result,
|
||||
$accessRestriction
|
||||
);
|
||||
}
|
||||
|
||||
public function storePremiumKeyAndState($key, $state) {
|
||||
return $this->storeKeyAndState(API::KEY_CHECK_TYPE_PREMIUM, $key, $state);
|
||||
}
|
||||
|
||||
private function storeKeyAndState(string $keyType, ?string $key, ?array $state) {
|
||||
if ($keyType === API::KEY_CHECK_TYPE_PREMIUM) {
|
||||
$keySettingName = self::PREMIUM_KEY_SETTING_NAME;
|
||||
$keyStateSettingName = self::PREMIUM_KEY_STATE_SETTING_NAME;
|
||||
} else {
|
||||
$keySettingName = self::API_KEY_SETTING_NAME;
|
||||
$keyStateSettingName = self::API_KEY_STATE_SETTING_NAME;
|
||||
}
|
||||
|
||||
if (
|
||||
empty($state['state'])
|
||||
|| $state['state'] === self::KEY_CHECK_ERROR
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$previousKey = $this->settings->get($keySettingName);
|
||||
// If the key remain the same and the new state is not valid we want to preserve the data from the previous state.
|
||||
// The data contain information about state limits. We need those to display the correct information to users.
|
||||
if (empty($state['data']) && $previousKey === $key) {
|
||||
$previousState = $this->settings->get($keyStateSettingName);
|
||||
if (!empty($previousState['data'])) {
|
||||
$state['data'] = $previousState['data'];
|
||||
}
|
||||
}
|
||||
|
||||
// store the key itself
|
||||
if ($previousKey !== $key) {
|
||||
$this->settings->set(
|
||||
$keySettingName,
|
||||
$key
|
||||
);
|
||||
}
|
||||
|
||||
// store the key state
|
||||
$this->settings->set(
|
||||
$keyStateSettingName,
|
||||
$state
|
||||
);
|
||||
|
||||
// store the subscription type
|
||||
if (!empty($state['data']) && !empty($state['data']['subscription_type'])) {
|
||||
$this->storeSubscriptionType($state['data']['subscription_type']);
|
||||
}
|
||||
}
|
||||
|
||||
private function buildKeyState($keyState, $result, ?string $accessRestriction): array {
|
||||
return [
|
||||
'state' => $keyState,
|
||||
'access_restriction' => $accessRestriction,
|
||||
'data' => !empty($result['data']) ? $result['data'] : null,
|
||||
'code' => !empty($result['code']) ? $result['code'] : self::CHECK_ERROR_UNKNOWN,
|
||||
];
|
||||
}
|
||||
|
||||
public function updateSubscriberCount(string $key, int $count): bool {
|
||||
return $this->getApi($key)->updateSubscriberCount($count);
|
||||
}
|
||||
|
||||
public function invalidateMssKey() {
|
||||
$key = $this->settings->get(self::API_KEY_SETTING_NAME);
|
||||
$this->storeMSSKeyAndState($key, $this->buildKeyState(
|
||||
self::KEY_INVALID,
|
||||
['code' => API::RESPONSE_CODE_KEY_INVALID],
|
||||
null
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Services\Bridge;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Logging\LoggerFactory;
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
use WP_Error;
|
||||
|
||||
class API {
|
||||
const RESPONSE_STATUS_OK = 'ok';
|
||||
const RESPONSE_STATUS_ERROR = 'error';
|
||||
const SENDING_STATUS_CONNECTION_ERROR = 'connection_error';
|
||||
const SENDING_STATUS_SEND_ERROR = 'send_error';
|
||||
|
||||
const REQUEST_TIMEOUT = 10; // seconds
|
||||
|
||||
const RESPONSE_CODE_KEY_INVALID = 401;
|
||||
const RESPONSE_CODE_STATS_SAVED = 204;
|
||||
const RESPONSE_CODE_CREATED = 201;
|
||||
const RESPONSE_CODE_INTERNAL_SERVER_ERROR = 500;
|
||||
const RESPONSE_CODE_BAD_GATEWAY = 502;
|
||||
const RESPONSE_CODE_TEMPORARY_UNAVAILABLE = 503;
|
||||
const RESPONSE_CODE_GATEWAY_TIMEOUT = 504;
|
||||
const RESPONSE_CODE_NOT_ARRAY = 422;
|
||||
const RESPONSE_CODE_PAYLOAD_TOO_BIG = 413;
|
||||
const RESPONSE_CODE_PAYLOAD_ERROR = 400;
|
||||
const RESPONSE_CODE_CAN_NOT_SEND = 403;
|
||||
|
||||
// Bridge messages from https://github.com/mailpoet/services-bridge/blob/master/api/messages.rb
|
||||
public const ERROR_MESSAGE_BANNED = 'Key is valid, but the action is forbidden';
|
||||
public const ERROR_MESSAGE_INVALID_FROM = 'The email address is not authorized';
|
||||
public const ERROR_MESSAGE_PENDING_APPROVAL = 'Key is valid, but not approved yet; you can send only to authorized email addresses at the moment';
|
||||
public const ERROR_MESSAGE_DMRAC = "Email violates Sender Domain's DMARC policy. Please set up sender authentication.";
|
||||
public const ERROR_MESSAGE_BULK_EMAIL_FORBIDDEN = 'Please update the plugin and add/update your sender domain (refer to https://account.mailpoet.com/sender_domains)';
|
||||
// Bridge message from https://github.com/mailpoet/services-bridge/blob/master/extensions/authentication/basic_strategy.rb
|
||||
public const ERROR_MESSAGE_UNAUTHORIZED = 'No valid API key provided';
|
||||
public const ERROR_MESSAGE_INSUFFICIENT_PRIVILEGES = 'Insufficient privileges';
|
||||
public const ERROR_MESSAGE_EMAIL_VOLUME_LIMIT_REACHED = 'Email volume limit reached';
|
||||
public const ERROR_MESSAGE_SUBSCRIBERS_LIMIT_REACHED = 'Subscribers limit reached';
|
||||
// Proxy request `authorized_email_address` from shop https://github.com/mailpoet/shop/blob/master/routes/hooks/sending/v1/index.js#L65
|
||||
public const ERROR_MESSAGE_AUTHORIZED_EMAIL_NO_FREE = 'You cannot use a free email address. Please use an address from your website’s domain, for example.';
|
||||
public const ERROR_MESSAGE_AUTHORIZED_EMAIL_INVALID = 'Invalid email.';
|
||||
public const ERROR_MESSAGE_AUTHORIZED_EMAIL_ALREADY_ADDED = 'This email was already added to the list.';
|
||||
// Proxy request `sender_domain_verify` from shop https://github.com/mailpoet/shop/blob/master/routes/hooks/sending/v1/index.js#L137
|
||||
public const ERROR_MESSAGE_AUTHORIZED_DOMAIN_VERIFY_NOT_FOUND = 'Domain not found';
|
||||
public const ERROR_MESSAGE_AUTHORIZED_DOMAIN_VERIFY_FAILED = 'Some DNS records were not set up correctly. Please check the records again. You may need to wait up to 24 hours for DNS changes to propagate.';
|
||||
// Proxy request `sender_domain` from shop https://github.com/mailpoet/shop/blob/master/routes/hooks/sending/v1/index.js#L65
|
||||
public const ERROR_MESSAGE_SENDER_DOMAIN_INVALID = 'Invalid domain. Please enter a valid domain name.';
|
||||
public const ERROR_MESSAGE_SENDER_DOMAIN_ALREADY_ADDED = 'This domain was already added to the list.';
|
||||
|
||||
public const KEY_CHECK_TYPE_PREMIUM = 'premium';
|
||||
public const KEY_CHECK_TYPE_MSS = 'mss';
|
||||
|
||||
private $apiKey;
|
||||
private $wp;
|
||||
/** @var LoggerFactory */
|
||||
private $loggerFactory;
|
||||
/** @var mixed|null It is an instance of \CurlHandle in PHP8 and aboove but a resource in PHP7 */
|
||||
private $curlHandle = null;
|
||||
|
||||
public $urlMe = 'https://bridge.mailpoet.com/api/v0/me';
|
||||
public $urlPremium = 'https://bridge.mailpoet.com/api/v0/premium';
|
||||
public $urlMessages = 'https://bridge.mailpoet.com/api/v0/messages';
|
||||
public $urlBounces = 'https://bridge.mailpoet.com/api/v0/bounces/search';
|
||||
public $urlStats = 'https://bridge.mailpoet.com/api/v0/stats';
|
||||
public $urlAuthorizedEmailAddresses = 'https://bridge.mailpoet.com/api/v1/authorized_email_address';
|
||||
public $urlAuthorizedSenderDomains = 'https://bridge.mailpoet.com/api/v1/sender_domain';
|
||||
public $urlAuthorizedSenderDomainVerification = 'https://bridge.mailpoet.com/api/v1/sender_domain_verify';
|
||||
|
||||
public function __construct(
|
||||
$apiKey,
|
||||
$wp = null
|
||||
) {
|
||||
$this->setKey($apiKey);
|
||||
if (is_null($wp)) {
|
||||
$this->wp = new WPFunctions();
|
||||
} else {
|
||||
$this->wp = $wp;
|
||||
}
|
||||
$this->loggerFactory = LoggerFactory::getInstance();
|
||||
}
|
||||
|
||||
public function checkMSSKey() {
|
||||
return $this->checkKey(self::KEY_CHECK_TYPE_MSS);
|
||||
}
|
||||
|
||||
public function checkPremiumKey() {
|
||||
return $this->checkKey(self::KEY_CHECK_TYPE_PREMIUM);
|
||||
}
|
||||
|
||||
private function checkKey(string $keyCheckType): array {
|
||||
if ($keyCheckType === self::KEY_CHECK_TYPE_PREMIUM) {
|
||||
$apiUrl = $this->urlPremium;
|
||||
} else {
|
||||
$apiUrl = $this->urlMe;
|
||||
}
|
||||
$result = $this->request(
|
||||
$apiUrl,
|
||||
['site' => strtolower(WPFunctions::get()->homeUrl())]
|
||||
);
|
||||
|
||||
$errorMessage = null;
|
||||
$code = $this->wp->wpRemoteRetrieveResponseCode($result);
|
||||
switch ($code) {
|
||||
case 200:
|
||||
$body = $this->wp->wpRemoteRetrieveBody($result);
|
||||
if ($body) {
|
||||
$body = json_decode($body, true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$this->logKeyCheckError((int)$code, $keyCheckType);
|
||||
$body = null;
|
||||
$errorMessage = $this->wp->wpRemoteRetrieveBody($result);
|
||||
break;
|
||||
}
|
||||
|
||||
return ['code' => $code, 'data' => $body, 'error_message' => $errorMessage];
|
||||
}
|
||||
|
||||
/**
|
||||
* This method logs data from 'requests-curl.after_request' hook.
|
||||
* The hook is mostly called with two parameters but sometimes only with one.
|
||||
*/
|
||||
public function logCurlInformation($headers, $info = null) {
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_MSS)->info(
|
||||
'requests-curl.after_request',
|
||||
['headers' => $headers, 'curl_info' => $info]
|
||||
);
|
||||
}
|
||||
|
||||
public function setCurlHandle($handle) {
|
||||
$this->curlHandle = $handle;
|
||||
}
|
||||
|
||||
public function sendMessages($messageBody) {
|
||||
$this->curlHandle = null;
|
||||
add_action('requests-curl.before_request', [$this, 'setCurlHandle'], 10, 1);
|
||||
add_action('requests-curl.after_request', [$this, 'logCurlInformation'], 10, 2);
|
||||
$result = $this->request(
|
||||
$this->urlMessages,
|
||||
$messageBody
|
||||
);
|
||||
remove_action('requests-curl.after_request', [$this, 'logCurlInformation']);
|
||||
remove_action('requests-curl.before_request', [$this, 'setCurlHandle']);
|
||||
if (is_wp_error($result)) {
|
||||
$this->logCurlError($result);
|
||||
return [
|
||||
'status' => self::SENDING_STATUS_CONNECTION_ERROR,
|
||||
'message' => $result->get_error_message(),
|
||||
];
|
||||
}
|
||||
|
||||
$responseCode = $this->wp->wpRemoteRetrieveResponseCode($result);
|
||||
if ($responseCode !== 201) {
|
||||
$response = ($this->wp->wpRemoteRetrieveBody($result)) ?
|
||||
$this->wp->wpRemoteRetrieveBody($result) :
|
||||
$this->wp->wpRemoteRetrieveResponseMessage($result);
|
||||
return $this->createErrorResponse((int)$responseCode, $response, self::SENDING_STATUS_SEND_ERROR);
|
||||
}
|
||||
return ['status' => self::RESPONSE_STATUS_OK];
|
||||
}
|
||||
|
||||
public function checkBounces(array $emails) {
|
||||
$result = $this->request(
|
||||
$this->urlBounces,
|
||||
$emails
|
||||
);
|
||||
if ($this->wp->wpRemoteRetrieveResponseCode($result) === 200) {
|
||||
return json_decode($this->wp->wpRemoteRetrieveBody($result), true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function updateSubscriberCount($count): bool {
|
||||
$result = $this->request(
|
||||
$this->urlStats,
|
||||
['subscriber_count' => (int)$count],
|
||||
'PUT'
|
||||
);
|
||||
$code = $this->wp->wpRemoteRetrieveResponseCode($result);
|
||||
$isSuccess = $code === self::RESPONSE_CODE_STATS_SAVED;
|
||||
if (!$isSuccess) {
|
||||
$logData = [
|
||||
'code' => $code,
|
||||
'error' => is_wp_error($result) ? $result->get_error_message() : null,
|
||||
];
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_BRIDGE)->error('Stats API call failed.', $logData);
|
||||
}
|
||||
return $isSuccess;
|
||||
}
|
||||
|
||||
public function getAuthorizedEmailAddresses(): ?array {
|
||||
$result = $this->request(
|
||||
$this->urlAuthorizedEmailAddresses,
|
||||
null,
|
||||
'GET'
|
||||
);
|
||||
if ($this->wp->wpRemoteRetrieveResponseCode($result) !== 200) {
|
||||
return null;
|
||||
}
|
||||
$data = json_decode($this->wp->wpRemoteRetrieveBody($result), true);
|
||||
return is_array($data) ? $data : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Authorized Email Address
|
||||
*
|
||||
* @param string $emailAddress
|
||||
* @return array{status: string, code?: int, error?: string, message?: string}
|
||||
*/
|
||||
public function createAuthorizedEmailAddress(string $emailAddress): array {
|
||||
$body = ['email' => $emailAddress];
|
||||
$result = $this->request(
|
||||
$this->urlAuthorizedEmailAddresses,
|
||||
$body
|
||||
);
|
||||
|
||||
$responseCode = $this->wp->wpRemoteRetrieveResponseCode($result);
|
||||
|
||||
if ($responseCode !== self::RESPONSE_CODE_CREATED) {
|
||||
$errorBody = $this->wp->wpRemoteRetrieveBody($result);
|
||||
$logData = [
|
||||
'code' => $responseCode,
|
||||
'error' => is_wp_error($result) ? $result->get_error_message() : $errorBody,
|
||||
];
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_BRIDGE)->error('CreateAuthorizedEmailAddress API call failed.', $logData);
|
||||
|
||||
$errorResponseData = json_decode($errorBody, true);
|
||||
// translators: %d is the error code.
|
||||
$fallbackError = sprintf(__('An error has happened while performing a request, the server has responded with response code %d', 'mailpoet'), $responseCode);
|
||||
|
||||
$error = is_array($errorResponseData) && isset($errorResponseData['error']) ? $errorResponseData['error'] : $fallbackError;
|
||||
return $this->createErrorResponse((int)$responseCode, $error);
|
||||
}
|
||||
|
||||
return ['status' => self::RESPONSE_STATUS_OK];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of sender domains
|
||||
* Fetched from API
|
||||
* @see https://github.com/mailpoet/services-bridge#sender-domains
|
||||
*/
|
||||
public function getAuthorizedSenderDomains(): ?array {
|
||||
$result = $this->request(
|
||||
$this->urlAuthorizedSenderDomains,
|
||||
null,
|
||||
'GET'
|
||||
);
|
||||
if ($this->wp->wpRemoteRetrieveResponseCode($result) !== 200) {
|
||||
return null;
|
||||
}
|
||||
$rawData = $this->wp->wpRemoteRetrieveBody($result);
|
||||
$data = json_decode($rawData, true);
|
||||
if (!is_array($data)) {
|
||||
$this->logInvalidDataFormat('getAuthorizedSenderDomains', $rawData);
|
||||
return null;
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Sender domain record
|
||||
* Done via API
|
||||
* Returns same response se sender_domain_verify @see https://github.com/mailpoet/services-bridge#verify-a-sender-domain
|
||||
*/
|
||||
public function createAuthorizedSenderDomain(string $domain): array {
|
||||
$body = ['domain' => strtolower($domain)];
|
||||
$result = $this->request(
|
||||
$this->urlAuthorizedSenderDomains,
|
||||
$body
|
||||
);
|
||||
|
||||
$responseCode = $this->wp->wpRemoteRetrieveResponseCode($result);
|
||||
$rawResponseBody = $this->wp->wpRemoteRetrieveBody($result);
|
||||
|
||||
$responseBody = json_decode($rawResponseBody, true);
|
||||
|
||||
if ($responseCode !== self::RESPONSE_CODE_CREATED) {
|
||||
$logData = [
|
||||
'code' => $responseCode,
|
||||
'error' => is_wp_error($result) ? $result->get_error_message() : $rawResponseBody,
|
||||
];
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_BRIDGE)->error('createAuthorizedSenderDomain API call failed.', $logData);
|
||||
|
||||
// translators: %d will be replaced by an error code
|
||||
$fallbackError = sprintf(__('An error has happened while performing a request, the server has responded with response code %d', 'mailpoet'), $responseCode);
|
||||
|
||||
$error = is_array($responseBody) && isset($responseBody['error']) ? $responseBody['error'] : $fallbackError;
|
||||
return $this->createErrorResponse((int)$responseCode, $error);
|
||||
}
|
||||
|
||||
if (!is_array($responseBody)) {
|
||||
$this->logInvalidDataFormat('createAuthorizedSenderDomain', $rawResponseBody);
|
||||
return [];
|
||||
}
|
||||
|
||||
$responseBody['status'] = self::RESPONSE_STATUS_OK;
|
||||
return $responseBody;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Sender Domain records
|
||||
* returns an Array of DNS response or an array of error
|
||||
* @see https://github.com/mailpoet/services-bridge#verify-a-sender-domain
|
||||
*/
|
||||
public function verifyAuthorizedSenderDomain(string $domain): array {
|
||||
$url = $this->urlAuthorizedSenderDomainVerification . '/' . urlencode(strtolower($domain));
|
||||
$result = $this->request(
|
||||
$url,
|
||||
null
|
||||
);
|
||||
|
||||
$responseCode = $this->wp->wpRemoteRetrieveResponseCode($result);
|
||||
$rawResponseBody = $this->wp->wpRemoteRetrieveBody($result);
|
||||
|
||||
$responseBody = json_decode($rawResponseBody, true);
|
||||
if ($responseCode !== 200) {
|
||||
if ($responseCode === 400) {
|
||||
// we need to return the body as it is, but for consistency we add status and translated error message
|
||||
$response = is_array($responseBody) ? $responseBody : [];
|
||||
$response['status'] = self::RESPONSE_STATUS_ERROR;
|
||||
$response['message'] = $this->getTranslatedErrorMessage($response['error']);
|
||||
return $response;
|
||||
}
|
||||
$logData = [
|
||||
'code' => $responseCode,
|
||||
'error' => is_wp_error($result) ? $result->get_error_message() : $rawResponseBody,
|
||||
];
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_BRIDGE)->error('verifyAuthorizedSenderDomain API call failed.', $logData);
|
||||
|
||||
// translators: %d will be replaced by an error code
|
||||
$fallbackError = sprintf(__('An error has happened while performing a request, the server has responded with response code %d', 'mailpoet'), $responseCode);
|
||||
|
||||
$error = is_array($responseBody) && isset($responseBody['error']) ? $responseBody['error'] : $fallbackError;
|
||||
return $this->createErrorResponse((int)$responseCode, $error);
|
||||
}
|
||||
|
||||
if (!is_array($responseBody)) {
|
||||
$this->logInvalidDataFormat('verifyAuthorizedSenderDomain', $rawResponseBody);
|
||||
return [];
|
||||
}
|
||||
|
||||
$responseBody['status'] = self::RESPONSE_STATUS_OK;
|
||||
return $responseBody;
|
||||
}
|
||||
|
||||
public function setKey($apiKey) {
|
||||
$this->apiKey = $apiKey;
|
||||
}
|
||||
|
||||
public function getKey() {
|
||||
return $this->apiKey;
|
||||
}
|
||||
|
||||
public function getTranslatedErrorMessage(string $errorMessage): string {
|
||||
switch ($errorMessage) {
|
||||
case self::ERROR_MESSAGE_BANNED:
|
||||
return __('Key is valid, but the action is forbidden.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_INVALID_FROM:
|
||||
return __('The email address is not authorized.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_PENDING_APPROVAL:
|
||||
return __('Key is valid, but not approved yet; you can send only to authorized email addresses at the moment.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_DMRAC:
|
||||
return __("Email violates Sender Domain's DMARC policy. Please set up sender authentication.", 'mailpoet');
|
||||
case self::ERROR_MESSAGE_BULK_EMAIL_FORBIDDEN:
|
||||
return __('Email violates Sender Domain requirements. Please authenticate the sender domain.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_UNAUTHORIZED:
|
||||
return __('No valid API key provided.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_INSUFFICIENT_PRIVILEGES:
|
||||
return __('Insufficient privileges.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_EMAIL_VOLUME_LIMIT_REACHED:
|
||||
return __('Email volume limit reached.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_SUBSCRIBERS_LIMIT_REACHED:
|
||||
return __('Subscribers limit reached.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_AUTHORIZED_EMAIL_NO_FREE:
|
||||
return __('You cannot use a free email address. Please use an address from your website’s domain, for example.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_AUTHORIZED_EMAIL_INVALID:
|
||||
return __('Invalid email.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_AUTHORIZED_EMAIL_ALREADY_ADDED:
|
||||
return __('This email was already added to the list.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_AUTHORIZED_DOMAIN_VERIFY_NOT_FOUND:
|
||||
return __('Domain not found.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_AUTHORIZED_DOMAIN_VERIFY_FAILED:
|
||||
return __('Some DNS records were not set up correctly. Please check the records again. You may need to wait up to 24 hours for DNS changes to propagate.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_SENDER_DOMAIN_INVALID:
|
||||
return __('Invalid domain. Please enter a valid domain name.', 'mailpoet');
|
||||
case self::ERROR_MESSAGE_SENDER_DOMAIN_ALREADY_ADDED:
|
||||
return __('This domain was already added to the list.', 'mailpoet');
|
||||
// when we don't match translation we return the origin
|
||||
default:
|
||||
return $errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
private function auth() {
|
||||
return 'Basic ' . base64_encode('api:' . $this->apiKey);
|
||||
}
|
||||
|
||||
private function request($url, $body, $method = 'POST') {
|
||||
$params = [
|
||||
'timeout' => $this->wp->applyFilters('mailpoet_bridge_api_request_timeout', self::REQUEST_TIMEOUT),
|
||||
'httpversion' => '1.0',
|
||||
'method' => $method,
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'Authorization' => $this->auth(),
|
||||
],
|
||||
'body' => $body !== null ? json_encode($body) : null,
|
||||
];
|
||||
return $this->wp->wpRemotePost($url, $params);
|
||||
}
|
||||
|
||||
private function logCurlError(WP_Error $error) {
|
||||
$logData = [
|
||||
'curl_errno' => $this->curlHandle ? curl_errno($this->curlHandle) : 'n/a',
|
||||
'curl_error' => $this->curlHandle ? curl_error($this->curlHandle) : $error->get_error_message(),
|
||||
'curl_info' => $this->curlHandle ? curl_getinfo($this->curlHandle) : 'n/a',
|
||||
];
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_MSS)->error('requests-curl.failed', $logData);
|
||||
}
|
||||
|
||||
private function logKeyCheckError(int $code, string $keyType): void {
|
||||
$logData = [
|
||||
'http_code' => $code,
|
||||
'home_url' => $this->wp->homeUrl(),
|
||||
'key_type' => $keyType,
|
||||
];
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_MSS)->error('key-validation.failed', $logData);
|
||||
}
|
||||
|
||||
private function logInvalidDataFormat(string $method, ?string $response = null): void {
|
||||
$logData = [
|
||||
'code' => json_last_error(),
|
||||
'response' => $response,
|
||||
];
|
||||
$this->loggerFactory->getLogger(LoggerFactory::TOPIC_BRIDGE)->error($method . ' API response was not in expected format.', $logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{status: string, code: int, error: string, message: string}
|
||||
*/
|
||||
private function createErrorResponse(int $responseCode, string $error, string $errorStatus = self::RESPONSE_STATUS_ERROR): array {
|
||||
return [
|
||||
'status' => $errorStatus,
|
||||
'code' => $responseCode,
|
||||
'error' => $error,
|
||||
'message' => $this->getTranslatedErrorMessage($error),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Services;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Config\Renderer;
|
||||
use MailPoet\Mailer\MailerFactory;
|
||||
use MailPoet\Mailer\MetaInfo;
|
||||
|
||||
class CongratulatoryMssEmailController {
|
||||
/** @var MailerFactory */
|
||||
private $mailerFactory;
|
||||
|
||||
/** @var MetaInfo */
|
||||
private $mailerMetaInfo;
|
||||
|
||||
/** @var Renderer */
|
||||
private $renderer;
|
||||
|
||||
public function __construct(
|
||||
MailerFactory $mailerFactory,
|
||||
MetaInfo $mailerMetaInfo,
|
||||
Renderer $renderer
|
||||
) {
|
||||
$this->mailerFactory = $mailerFactory;
|
||||
$this->mailerMetaInfo = $mailerMetaInfo;
|
||||
$this->renderer = $renderer;
|
||||
}
|
||||
|
||||
public function sendCongratulatoryEmail(string $toEmailAddress) {
|
||||
$renderedNewsletter = [
|
||||
'subject' => _x('Sending with MailPoet works!', 'Subject of an email confirming that MailPoet Sending Service works', 'mailpoet'),
|
||||
'body' => [
|
||||
'html' => $this->renderer->render('emails/congratulatoryMssEmail.html'),
|
||||
'text' => $this->renderer->render('emails/congratulatoryMssEmail.txt'),
|
||||
],
|
||||
];
|
||||
|
||||
$extraParams = [
|
||||
'meta' => $this->mailerMetaInfo->getSendingTestMetaInfo(),
|
||||
];
|
||||
$this->mailerFactory->getDefaultMailer()->send($renderedNewsletter, $toEmailAddress, $extraParams);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
|
||||
|
||||
namespace MailPoet\Services\Release;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
class API {
|
||||
private $apiKey;
|
||||
private $wp;
|
||||
public $urlProducts = 'https://release.mailpoet.com/products/';
|
||||
|
||||
public function __construct(
|
||||
$apiKey
|
||||
) {
|
||||
$this->setKey($apiKey);
|
||||
$this->wp = new WPFunctions();
|
||||
}
|
||||
|
||||
public function getPluginInformation($pluginName) {
|
||||
$result = $this->request(
|
||||
$this->urlProducts . $pluginName
|
||||
);
|
||||
|
||||
$code = $this->wp->wpRemoteRetrieveResponseCode($result);
|
||||
switch ($code) {
|
||||
case 200:
|
||||
$body = $this->wp->wpRemoteRetrieveBody($result);
|
||||
if ($body) {
|
||||
$body = $this->formatPluginInformation(json_decode($body));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$body = null;
|
||||
break;
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function setKey($apiKey) {
|
||||
$this->apiKey = $apiKey;
|
||||
}
|
||||
|
||||
public function getKey() {
|
||||
return $this->apiKey;
|
||||
}
|
||||
|
||||
private function formatPluginInformation($info) {
|
||||
if (!$info instanceof \stdClass) return $info;
|
||||
|
||||
$propKeys = array_keys(get_object_vars($info));
|
||||
$newInfo = clone $info;
|
||||
|
||||
foreach ($propKeys as $key) {
|
||||
if (gettype($newInfo->{$key}) === 'object') {
|
||||
// cast objects to array for WP to understand
|
||||
$newInfo->{$key} = (array)$newInfo->{$key};
|
||||
}
|
||||
}
|
||||
|
||||
return $newInfo;
|
||||
}
|
||||
|
||||
private function request($url, $params = []) {
|
||||
$params['license'] = $this->apiKey;
|
||||
$url = WPFunctions::get()->addQueryArg($params, $url);
|
||||
$args = [
|
||||
'timeout' => 10,
|
||||
'httpversion' => '1.0',
|
||||
];
|
||||
return $this->wp->wpRemoteGet($url, $args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Services;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\Util\License\Features\Subscribers;
|
||||
|
||||
class SubscribersCountReporter {
|
||||
/** @var Bridge */
|
||||
private $bridge;
|
||||
|
||||
/** @var Subscribers */
|
||||
private $subscribersFeature;
|
||||
|
||||
public function __construct(
|
||||
Bridge $bridge,
|
||||
Subscribers $subscribersFeature
|
||||
) {
|
||||
$this->bridge = $bridge;
|
||||
$this->subscribersFeature = $subscribersFeature;
|
||||
}
|
||||
|
||||
public function report(string $key): bool {
|
||||
$subscribersCount = $this->subscribersFeature->getSubscribersCount();
|
||||
return $this->bridge->updateSubscriberCount($key, $subscribersCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php declare(strict_types = 1);
|
||||
|
||||
namespace MailPoet\Services;
|
||||
|
||||
if (!defined('ABSPATH')) exit;
|
||||
|
||||
|
||||
use MailPoet\WP\Functions as WPFunctions;
|
||||
|
||||
/**
|
||||
* This class contains validation methods that were extracted from the \MailPoet\Models\ModelValidator class.
|
||||
* It is used only in a few places and there is a chance in the future we can remove it.
|
||||
*/
|
||||
class Validator {
|
||||
const EMAIL_MIN_LENGTH = 6;
|
||||
const EMAIL_MAX_LENGTH = 150;
|
||||
const ROLE_EMAILS = [
|
||||
'abuse',
|
||||
'compliance',
|
||||
'devnull',
|
||||
'dns',
|
||||
'ftp',
|
||||
'hostmaster',
|
||||
'inoc',
|
||||
'ispfeedback',
|
||||
'ispsupport',
|
||||
'list-request',
|
||||
'list',
|
||||
'maildaemon',
|
||||
'noc',
|
||||
'no-reply',
|
||||
'noreply',
|
||||
'nospam',
|
||||
'null',
|
||||
'phish',
|
||||
'phishing',
|
||||
'postmaster',
|
||||
'privacy',
|
||||
'registrar',
|
||||
'root',
|
||||
'security',
|
||||
'spam',
|
||||
'sysadmin',
|
||||
'undisclosed-recipients',
|
||||
'unsubscribe',
|
||||
'usenet',
|
||||
'uucp',
|
||||
'webmaster',
|
||||
'www',
|
||||
];
|
||||
|
||||
public function validateEmail($email): bool {
|
||||
$permittedLength = (strlen($email) >= self::EMAIL_MIN_LENGTH && strlen($email) <= self::EMAIL_MAX_LENGTH);
|
||||
$validEmail = WPFunctions::get()->isEmail($email) !== false && filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
|
||||
return ($permittedLength && $validEmail);
|
||||
}
|
||||
|
||||
public function validateNonRoleEmail($email): bool {
|
||||
if (!$this->validateEmail($email)) return false;
|
||||
$firstPart = strtolower(substr($email, 0, (int)strpos($email, '@')));
|
||||
return array_search($firstPart, self::ROLE_EMAILS) === false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<?php
|
||||
Reference in New Issue
Block a user