This commit is contained in:
emmymayo
2025-02-05 23:15:46 +01:00
commit 7269c99357
16995 changed files with 3389680 additions and 0 deletions
@@ -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 websites 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 websites 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