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,82 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\SubscriberEntity;
use MailPoet\Mailer\Methods\MailerMethod;
class Mailer {
/** @var MailerMethod */
public $mailerMethod;
const MAILER_CONFIG_SETTING_NAME = 'mta';
const SENDING_LIMIT_INTERVAL_MULTIPLIER = 60;
const METHOD_MAILPOET = 'MailPoet';
const METHOD_AMAZONSES = 'AmazonSES';
const METHOD_SENDGRID = 'SendGrid';
const METHOD_PHPMAIL = 'PHPMail';
const METHOD_SMTP = 'SMTP';
public function __construct(
MailerMethod $mailerMethod
) {
$this->mailerMethod = $mailerMethod;
}
public function send($newsletter, $subscriber, $extraParams = []) {
$subscriber = $this->formatSubscriberNameAndEmailAddress($subscriber);
return $this->mailerMethod->send($newsletter, $subscriber, $extraParams);
}
/**
* @param SubscriberEntity|array|string $subscriber
*/
public function formatSubscriberNameAndEmailAddress($subscriber) {
if ($subscriber instanceof SubscriberEntity) {
$prepareSubscriber = [];
$prepareSubscriber['email'] = $subscriber->getEmail();
$prepareSubscriber['first_name'] = $subscriber->getFirstName();
$prepareSubscriber['last_name'] = $subscriber->getLastName();
$subscriber = $prepareSubscriber;
}
if (!is_array($subscriber)) return $subscriber;
if (isset($subscriber['address'])) $subscriber['email'] = $subscriber['address'];
$firstName = (isset($subscriber['first_name'])) ? $subscriber['first_name'] : '';
$lastName = (isset($subscriber['last_name'])) ? $subscriber['last_name'] : '';
$fullName = (isset($subscriber['full_name'])) ? $subscriber['full_name'] : null;
if (!$firstName && !$lastName && !$fullName) return $subscriber['email'];
$fullName = is_null($fullName) ? sprintf('%s %s', $firstName, $lastName) : $fullName;
$fullName = trim(preg_replace('!\s\s+!', ' ', $fullName));
$fullName = $this->encodeAddressNamePart($fullName);
$subscriber = sprintf(
'%s <%s>',
$fullName,
$subscriber['email']
);
return $subscriber;
}
public function encodeAddressNamePart($name) {
if (mb_detect_encoding($name) === 'ASCII') return $name;
// encode non-ASCII string as per RFC 2047 (https://www.ietf.org/rfc/rfc2047.txt)
return sprintf('=?utf-8?B?%s?=', base64_encode($name));
}
public static function formatMailerErrorResult(MailerError $error) {
return [
'response' => false,
'error' => $error,
];
}
public static function formatMailerSendSuccessResult() {
return [
'response' => true,
];
}
}
@@ -0,0 +1,114 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer;
if (!defined('ABSPATH')) exit;
class MailerError {
const OPERATION_CONNECT = 'connect';
const OPERATION_SEND = 'send';
const OPERATION_AUTHORIZATION = 'authorization';
const OPERATION_DOMAIN_AUTHORIZATION = 'domain_authorization';
const OPERATION_INSUFFICIENT_PRIVILEGES = 'insufficient_privileges';
const OPERATION_SUBSCRIBER_LIMIT_REACHED = 'subscriber_limit_reached';
const OPERATION_EMAIL_LIMIT_REACHED = 'email_limit_reached';
const OPERATION_PENDING_APPROVAL = 'pending_approval';
const LEVEL_HARD = 'hard';
const LEVEL_SOFT = 'soft';
/** @var string */
private $operation;
/** @var string */
private $level;
/** @var string|null */
private $message;
/** @var int|null */
private $retryInterval;
/** @var array */
private $subscribersErrors = [];
/**
* @param string $operation
* @param string $level
* @param null|string $message
* @param int|null $retryInterval
* @param array $subscribersErrors
*/
public function __construct(
$operation,
$level,
$message = null,
$retryInterval = null,
array $subscribersErrors = []
) {
$this->operation = $operation;
$this->level = $level;
$this->message = $message;
$this->retryInterval = $retryInterval;
$this->subscribersErrors = $subscribersErrors;
}
/**
* @return string
*/
public function getOperation() {
return $this->operation;
}
/**
* @return string
*/
public function getLevel() {
return $this->level;
}
/**
* @return null|string
*/
public function getMessage() {
return $this->message;
}
/**
* @return int|null
*/
public function getRetryInterval() {
return $this->retryInterval;
}
/**
* @return SubscriberError[]
*/
public function getSubscriberErrors() {
return $this->subscribersErrors;
}
public function getMessageWithFailedSubscribers() {
$message = $this->message ?: '';
if (!$this->subscribersErrors) {
return $message;
}
$message .= $this->message ? ' ' : '';
if (count($this->subscribersErrors) === 1) {
$message .= __('Unprocessed subscriber:', 'mailpoet') . ' ';
} else {
$message .= __('Unprocessed subscribers:', 'mailpoet') . ' ';
}
$message .= implode(
', ',
array_map(function (SubscriberError $subscriberError) {
return "($subscriberError)";
}, $this->subscribersErrors)
);
return $message;
}
}
@@ -0,0 +1,170 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer;
if (!defined('ABSPATH')) exit;
use MailPoet\DI\ContainerWrapper;
use MailPoet\InvalidStateException;
use MailPoet\Mailer\Methods\AmazonSES;
use MailPoet\Mailer\Methods\ErrorMappers\AmazonSESMapper;
use MailPoet\Mailer\Methods\ErrorMappers\MailPoetMapper;
use MailPoet\Mailer\Methods\ErrorMappers\PHPMailMapper;
use MailPoet\Mailer\Methods\ErrorMappers\SendGridMapper;
use MailPoet\Mailer\Methods\ErrorMappers\SMTPMapper;
use MailPoet\Mailer\Methods\MailPoet;
use MailPoet\Mailer\Methods\PHPMail;
use MailPoet\Mailer\Methods\SendGrid;
use MailPoet\Mailer\Methods\SMTP;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Services\Bridge;
use MailPoet\Settings\SettingsController;
use MailPoet\Util\Url;
use MailPoet\WP\Functions as WPFunctions;
class MailerFactory {
/** @var SettingsController */
private $settings;
/** @var WPFunctions */
private $wp;
/** @var Mailer */
private $defaultMailer;
public function __construct(
SettingsController $settings,
WPFunctions $wp
) {
$this->settings = $settings;
$this->wp = $wp;
}
public function getDefaultMailer(): Mailer {
if ($this->defaultMailer === null) {
$this->defaultMailer = $this->buildMailer();
}
return $this->defaultMailer;
}
public function buildMailer(array $mailerConfig = null, array $sender = null, array $replyTo = null, string $returnPath = null): Mailer {
$sender = $this->getSenderNameAndAddress($sender);
$replyTo = $this->getReplyToNameAndAddress($sender, $replyTo);
$mailerConfig = $mailerConfig ?? $this->getMailerConfig();
$returnPath = $returnPath ?? $this->getReturnPathAddress($sender);
switch ($mailerConfig['method']) {
case Mailer::METHOD_AMAZONSES:
$mailerMethod = new AmazonSES(
$mailerConfig['region'],
$mailerConfig['access_key'],
$mailerConfig['secret_key'],
$sender,
$replyTo,
$returnPath,
new AmazonSESMapper(),
$this->wp,
ContainerWrapper::getInstance()->get(Url::class)
);
break;
case Mailer::METHOD_MAILPOET:
$mailerMethod = new MailPoet(
$mailerConfig['mailpoet_api_key'],
$sender,
$replyTo,
ContainerWrapper::getInstance()->get(MailPoetMapper::class),
ContainerWrapper::getInstance()->get(AuthorizedEmailsController::class),
ContainerWrapper::getInstance()->get(Bridge::class),
ContainerWrapper::getInstance()->get(Url::class)
);
break;
case Mailer::METHOD_SENDGRID:
$mailerMethod = new SendGrid(
$mailerConfig['api_key'],
$sender,
$replyTo,
new SendGridMapper(),
ContainerWrapper::getInstance()->get(Url::class)
);
break;
case Mailer::METHOD_PHPMAIL:
$mailerMethod = new PHPMail(
$sender,
$replyTo,
$returnPath,
new PHPMailMapper(),
ContainerWrapper::getInstance()->get(Url::class)
);
break;
case Mailer::METHOD_SMTP:
$mailerMethod = new SMTP(
$mailerConfig['host'],
$mailerConfig['port'],
(int)$mailerConfig['authentication'],
$mailerConfig['encryption'],
$sender,
$replyTo,
$returnPath,
new SMTPMapper(),
ContainerWrapper::getInstance()->get(Url::class),
$mailerConfig['login'],
$mailerConfig['password']
);
break;
default:
throw new InvalidStateException(__('Mailing method does not exist.', 'mailpoet'));
}
return new Mailer($mailerMethod);
}
private function getMailerConfig(): array {
$config = $this->settings->get(Mailer::MAILER_CONFIG_SETTING_NAME);
if (!$config || !isset($config['method'])) throw new InvalidStateException(__('Mailer is not configured.', 'mailpoet'));
return $config;
}
private function getSenderNameAndAddress(array $sender = null): array {
if (empty($sender)) {
$sender = $this->settings->get('sender', []);
if (empty($sender['address'])) throw new InvalidStateException(__('Sender name and email are not configured.', 'mailpoet'));
}
$fromName = $this->encodeAddressNamePart($sender['name'] ?? '');
return [
'from_name' => $fromName,
'from_email' => $sender['address'],
'from_name_email' => sprintf('%s <%s>', $fromName, $sender['address']),
];
}
private function getReplyToNameAndAddress(array $sender, array $replyTo = null): array {
if (!$replyTo) {
$replyTo = $this->settings->get('reply_to');
$replyTo['name'] = (!empty($replyTo['name'])) ?
$replyTo['name'] :
$sender['from_name'];
$replyTo['address'] = (!empty($replyTo['address'])) ?
$replyTo['address'] :
$sender['from_email'];
}
if (empty($replyTo['address'])) {
$replyTo['address'] = $sender['from_email'];
}
$replyToName = $this->encodeAddressNamePart($replyTo['name'] ?? '');
return [
'reply_to_name' => $replyToName,
'reply_to_email' => $replyTo['address'],
'reply_to_name_email' => sprintf('%s <%s>', $replyToName, $replyTo['address']),
];
}
private function getReturnPathAddress(array $sender): ?string {
$bounceAddress = (string)$this->settings->get('bounce.address');
return $this->wp->isEmail($bounceAddress) ? $bounceAddress : $sender['from_email'];
}
private function encodeAddressNamePart($name): string {
if (mb_detect_encoding($name) === 'ASCII') return $name;
// encode non-ASCII string as per RFC 2047 (https://www.ietf.org/rfc/rfc2047.txt)
return sprintf('=?utf-8?B?%s?=', base64_encode($name));
}
}
@@ -0,0 +1,404 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer;
if (!defined('ABSPATH')) exit;
use MailPoet\Logging\LoggerFactory;
use MailPoet\Settings\SettingsController;
/**
* @phpstan-type MailerLogError array{
* "error_code"?: non-empty-string,
* "error_message": string,
* "operation": string
* }
* @phpstan-type MailerLogData array{
* "sent": array<string,int>,
* "started": int,
* "status": ?string,
* "retry_attempt": ?int,
* "retry_at": ?int,
* "error": ?MailerLogError,
* "transactional_email_last_error_at": ?int,
* "transactional_email_error_count": ?int,
* }
*/
class MailerLog {
const SETTING_NAME = 'mta_log';
const STATUS_PAUSED = 'paused';
const RETRY_ATTEMPTS_LIMIT = 3;
const RETRY_INTERVAL = 120; // seconds
/**
* @param MailerLogData|null $mailerLog
* @return MailerLogData
*/
public static function getMailerLog(array $mailerLog = null): array {
if ($mailerLog) return $mailerLog;
$settings = SettingsController::getInstance();
$mailerLog = $settings->get(self::SETTING_NAME);
if (!$mailerLog) {
$mailerLog = self::createMailerLog();
}
/**
* The old "sent" entry was just the number of emails.
* We need to update this entry to the new data structure.
*/
$mailerLog['sent'] = is_numeric($mailerLog['sent']) ? [self::sentEntriesDate(time() - 1) => $mailerLog['sent']] : (array)$mailerLog['sent'];
return $mailerLog;
}
/**
* @return MailerLogData
*/
public static function createMailerLog(): array {
$mailerLog = [
'sent' => [],
'started' => time(),
'status' => null,
'retry_attempt' => null,
'retry_at' => null,
'error' => null,
'transactional_email_last_error_at' => null,
'transactional_email_error_count' => null,
];
$settings = SettingsController::getInstance();
$settings->set(self::SETTING_NAME, $mailerLog);
return $mailerLog;
}
/**
* @return MailerLogData
*/
public static function resetMailerLog(): array {
return self::createMailerLog();
}
/**
* @param MailerLogData $mailerLog
* @return MailerLogData
*/
public static function updateMailerLog(array $mailerLog): array {
$mailerLog = self::removeOutdatedSentInformationFromMailerlog($mailerLog);
$settings = SettingsController::getInstance();
$settings->set(self::SETTING_NAME, $mailerLog);
return $mailerLog;
}
/**
* @param MailerLogData|null $mailerLog
* @return null
* @throws \Exception
*/
public static function enforceExecutionRequirements(array $mailerLog = null) {
$mailerLog = self::getMailerLog($mailerLog);
if ($mailerLog['retry_attempt'] === self::RETRY_ATTEMPTS_LIMIT) {
$mailerLog = self::pauseSending($mailerLog);
}
if (self::isSendingPaused($mailerLog)) {
throw new \Exception(__('Sending has been paused.', 'mailpoet'));
}
if (self::isSendingWaitingForRetry($mailerLog)) {
throw new \Exception(__('Sending is waiting to be retried.', 'mailpoet'));
} else {
$mailerLog['retry_at'] = null;
self::updateMailerLog($mailerLog);
}
// ensure that sending frequency has not been reached
if (self::isSendingLimitReached($mailerLog)) {
throw new \Exception(__('Sending frequency limit has been reached.', 'mailpoet'));
}
return null;
}
/**
* @param MailerLogData $mailerLog
* @return MailerLogData
*/
public static function pauseSending($mailerLog): array {
$mailerLog['status'] = self::STATUS_PAUSED;
$mailerLog['retry_attempt'] = null;
$mailerLog['retry_at'] = null;
$mailerLog['transactional_email_last_error_at'] = null;
$mailerLog['transactional_email_error_count'] = null;
return self::updateMailerLog($mailerLog);
}
/**
* @return MailerLogData
*/
public static function resumeSending(): array {
return self::resetMailerLog();
}
/**
* Process error, doesn't increase retry_attempt so it will not block sending
*
* @param string $operation
* @param string $errorMessage
* @param int $retryInterval
*
* @throws \Exception
*/
public static function processNonBlockingError(string $operation, string $errorMessage, int $retryInterval = self::RETRY_INTERVAL) {
$mailerLog = self::getMailerLog();
$mailerLog['retry_at'] = time() + $retryInterval;
$mailerLog = self::setError($mailerLog, $operation, $errorMessage);
self::updateMailerLog($mailerLog);
self::enforceExecutionRequirements();
}
/**
* Process error, increase retry_attempt and block sending if it goes above RETRY_INTERVAL
*
* @param string $operation
* @param string $errorMessage
* @param string $errorCode
* @param bool $pauseSending
*
* @throws \Exception
*/
public static function processError(
string $operation,
string $errorMessage,
string $errorCode = null,
bool $pauseSending = false,
int $throttledBatchSize = null
) {
$mailerLog = self::getMailerLog();
if (!isset($throttledBatchSize) || $throttledBatchSize === 1) {
$mailerLog['retry_attempt']++;
}
$mailerLog['retry_at'] = time() + self::RETRY_INTERVAL;
$mailerLog = self::setError($mailerLog, $operation, $errorMessage, $errorCode);
self::updateMailerLog($mailerLog);
if ($pauseSending) {
LoggerFactory::getInstance()->getLogger(LoggerFactory::TOPIC_SENDING)->error(
'Email sending was paused due an error',
[
'error_message' => $errorMessage,
'error_code' => $errorCode,
]
);
self::pauseSending($mailerLog);
}
self::enforceExecutionRequirements();
}
/**
* Process error, increase transactional_email_error_count and pauses sending if it reaches retry limit
* This method is meant to be used for processing errors when sending transactional emails
* like: Confirmation Email, Preview email, Stats Notification etc.
*
* @throws \Exception
*/
public static function processTransactionalEmailError(
string $operation,
string $errorMessage,
?string $errorCode = null
): void {
$mailerLog = self::getMailerLog();
$lastErrorTime = $mailerLog['transactional_email_last_error_at'] ?? null;
$ignoreErrorThreshold = time() - (2 * 60); // 2 minutes ago
// We want to log the error max one time per 2 minutes
if ($lastErrorTime && $lastErrorTime > $ignoreErrorThreshold) {
return;
}
$mailerLog = self::setError($mailerLog, $operation, $errorMessage, $errorCode);
$mailerLog['transactional_email_last_error_at'] = time();
$mailerLog['transactional_email_error_count'] = ($mailerLog['transactional_email_error_count'] ?? 0) + 1;
self::updateMailerLog($mailerLog);
if ($mailerLog['transactional_email_error_count'] >= self::RETRY_ATTEMPTS_LIMIT) {
LoggerFactory::getInstance()->getLogger(LoggerFactory::TOPIC_SENDING)->error(
'Email sending was paused due a transactional email error',
[
'error_message' => $errorMessage,
'error_code' => $errorCode,
]
);
self::pauseSending($mailerLog);
}
}
/**
* @param MailerLogData $mailerLog
* @param string $operation
* @param string $errorMessage
* @param string|null $errorCode
* @return MailerLogData
*/
public static function setError(
array $mailerLog,
string $operation,
string $errorMessage,
string $errorCode = null
): array {
$mailerLog['error'] = [
'operation' => $operation,
'error_message' => $errorMessage,
];
if ($errorCode) {
$mailerLog['error']['error_code'] = $errorCode;
}
return $mailerLog;
}
/**
* @param MailerLogData|null $mailerLog
* @return MailerLogError|null
*/
public static function getError(array $mailerLog = null): ?array {
$mailerLog = self::getMailerLog($mailerLog);
return isset($mailerLog['error']) ? $mailerLog['error'] : null;
}
/**
* @return MailerLogData|null
*/
public static function incrementSentCount(): ?array {
$settings = SettingsController::getInstance();
$mailerConfig = $settings->get(Mailer::MAILER_CONFIG_SETTING_NAME);
$mailerLog = self::getMailerLog();
// do not increment count if sending limit is reached
if (self::isSendingLimitReached($mailerLog)) {
return null;
}
// clear previous retry count, errors, etc.
if ($mailerLog['error'] !== null) {
$mailerLog = self::clearSendingErrorLog($mailerLog);
}
// do not enforce sending limit for MailPoet's sending method
if ($mailerConfig['method'] === Mailer::METHOD_MAILPOET) {
return null;
}
$time = self::sentEntriesDate();
if (!isset($mailerLog['sent'][$time])) {
$mailerLog['sent'][$time] = 0;
}
$mailerLog['sent'][$time]++;
return self::updateMailerLog($mailerLog);
}
/**
* @param MailerLogData $mailerLog
* @return MailerLogData
*/
public static function clearSendingErrorLog(array $mailerLog): array {
$mailerLog['retry_attempt'] = null;
$mailerLog['retry_at'] = null;
$mailerLog['error'] = null;
$mailerLog['transactional_email_last_error_at'] = null;
$mailerLog['transactional_email_error_count'] = null;
return self::updateMailerLog($mailerLog);
}
/**
* @param MailerLogData|null $mailerLog
* @return bool
*/
public static function isSendingLimitReached(array $mailerLog = null): bool {
$settings = SettingsController::getInstance();
$mailerConfig = $settings->get(Mailer::MAILER_CONFIG_SETTING_NAME);
// do not enforce sending limit for MailPoet's sending method
if ($mailerConfig['method'] === Mailer::METHOD_MAILPOET) return false;
$mailerLog = self::getMailerLog($mailerLog);
if (empty($mailerConfig['frequency'])) {
$defaultSettings = $settings->getAllDefaults();
$mailerConfig['frequency'] = $defaultSettings['mta']['frequency'];
}
$frequencyInterval = (int)$mailerConfig['frequency']['interval'] * Mailer::SENDING_LIMIT_INTERVAL_MULTIPLIER;
$frequencyLimit = (int)$mailerConfig['frequency']['emails'];
$sent = self::sentSince($frequencyInterval, $mailerLog);
return $sent >= $frequencyLimit;
}
/**
* @param int|null $sinceSeconds
* @param MailerLogData|null $mailerLog
* @return int
*/
public static function sentSince(int $sinceSeconds = null, array $mailerLog = null): int {
if ($sinceSeconds === null) {
$settings = SettingsController::getInstance();
$mailerConfig = $settings->get(Mailer::MAILER_CONFIG_SETTING_NAME);
if (empty($mailerConfig['frequency'])) {
$defaultSettings = $settings->getAllDefaults();
$mailerConfig['frequency'] = $defaultSettings['mta']['frequency'];
}
$sinceSeconds = (int)$mailerConfig['frequency']['interval'] * Mailer::SENDING_LIMIT_INTERVAL_MULTIPLIER;
}
$sinceDate = date('Y-m-d H:i:s', time() - $sinceSeconds);
$mailerLog = self::getMailerLog($mailerLog);
return (int)array_sum(
array_filter(
(array)$mailerLog['sent'],
function($date) use ($sinceDate): bool {
return $sinceDate <= $date;
},
\ARRAY_FILTER_USE_KEY
)
);
}
/**
* Clears "sent" section of the mailer log from outdated entries.
*
* @param MailerLogData|null $mailerLog
* @return MailerLogData
*/
private static function removeOutdatedSentInformationFromMailerlog(array $mailerLog = null): array {
$settings = SettingsController::getInstance();
$mailerConfig = $settings->get(Mailer::MAILER_CONFIG_SETTING_NAME);
$frequencyInterval = (int)$mailerConfig['frequency']['interval'] * Mailer::SENDING_LIMIT_INTERVAL_MULTIPLIER;
$sinceDate = self::sentEntriesDate(time() - $frequencyInterval);
$mailerLog = self::getMailerLog($mailerLog);
$mailerLog['sent'] = array_filter(
(array)$mailerLog['sent'],
function($date) use ($sinceDate): bool {
return $sinceDate <= $date;
},
\ARRAY_FILTER_USE_KEY
);
return $mailerLog;
}
/**
* @param int|null $timestamp
* @return string
*/
private static function sentEntriesDate(int $timestamp = null): string {
return date('Y-m-d H:i:s', $timestamp ?? time());
}
/**
* @param MailerLogData|null $mailerLog
* @return bool
*/
public static function isSendingPaused(array $mailerLog = null): bool {
$mailerLog = self::getMailerLog($mailerLog);
return $mailerLog['status'] === self::STATUS_PAUSED;
}
/**
* @param MailerLogData|null $mailerLog
* @return bool
*/
public static function isSendingWaitingForRetry(array $mailerLog = null): bool {
$mailerLog = self::getMailerLog($mailerLog);
$retryAt = $mailerLog['retry_at'] ?? null;
return $retryAt && (time() <= $retryAt);
}
}
@@ -0,0 +1,69 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer;
if (!defined('ABSPATH')) exit;
use MailPoet\Entities\NewsletterEntity;
use MailPoet\Entities\SubscriberEntity;
class MetaInfo {
public function getSendingTestMetaInfo() {
return $this->makeMetaInfo('sending_test', 'unknown', 'administrator');
}
public function getPreviewMetaInfo() {
return $this->makeMetaInfo('preview', 'unknown', 'administrator');
}
public function getStatsNotificationMetaInfo() {
return $this->makeMetaInfo('email_stats_notification', 'unknown', 'administrator');
}
public function getWordPressTransactionalMetaInfo(SubscriberEntity $subscriber = null) {
return $this->makeMetaInfo(
'transactional',
$subscriber ? $subscriber->getStatus() : 'unknown',
$subscriber ? $subscriber->getSource() : 'unknown'
);
}
public function getConfirmationMetaInfo(SubscriberEntity $subscriber) {
return $this->makeMetaInfo('confirmation', $subscriber->getStatus(), $subscriber->getSource());
}
public function getNewSubscriberNotificationMetaInfo() {
return $this->makeMetaInfo('new_subscriber_notification', 'unknown', 'administrator');
}
public function getNewsletterMetaInfo(NewsletterEntity $newsletter, SubscriberEntity $subscriber) {
$type = $newsletter->getType();
switch ($newsletter->getType()) {
case NewsletterEntity::TYPE_AUTOMATIC:
$group = !is_null($newsletter->getOptionValue('group')) ? $newsletter->getOptionValue('group') : 'unknown';
$event = !is_null($newsletter->getOptionValue('event')) ? $newsletter->getOptionValue('event') : 'unknown';
$type = sprintf('automatic_%s_%s', $group, $event);
break;
case NewsletterEntity::TYPE_STANDARD:
$type = 'newsletter';
break;
case NewsletterEntity::TYPE_WELCOME:
$type = 'welcome';
break;
case NewsletterEntity::TYPE_NOTIFICATION:
case NewsletterEntity::TYPE_NOTIFICATION_HISTORY:
$type = 'post_notification';
break;
}
return $this->makeMetaInfo($type, $subscriber->getStatus(), $subscriber->getSource());
}
private function makeMetaInfo($emailType, $subscriberStatus, $subscriberSource) {
return [
'email_type' => $emailType,
'subscriber_status' => $subscriberStatus,
'subscriber_source' => $subscriberSource ?: 'unknown',
];
}
}
@@ -0,0 +1,249 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\Methods;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\Methods\Common\BlacklistCheck;
use MailPoet\Mailer\Methods\ErrorMappers\AmazonSESMapper;
use MailPoet\Util\Url;
use MailPoet\WP\Functions as WPFunctions;
use PHPMailer\PHPMailer\PHPMailer;
class AmazonSES extends PHPMailerMethod {
/** @var string */
public $awsAccessKey;
/** @var string */
public $awsSecretKey;
/** @var string */
public $awsRegion;
/** @var string */
public $awsEndpoint;
/** @var string */
public $awsSigningAlgorithm;
/** @var string */
public $awsService;
/** @var string */
public $awsTerminationString;
/** @var string */
public $hashAlgorithm;
/** @var string */
public $url;
/** @var string */
public $rawMessage;
/** @var string */
public $date;
/** @var string */
public $dateWithoutTime;
/** @var string[] */
private $availableRegions = [
'US East (N. Virginia)' => 'us-east-1',
'US East (Ohio)' => 'us-east-2',
'US West (N. California)' => 'us-west-1',
'US West (Oregon)' => 'us-west-2',
'EU (Ireland)' => 'eu-west-1',
'EU (London)' => 'eu-west-2',
'EU (Paris)' => 'eu-west-3',
'EU (Milan)' => 'eu-south-1',
'EU (Frankfurt)' => 'eu-central-1',
'EU (Stockholm)' => 'eu-north-1',
'Canada (Central)' => 'ca-central-1',
'China (Beijing)' => 'cn-north-1',
'China (Ningxia)' => 'cn-northwest-1',
'Africa (Cape Town)' => 'af-south-1',
'Asia Pacific (Hong Kong)' => 'ap-east-1',
'Asia Pacific (Jakarta)' => 'ap-southeast-3',
'Asia Pacific (Mumbai)' => 'ap-south-1',
'Asia Pacific (Seoul)' => 'ap-northeast-2',
'Asia Pacific (Osaka)' => 'ap-northeast-3',
'Asia Pacific (Singapore)' => 'ap-southeast-1',
'Asia Pacific (Sydney)' => 'ap-southeast-2',
'Asia Pacific (Tokyo)' => 'ap-northeast-1',
'Middle East (Bahrain)' => 'me-south-1',
'South America (Sao Paulo)' => 'sa-east-1',
'AWS GovCloud (US)' => 'us-gov-west-1',
];
/** @var AmazonSESMapper */
protected $errorMapper;
/** @var WPFunctions */
protected $wp;
public function __construct(
$region,
$accessKey,
$secretKey,
$sender,
$replyTo,
$returnPath,
AmazonSESMapper $errorMapper,
WPFunctions $wp,
Url $urlUtils
) {
$this->awsAccessKey = $accessKey;
$this->awsSecretKey = $secretKey;
$this->awsRegion = (in_array($region, $this->availableRegions)) ? $region : false;
if (!$this->awsRegion) {
throw new \Exception(__('Unsupported Amazon SES region', 'mailpoet'));
}
$this->awsEndpoint = sprintf('email.%s.amazonaws.com', $this->awsRegion);
$this->awsSigningAlgorithm = 'AWS4-HMAC-SHA256';
$this->awsService = 'ses';
$this->awsTerminationString = 'aws4_request';
$this->hashAlgorithm = 'sha256';
$this->url = 'https://' . $this->awsEndpoint;
$this->sender = $sender;
$this->replyTo = $replyTo;
$this->returnPath = $returnPath;
$this->date = gmdate('Ymd\THis\Z');
$this->dateWithoutTime = gmdate('Ymd');
$this->errorMapper = $errorMapper;
$this->wp = $wp;
$this->urlUtils = $urlUtils;
$this->blacklist = new BlacklistCheck();
$this->mailer = $this->buildMailer();
}
public function send($newsletter, $subscriber, $extraParams = []): array {
if ($this->blacklist->isBlacklisted($subscriber)) {
$error = $this->errorMapper->getBlacklistError($subscriber);
return Mailer::formatMailerErrorResult($error);
}
try {
$result = $this->wp->wpRemotePost(
$this->url,
$this->request($newsletter, $subscriber, $extraParams)
);
} catch (\Exception $e) {
$error = $this->errorMapper->getErrorFromException($e, $subscriber);
return Mailer::formatMailerErrorResult($error);
}
if (is_wp_error($result)) {
$error = $this->errorMapper->getConnectionError($result->get_error_message());
return Mailer::formatMailerErrorResult($error);
}
if ($this->wp->wpRemoteRetrieveResponseCode($result) !== 200) {
$response = simplexml_load_string($this->wp->wpRemoteRetrieveBody($result));
$error = $this->errorMapper->getErrorFromResponse($response, $subscriber);
return Mailer::formatMailerErrorResult($error);
}
return Mailer::formatMailerSendSuccessResult();
}
public function buildMailer(): PHPMailer {
return new PHPMailer(true);
}
public function getBody($newsletter, $subscriber, $extraParams = []) {
/* Configure mailer and call preSend() method to prepare message */
$mailer = $this->configureMailerWithMessage($newsletter, $subscriber, $extraParams);
$mailer->preSend();
/* When message is prepared, we can get the raw message */
$this->rawMessage = $mailer->getSentMIMEMessage();
return [
'Action' => 'SendRawEmail',
'Version' => '2010-12-01',
'Source' => $this->sender['from_name_email'],
'RawMessage.Data' => $this->encodeMessage($this->rawMessage),
];
}
public function encodeMessage(string $message) {
return base64_encode($message);
}
public function request($newsletter, $subscriber, $extraParams = []) {
$body = array_map('urlencode', $this->getBody($newsletter, $subscriber, $extraParams));
return [
'timeout' => 10,
'httpversion' => '1.1',
'method' => 'POST',
'headers' => [
'Host' => $this->awsEndpoint,
'Authorization' => $this->signRequest($body),
'X-Amz-Date' => $this->date,
],
'body' => urldecode(http_build_query($body, '', '&')),
];
}
public function signRequest($body) {
$stringToSign = $this->createStringToSign(
$this->getCredentialScope(),
$this->getCanonicalRequest($body)
);
$signature = hash_hmac(
$this->hashAlgorithm,
$stringToSign,
$this->getSigningKey()
);
return sprintf(
'%s Credential=%s/%s, SignedHeaders=host;x-amz-date, Signature=%s',
$this->awsSigningAlgorithm,
$this->awsAccessKey,
$this->getCredentialScope(),
$signature
);
}
public function getCredentialScope() {
return sprintf(
'%s/%s/%s/%s',
$this->dateWithoutTime,
$this->awsRegion,
$this->awsService,
$this->awsTerminationString
);
}
public function getCanonicalRequest($body) {
return implode("\n", [
'POST',
'/',
'',
'host:' . $this->awsEndpoint,
'x-amz-date:' . $this->date,
'',
'host;x-amz-date',
hash($this->hashAlgorithm, urldecode(http_build_query($body, '', '&'))),
]);
}
public function createStringToSign($credentialScope, $canonicalRequest) {
return implode("\n", [
$this->awsSigningAlgorithm,
$this->date,
$credentialScope,
hash($this->hashAlgorithm, $canonicalRequest),
]);
}
public function getSigningKey() {
$dateKey = hash_hmac(
$this->hashAlgorithm,
$this->dateWithoutTime,
'AWS4' . $this->awsSecretKey,
true
);
$regionKey = hash_hmac(
$this->hashAlgorithm,
$this->awsRegion,
$dateKey,
true
);
$serviceKey = hash_hmac(
$this->hashAlgorithm,
$this->awsService,
$regionKey,
true
);
return hash_hmac(
$this->hashAlgorithm,
$this->awsTerminationString,
$serviceKey,
true
);
}
}
@@ -0,0 +1,35 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer\Methods\Common;
if (!defined('ABSPATH')) exit;
use MailPoet\Subscription\Blacklist;
class BlacklistCheck {
/** @var Blacklist */
private $blacklist;
public function __construct(
Blacklist $blacklist = null
) {
if (is_null($blacklist)) {
$blacklist = new Blacklist();
}
$this->blacklist = $blacklist;
}
public function isBlacklisted($subscriber) {
$email = $this->getSubscriberEmailForBlacklistCheck($subscriber);
return $this->blacklist->isBlacklisted($email);
}
private function getSubscriberEmailForBlacklistCheck($subscriberString) {
preg_match('!(?P<name>.*?)\s<(?P<email>.*?)>!', $subscriberString, $subscriberData);
if (!isset($subscriberData['email'])) {
return $subscriberString;
}
return $subscriberData['email'];
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,44 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\Methods\ErrorMappers;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\SubscriberError;
class AmazonSESMapper {
use BlacklistErrorMapperTrait;
use ConnectionErrorMapperTrait;
const METHOD = Mailer::METHOD_AMAZONSES;
public function getErrorFromException(\Exception $e, $subscriber) {
$level = MailerError::LEVEL_HARD;
if (strpos($e->getMessage(), 'Invalid address') !== false && strpos($e->getMessage(), '(to):') !== false) {
$level = MailerError::LEVEL_SOFT;
}
$subscriberErrors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, $level, $e->getMessage(), null, $subscriberErrors);
}
/**
* @see https://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html
* @return MailerError
*/
public function getErrorFromResponse($response, $subscriber) {
$message = ($response) ?
$response->Error->Message->__toString() : // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
// translators: %s is the name of the method.
sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_AMAZONSES);
$level = MailerError::LEVEL_HARD;
if ($response && $response->Error->Code->__toString() === 'MessageRejected') { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$level = MailerError::LEVEL_SOFT;
}
$subscriberErrors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, $level, $message, null, $subscriberErrors);
}
}
@@ -0,0 +1,18 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer\Methods\ErrorMappers;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\SubscriberError;
trait BlacklistErrorMapperTrait {
public function getBlacklistError($subscriber) {
// translators: %s is the name of the method.
$message = sprintf(__('%s has returned an unknown error.', 'mailpoet'), self::METHOD);
$subscriberErrors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, MailerError::LEVEL_SOFT, $message, null, $subscriberErrors);
}
}
@@ -0,0 +1,18 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer\Methods\ErrorMappers;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\MailerError;
trait ConnectionErrorMapperTrait {
public function getConnectionError($message) {
return new MailerError(
MailerError::OPERATION_CONNECT,
MailerError::LEVEL_HARD,
$message
);
}
}
@@ -0,0 +1,280 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer\Methods\ErrorMappers;
if (!defined('ABSPATH')) exit;
use InvalidArgumentException;
use MailPoet\Config\ServicesChecker;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\SubscriberError;
use MailPoet\Services\Bridge\API;
use MailPoet\Util\Helpers;
use MailPoet\Util\License\Features\Subscribers as SubscribersFeature;
use MailPoet\Util\Notices\PendingApprovalNotice;
use MailPoet\Util\Notices\UnauthorizedEmailNotice;
use MailPoet\WP\Functions as WPFunctions;
use MailPoetVendor\Carbon\Carbon;
class MailPoetMapper {
use BlacklistErrorMapperTrait;
use ConnectionErrorMapperTrait;
const METHOD = Mailer::METHOD_MAILPOET;
const TEMPORARY_UNAVAILABLE_RETRY_INTERVAL = 300; // seconds
/** @var ServicesChecker */
private $servicesChecker;
/** @var SubscribersFeature */
private $subscribersFeature;
/** @var WPFunctions */
private $wp;
/** @var PendingApprovalNotice */
private $pendingApprovalNotice;
public function __construct(
ServicesChecker $servicesChecker,
SubscribersFeature $subscribers,
WPFunctions $wp,
PendingApprovalNotice $pendingApprovalNotice
) {
$this->servicesChecker = $servicesChecker;
$this->subscribersFeature = $subscribers;
$this->wp = $wp;
$this->pendingApprovalNotice = $pendingApprovalNotice;
}
public function getInvalidApiKeyError() {
return new MailerError(
MailerError::OPERATION_SEND,
MailerError::LEVEL_HARD,
__('MailPoet API key is invalid!', 'mailpoet')
);
}
public function getErrorForResult(array $result, $subscribers, $sender = null, $newsletter = null) {
$level = MailerError::LEVEL_HARD;
$operation = MailerError::OPERATION_SEND;
$retryInterval = null;
$subscribersErrors = [];
$resultCode = !empty($result['code']) ? $result['code'] : null;
switch ($resultCode) {
case API::RESPONSE_CODE_NOT_ARRAY:
$message = __('JSON input is not an array', 'mailpoet');
break;
case API::RESPONSE_CODE_PAYLOAD_ERROR:
$resultParsed = json_decode($result['message'], true);
$message = __('Error while sending.', 'mailpoet');
if (is_array($resultParsed)) {
try {
$subscribersErrors = $this->getSubscribersErrors($resultParsed, $subscribers);
$level = MailerError::LEVEL_SOFT;
} catch (InvalidArgumentException $e) {
$message .= ' ' . $e->getMessage();
}
break;
}
$appendedMessage = ' ' . $result['message'];
if (isset($result['error']) && in_array($result['error'], [API::ERROR_MESSAGE_DMRAC, API::ERROR_MESSAGE_BULK_EMAIL_FORBIDDEN])) {
$appendedMessage = $this->getDmarcMessage($result, $sender);
if ($result['error'] === API::ERROR_MESSAGE_BULK_EMAIL_FORBIDDEN) {
$operation = MailerError::OPERATION_DOMAIN_AUTHORIZATION;
$level = MailerError::LEVEL_SOFT;
}
}
$message .= $appendedMessage;
break;
case API::RESPONSE_CODE_INTERNAL_SERVER_ERROR:
case API::RESPONSE_CODE_BAD_GATEWAY:
case API::RESPONSE_CODE_TEMPORARY_UNAVAILABLE:
case API::RESPONSE_CODE_GATEWAY_TIMEOUT:
$message = __('Email service is temporarily not available, please try again in a few minutes.', 'mailpoet');
$retryInterval = self::TEMPORARY_UNAVAILABLE_RETRY_INTERVAL;
break;
case API::RESPONSE_CODE_CAN_NOT_SEND:
[$operation, $message] = $this->getCanNotSendError($result, $sender);
break;
case API::RESPONSE_CODE_KEY_INVALID:
case API::RESPONSE_CODE_PAYLOAD_TOO_BIG:
default:
$message = $result['message'];
}
return new MailerError($operation, $level, $message, $retryInterval, $subscribersErrors);
}
private function getSubscribersErrors($resultParsed, $subscribers) {
$errors = [];
foreach ($resultParsed as $resultError) {
if (!is_array($resultError) || !isset($resultError['index']) || !isset($subscribers[$resultError['index']])) {
throw new InvalidArgumentException(__('Invalid MSS response format.', 'mailpoet'));
}
$subscriberErrors = [];
if (isset($resultError['errors']) && is_array($resultError['errors'])) {
array_walk_recursive($resultError['errors'], function($item) use (&$subscriberErrors) {
$subscriberErrors[] = $item;
});
}
$message = join(', ', $subscriberErrors);
$errors[] = new SubscriberError($subscribers[$resultError['index']], $message);
}
return $errors;
}
private function getUnauthorizedEmailMessage($sender) {
$email = $sender ? $sender['from_email'] : __('Unknown address', 'mailpoet');
$validationError = ['invalid_sender_address' => $email];
$notice = new UnauthorizedEmailNotice($this->wp, null);
$message = $notice->getMessage($validationError);
return $message;
}
private function getSubscribersLimitReachedMessage(): string {
$message = __('You have reached the subscriber limit of your plan. Please [link1]upgrade your plan[/link1], or [link2]contact our support team[/link2] if you have any questions.', 'mailpoet');
$message = Helpers::replaceLinkTags(
$message,
'https://account.mailpoet.com/account/',
[
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
'link1'
);
$message = Helpers::replaceLinkTags(
$message,
'https://www.mailpoet.com/support/',
[
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
'link2'
);
return "{$message}<br/>";
}
private function getAccountBannedMessage(): string {
$message = __('The MailPoet Sending Service has been temporarily suspended for your site due to a high number of [link1]undeliverable emails or emails marked as unwanted by recipients[/link1]. Please [link2]contact our support team[/link2] to resolve the issue.', 'mailpoet');
$message = Helpers::replaceLinkTags(
$message,
'https://kb.mailpoet.com/article/231-sending-does-not-work#suspended',
[
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
'link1'
);
$message = Helpers::replaceLinkTags(
$message,
'https://www.mailpoet.com/support-for-banned-users/',
[
'target' => '_blank',
'rel' => 'noopener noreferrer',
],
'link2'
);
return "{$message}<br/>";
}
private function getDmarcMessage($result, $sender): string {
$messageToAppend = __('[link1]Click here to start the authentication[/link1].', 'mailpoet');
$senderEmail = $sender['from_email'] ?? '';
$appendMessage = Helpers::replaceLinkTags(
$messageToAppend,
'#',
[
'class' => 'mailpoet-js-button-authorize-email-and-sender-domain',
'data-email' => $senderEmail,
'data-type' => 'domain',
'rel' => 'noopener noreferrer',
],
'link1'
);
$final = ' ' . $result['message'] . ' ' . $appendMessage;
return $final;
}
private function getEmailVolumeLimitReachedMessage(): string {
$partialApiKey = $this->servicesChecker->generatePartialApiKey();
$emailVolumeLimit = $this->subscribersFeature->getEmailVolumeLimit();
$date = Carbon::now()->startOfMonth()->addMonth();
if ($emailVolumeLimit) {
$message = sprintf(
// translators: %1$s is email volume limit and %2$s the date when you can resume sending.
__('You have sent more emails this month than your MailPoet plan includes (%1$s), and sending has been temporarily paused. To continue sending with MailPoet Sending Service please [link]upgrade your plan[/link], or wait until sending is automatically resumed on %2$s.', 'mailpoet'),
$emailVolumeLimit,
$this->wp->dateI18n($this->wp->getOption('date_format'), $date->getTimestamp())
);
} else {
$message = sprintf(
// translators: %1$s the date when you can resume sending.
__('You have sent more emails this month than your MailPoet plan includes, and sending has been temporarily paused. To continue sending with MailPoet Sending Service please [link]upgrade your plan[/link], or wait until sending is automatically resumed on %1$s.', 'mailpoet'),
$this->wp->dateI18n($this->wp->getOption('date_format'), $date->getTimestamp())
);
}
$message = Helpers::replaceLinkTags(
$message,
"https://account.mailpoet.com/orders/upgrade/{$partialApiKey}",
[
'target' => '_blank',
'rel' => 'noopener noreferrer',
]
);
return "{$message}<br/>";
}
/**
* Returns error $message and $operation for API::RESPONSE_CODE_CAN_NOT_SEND
*/
private function getCanNotSendError(array $result, $sender): array {
if ($result['error'] === API::ERROR_MESSAGE_PENDING_APPROVAL) {
$operation = MailerError::OPERATION_PENDING_APPROVAL;
$message = $this->pendingApprovalNotice->getPendingApprovalMessage() . '<br/>';
return [$operation, $message];
}
// Backward compatibility for older blocked keys.
// Exceeded subscribers limit used to use the same error message as insufficient privileges.
// We can change the message to "Insufficient privileges" like wording a couple of months after releasing SHOP-1228
if ($result['error'] === API::ERROR_MESSAGE_INSUFFICIENT_PRIVILEGES) {
$operation = MailerError::OPERATION_INSUFFICIENT_PRIVILEGES;
$message = $this->getSubscribersLimitReachedMessage();
return [$operation, $message];
}
if ($result['error'] === API::ERROR_MESSAGE_SUBSCRIBERS_LIMIT_REACHED) {
$operation = MailerError::OPERATION_SUBSCRIBER_LIMIT_REACHED;
$message = $this->getSubscribersLimitReachedMessage();
return [$operation, $message];
}
if ($result['error'] === API::ERROR_MESSAGE_EMAIL_VOLUME_LIMIT_REACHED) {
$operation = MailerError::OPERATION_EMAIL_LIMIT_REACHED;
$message = $this->getEmailVolumeLimitReachedMessage();
return [$operation, $message];
}
if ($result['error'] === API::ERROR_MESSAGE_INVALID_FROM) {
$operation = MailerError::OPERATION_AUTHORIZATION;
$message = $this->getUnauthorizedEmailMessage($sender);
return [$operation, $message];
}
$message = $this->getAccountBannedMessage();
$operation = MailerError::OPERATION_SEND;
return [$operation, $message];
}
}
@@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\Methods\ErrorMappers;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\Mailer;
class PHPMailMapper extends PHPMailerMapper {
use BlacklistErrorMapperTrait;
public const METHOD = Mailer::METHOD_PHPMAIL;
protected function getMethodName(): string {
return self::METHOD;
}
}
@@ -0,0 +1,32 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\Methods\ErrorMappers;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\SubscriberError;
abstract class PHPMailerMapper {
use ConnectionErrorMapperTrait;
public function getErrorFromException(\Exception $e, $subscriber) {
$level = MailerError::LEVEL_HARD;
if (strpos($e->getMessage(), 'Invalid address') === 0) {
$level = MailerError::LEVEL_SOFT;
}
$subscriberErrors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, $level, $e->getMessage(), null, $subscriberErrors);
}
public function getErrorForSubscriber($subscriber) {
// translators: %s is the name of the method.
$message = sprintf(__('%s has returned an unknown error.', 'mailpoet'), $this->getMethodName());
$subscriberErrors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, MailerError::LEVEL_HARD, $message, null, $subscriberErrors);
}
abstract protected function getMethodName(): string;
}
@@ -0,0 +1,18 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\Methods\ErrorMappers;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\Mailer;
class SMTPMapper extends PHPMailerMapper {
use BlacklistErrorMapperTrait;
public const METHOD = Mailer::METHOD_SMTP;
protected function getMethodName(): string {
return self::METHOD;
}
}
@@ -0,0 +1,31 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer\Methods\ErrorMappers;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\SubscriberError;
class SendGridMapper {
use BlacklistErrorMapperTrait;
use ConnectionErrorMapperTrait;
const METHOD = Mailer::METHOD_SENDGRID;
public function getErrorFromResponse($response, $subscriber) {
$response = (!empty($response['errors'][0])) ?
$response['errors'][0] :
// translators: %s is the name of the method.
sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_SENDGRID);
$level = MailerError::LEVEL_HARD;
if (strpos($response, 'Invalid email address') === 0) {
$level = MailerError::LEVEL_SOFT;
}
$subscriberErrors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, $level, $response, null, $subscriberErrors);
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,186 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer\Methods;
if (!defined('ABSPATH')) exit;
use MailPoet\Config\ServicesChecker;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\Methods\Common\BlacklistCheck;
use MailPoet\Mailer\Methods\ErrorMappers\MailPoetMapper;
use MailPoet\Services\AuthorizedEmailsController;
use MailPoet\Services\Bridge;
use MailPoet\Services\Bridge\API;
use MailPoet\Util\Url;
class MailPoet implements MailerMethod {
public $api;
public $sender;
public $replyTo;
public $servicesChecker;
/** @var AuthorizedEmailsController */
private $authorizedEmailsController;
/** @var MailPoetMapper */
private $errorMapper;
/** @var BlacklistCheck */
private $blacklist;
/*** @var Url */
private $url;
/** @var Bridge */
private $bridge;
public function __construct(
$apiKey,
$sender,
$replyTo,
MailPoetMapper $errorMapper,
AuthorizedEmailsController $authorizedEmailsController,
Bridge $bridge,
Url $url
) {
$this->api = new API($apiKey);
$this->sender = $sender;
$this->replyTo = $replyTo;
$this->servicesChecker = new ServicesChecker();
$this->errorMapper = $errorMapper;
$this->bridge = $bridge;
$this->authorizedEmailsController = $authorizedEmailsController;
$this->blacklist = new BlacklistCheck();
$this->url = $url;
}
public function send($newsletter, $subscriber, $extraParams = []): array {
if ($this->servicesChecker->isMailPoetAPIKeyValid() === false) {
return Mailer::formatMailerErrorResult($this->errorMapper->getInvalidApiKeyError());
}
$subscribersForBlacklistCheck = is_array($subscriber) ? $subscriber : [$subscriber];
foreach ($subscribersForBlacklistCheck as $sub) {
if ($this->blacklist->isBlacklisted($sub)) {
$error = $this->errorMapper->getBlacklistError($sub);
return Mailer::formatMailerErrorResult($error);
}
}
$messageBody = $this->getBody($newsletter, $subscriber, $extraParams);
$result = $this->api->sendMessages($messageBody);
switch ($result['status']) {
case API::SENDING_STATUS_CONNECTION_ERROR:
$error = $this->errorMapper->getConnectionError($result['message']);
return Mailer::formatMailerErrorResult($error);
case API::SENDING_STATUS_SEND_ERROR:
$error = $this->processSendError($result, $subscriber, $newsletter);
return Mailer::formatMailerErrorResult($error);
case API::RESPONSE_STATUS_OK:
default:
return Mailer::formatMailerSendSuccessResult();
}
}
public function processSendError($result, $subscriber, $newsletter) {
if (empty($result['code'])) {
return $this->errorMapper->getErrorForResult($result, $subscriber, $this->sender, $newsletter);
}
switch ($result['code']) {
case API::RESPONSE_CODE_KEY_INVALID:
$this->bridge->invalidateMssKey();
break;
case API::RESPONSE_CODE_CAN_NOT_SEND:
if ($result['error'] === API::ERROR_MESSAGE_INVALID_FROM) {
$this->authorizedEmailsController->checkAuthorizedEmailAddresses();
}
break;
case API::RESPONSE_CODE_PAYLOAD_ERROR:
if (!empty($result['error']) && $result['error'] === API::ERROR_MESSAGE_BULK_EMAIL_FORBIDDEN) {
$this->authorizedEmailsController->checkAuthorizedEmailAddresses();
}
break;
}
return $this->errorMapper->getErrorForResult($result, $subscriber, $this->sender, $newsletter);
}
public function processSubscriber($subscriber) {
preg_match('!(?P<name>.*?)\s<(?P<email>.*?)>!', $subscriber, $subscriberData);
if (!isset($subscriberData['email'])) {
$subscriberData = [
'email' => $subscriber,
];
}
return [
'email' => $subscriberData['email'],
'name' => (isset($subscriberData['name'])) ? $subscriberData['name'] : '',
];
}
public function getBody($newsletter, $subscriber, $extraParams = []) {
if (is_array($newsletter) && is_array($subscriber)) {
$body = [];
for ($record = 0; $record < count($newsletter); $record++) {
$body[] = $this->composeBody(
$newsletter[$record],
$this->processSubscriber($subscriber[$record]),
(!empty($extraParams['unsubscribe_url'][$record])) ? $extraParams['unsubscribe_url'][$record] : false,
(!empty($extraParams['one_click_unsubscribe'][$record])) ? $extraParams['one_click_unsubscribe'][$record] : false,
(!empty($extraParams['meta'][$record])) ? $extraParams['meta'][$record] : false
);
}
} else {
$body[] = $this->composeBody(
$newsletter,
$this->processSubscriber($subscriber),
(!empty($extraParams['unsubscribe_url'])) ? $extraParams['unsubscribe_url'] : false,
(!empty($extraParams['one_click_unsubscribe'])) ? $extraParams['one_click_unsubscribe'] : false,
(!empty($extraParams['meta'])) ? $extraParams['meta'] : false
);
}
return $body;
}
private function composeBody($newsletter, $subscriber, $unsubscribeUrl, $oneClickUnsubscribeUrl, $meta): array {
$body = [
'to' => ([
'address' => $subscriber['email'],
'name' => $subscriber['name'],
]),
'from' => ([
'address' => $this->sender['from_email'],
'name' => $this->sender['from_name'],
]),
'reply_to' => ([
'address' => $this->replyTo['reply_to_email'],
]),
'subject' => $newsletter['subject'],
];
if (!empty($this->replyTo['reply_to_name'])) {
$body['reply_to']['name'] = $this->replyTo['reply_to_name'];
}
if (!empty($newsletter['body']['html'])) {
$body['html'] = $newsletter['body']['html'];
}
if (!empty($newsletter['body']['text'])) {
$body['text'] = $newsletter['body']['text'];
}
if ($unsubscribeUrl) {
$isHttps = $this->url->isUsingHttps($unsubscribeUrl);
$body['unsubscribe'] = [
'url' => $isHttps && $oneClickUnsubscribeUrl ? $oneClickUnsubscribeUrl : $unsubscribeUrl,
'post' => $isHttps,
];
}
if ($meta) {
$body['meta'] = $meta;
}
return $body;
}
}
@@ -0,0 +1,10 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\Methods;
if (!defined('ABSPATH')) exit;
interface MailerMethod {
public function send(array $newsletter, array $subscriber, array $extraParams = []): array;
}
@@ -0,0 +1,17 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\Methods;
if (!defined('ABSPATH')) exit;
use PHPMailer\PHPMailer\PHPMailer;
class PHPMail extends PHPMailerMethod {
public function buildMailer(): PHPMailer {
$mailer = new PHPMailer(true);
// send using PHP's mail() function
$mailer->isMail();
return $mailer;
}
}
@@ -0,0 +1,126 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\Methods;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\Methods\Common\BlacklistCheck;
use MailPoet\Mailer\WordPress\PHPMailerLoader;
use MailPoet\Util\Url;
use PHPMailer\PHPMailer\PHPMailer;
PHPMailerLoader::load();
abstract class PHPMailerMethod implements MailerMethod {
/** @var string[] */
public $sender;
/** @var string[] */
public $replyTo;
/** @var string */
public $returnPath;
/** @var PHPMailer */
public $mailer;
protected $errorMapper;
/** @var Url */
protected $urlUtils;
/** @var BlacklistCheck */
protected $blacklist;
public function __construct(
$sender,
$replyTo,
$returnPath,
$errorMapper,
Url $urlUtils
) {
$this->sender = $sender;
$this->replyTo = $replyTo;
$this->returnPath = $returnPath;
$this->mailer = $this->buildMailer();
$this->errorMapper = $errorMapper;
$this->urlUtils = $urlUtils;
$this->blacklist = new BlacklistCheck();
}
public function send($newsletter, $subscriber, $extraParams = []): array {
if ($this->blacklist->isBlacklisted($subscriber)) {
$error = $this->errorMapper->getBlacklistError($subscriber);
return Mailer::formatMailerErrorResult($error);
}
try {
$mailer = $this->configureMailerWithMessage($newsletter, $subscriber, $extraParams);
$result = $mailer->send();
} catch (\Exception $e) {
return Mailer::formatMailerErrorResult($this->errorMapper->getErrorFromException($e, $subscriber));
}
if ($result === true) {
return Mailer::formatMailerSendSuccessResult();
} else {
$error = $this->errorMapper->getErrorForSubscriber($subscriber);
return Mailer::formatMailerErrorResult($error);
}
}
abstract public function buildMailer(): PHPMailer;
public function configureMailerWithMessage($newsletter, $subscriber, $extraParams = []) {
$mailer = $this->mailer;
$mailer->clearAddresses();
$mailer->clearCustomHeaders();
$mailer->isHTML(!empty($newsletter['body']['html']));
$mailer->CharSet = 'UTF-8'; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$mailer->setFrom($this->sender['from_email'], $this->sender['from_name'], false);
$mailer->addReplyTo($this->replyTo['reply_to_email'], $this->replyTo['reply_to_name']);
$subscriber = $this->processSubscriber($subscriber);
$mailer->addAddress($subscriber['email'], $subscriber['name']);
$mailer->Subject = (!empty($newsletter['subject'])) ? $newsletter['subject'] : ''; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$mailer->Body = (!empty($newsletter['body']['html'])) ? $newsletter['body']['html'] : $newsletter['body']['text']; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
if ($mailer->ContentType !== 'text/plain') { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$mailer->AltBody = (!empty($newsletter['body']['text'])) ? $newsletter['body']['text'] : ''; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
}
$mailer->Sender = $this->returnPath; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
// unsubscribe header
$unsubscribeUrl = $extraParams['unsubscribe_url'] ?? null;
$oneClickUnsubscribeUrl = $extraParams['one_click_unsubscribe'] ?? null;
if ($unsubscribeUrl) {
$isHttps = $this->urlUtils->isUsingHttps($unsubscribeUrl);
$url = $isHttps && $oneClickUnsubscribeUrl ? $oneClickUnsubscribeUrl : $unsubscribeUrl;
if ($isHttps) {
$mailer->addCustomHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
}
$mailer->addCustomHeader('List-Unsubscribe', '<' . $url . '>');
}
// Enforce base64 encoding when lines are too long, otherwise quoted-printable encoding
// is automatically used which can occasionally break the email body.
// Explanation:
// The bug occurs on Unix systems where mail() function passes email to a variation of
// sendmail command which expects only NL as line endings (POSIX). Since quoted-printable
// requires CRLF some of those commands convert LF to CRLF which can break the email body
// because it already (correctly) uses CRLF. Such CRLF then (wrongly) becomes CRCRLF.
if (PHPMailer::hasLineLongerThanMax($mailer->Body)) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$mailer->Encoding = 'base64'; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
}
return $mailer;
}
public function processSubscriber($subscriber) {
preg_match('!(?P<name>.*?)\s<(?P<email>.*?)>!', $subscriber, $subscriberData);
if (!isset($subscriberData['email'])) {
$subscriberData = [
'email' => $subscriber,
];
}
return [
'email' => $subscriberData['email'],
'name' => (isset($subscriberData['name'])) ? $subscriberData['name'] : '',
];
}
}
@@ -0,0 +1,94 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\Methods;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\Methods\ErrorMappers\SMTPMapper;
use MailPoet\RuntimeException;
use MailPoet\Util\Url;
use MailPoet\WP\Functions as WPFunctions;
use PHPMailer\PHPMailer\PHPMailer;
class SMTP extends PHPMailerMethod {
const SMTP_CONNECTION_TIMEOUT = 15; // seconds
/** @var string */
public $host;
/** @var int */
public $port;
/** @var int */
public $authentication;
/** @var string */
public $login;
/** @var string */
public $password;
/** @var string */
public $encryption;
/** @var PHPMailer */
public $mailer;
/** @var WPFunctions */
protected $wp;
public function __construct(
$host,
$port,
$authentication,
$encryption,
$sender,
$replyTo,
$returnPath,
SMTPMapper $errorMapper,
Url $urlUtils,
$login = null,
$password = null
) {
$this->wp = new WPFunctions;
$this->host = $host;
$this->port = $port;
$this->authentication = $authentication;
$this->login = $login;
$this->password = $password;
$this->encryption = $encryption;
parent::__construct($sender, $replyTo, $returnPath, $errorMapper, $urlUtils);
}
public function buildMailer(): PHPMailer {
$mailer = new PHPMailer(true);
$mailer->isSMTP();
/** @phpstan-ignore-next-line - we cannot annotate the return type from a filter */
$mailer->Host = $this->wp->applyFilters('mailpoet_mailer_smtp_host', $this->host); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
/** @phpstan-ignore-next-line - we cannot annotate the return type from a filter */
$mailer->Port = $this->wp->applyFilters('mailpoet_mailer_smtp_port', $this->port); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
/** @phpstan-ignore-next-line - we cannot annotate the return type from a filter */
$mailer->SMTPSecure = $this->wp->applyFilters('mailpoet_mailer_smtp_encryption', $this->encryption);
/** @phpstan-ignore-next-line - we cannot annotate the return type from a filter */
$mailer->SMTPOptions = $this->wp->applyFilters('mailpoet_mailer_smtp_options', []);
/** @phpstan-ignore-next-line - we cannot annotate the return type from a filter */
$mailer->Timeout = $this->wp->applyFilters('mailpoet_mailer_smtp_connection_timeout', self::SMTP_CONNECTION_TIMEOUT); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
if ($this->authentication === 1) {
$mailer->SMTPAuth = true; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$mailer->Username = $this->login; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$mailer->Password = $this->password; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
}
// values from filters can overwrite username and password
$filterUsername = $this->wp->applyFilters('mailpoet_mailer_smtp_username', null);
$filterPassword = $this->wp->applyFilters('mailpoet_mailer_smtp_password', null);
if ($filterUsername && $filterPassword) {
$mailer->SMTPAuth = true; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
/** @phpstan-ignore-next-line - we cannot annotate the return type from a filter */
$mailer->Username = $filterUsername; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
/** @phpstan-ignore-next-line - we cannot annotate the return type from a filter */
$mailer->Password = $filterPassword; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
}
$mailer = $this->wp->applyFilters('mailpoet_mailer_smtp_instance', $mailer);
if (!$mailer instanceof PHPMailer) {
throw new RuntimeException(__('Filter "mailpoet_mailer_smtp_instance" must return an instance of PHPMailer.', 'mailpoet'));
}
return $mailer;
}
}
@@ -0,0 +1,117 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer\Methods;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\Methods\Common\BlacklistCheck;
use MailPoet\Mailer\Methods\ErrorMappers\SendGridMapper;
use MailPoet\Util\Url;
use MailPoet\WP\Functions as WPFunctions;
class SendGrid implements MailerMethod {
public $url = 'https://api.sendgrid.com/api/mail.send.json';
public $apiKey;
public $sender;
public $replyTo;
/** @var SendGridMapper */
private $errorMapper;
/** @var Url */
private $urlUtils;
/** @var BlacklistCheck */
private $blacklist;
private $wp;
public function __construct(
$apiKey,
$sender,
$replyTo,
SendGridMapper $errorMapper,
Url $urlUtils
) {
$this->apiKey = $apiKey;
$this->sender = $sender;
$this->replyTo = $replyTo;
$this->errorMapper = $errorMapper;
$this->urlUtils = $urlUtils;
$this->wp = new WPFunctions();
$this->blacklist = new BlacklistCheck();
}
public function send($newsletter, $subscriber, $extraParams = []): array {
if ($this->blacklist->isBlacklisted($subscriber)) {
$error = $this->errorMapper->getBlacklistError($subscriber);
return Mailer::formatMailerErrorResult($error);
}
$result = $this->wp->wpRemotePost(
$this->url,
$this->request($newsletter, $subscriber, $extraParams)
);
if (is_wp_error($result)) {
$error = $this->errorMapper->getConnectionError($result->get_error_message());
return Mailer::formatMailerErrorResult($error);
}
if ($this->wp->wpRemoteRetrieveResponseCode($result) !== 200) {
$response = json_decode($result['body'], true);
$error = $this->errorMapper->getErrorFromResponse($response, $subscriber);
return Mailer::formatMailerErrorResult($error);
}
return Mailer::formatMailerSendSuccessResult();
}
public function getBody($newsletter, $subscriber, $extraParams = []) {
$body = [
'to' => $subscriber,
'from' => $this->sender['from_email'],
'fromname' => $this->sender['from_name'],
'replyto' => $this->replyTo['reply_to_email'],
'subject' => $newsletter['subject'],
];
$headers = [];
// unsubscribe header
$unsubscribeUrl = $extraParams['unsubscribe_url'] ?? null;
$oneClickUnsubscribeUrl = $extraParams['one_click_unsubscribe'] ?? null;
if ($unsubscribeUrl) {
$isHttps = $this->urlUtils->isUsingHttps($unsubscribeUrl);
$url = $isHttps && $oneClickUnsubscribeUrl ? $oneClickUnsubscribeUrl : $unsubscribeUrl;
if ($isHttps) {
$headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
}
$headers['List-Unsubscribe'] = '<' . $url . '>';
}
if ($headers) {
$body['headers'] = json_encode($headers);
}
if (!empty($newsletter['body']['html'])) {
$body['html'] = $newsletter['body']['html'];
}
if (!empty($newsletter['body']['text'])) {
$body['text'] = $newsletter['body']['text'];
}
return $body;
}
public function auth() {
return 'Bearer ' . $this->apiKey;
}
public function request($newsletter, $subscriber, $extraParams = []) {
$body = $this->getBody($newsletter, $subscriber, $extraParams);
return [
'timeout' => 10,
'httpversion' => '1.1',
'method' => 'POST',
'headers' => [
'Authorization' => $this->auth(),
],
'body' => http_build_query($body, '', '&'),
];
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1,45 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer;
if (!defined('ABSPATH')) exit;
class SubscriberError {
/** @var string */
private $email;
/** @var string|null */
private $message;
/**
* @param string $email
* @param string $message|null
*/
public function __construct(
$email,
$message = null
) {
$this->email = $email;
$this->message = $message;
}
/**
* @return string
*/
public function getEmail() {
return $this->email;
}
/**
* @return null|string
*/
public function getMessage() {
return $this->message;
}
public function __toString() {
return $this->message ? $this->email . ': ' . $this->message : $this->email;
}
}
@@ -0,0 +1,23 @@
<?php declare(strict_types = 1);
namespace MailPoet\Mailer\WordPress;
if (!defined('ABSPATH')) exit;
class PHPMailerLoader {
/**
* Load PHPMailer because is not autoloaded
*/
public static function load(): void {
if (!class_exists('PHPMailer\PHPMailer\PHPMailer')) {
require_once ABSPATH . WPINC . '/PHPMailer/PHPMailer.php';
}
if (!class_exists('PHPMailer\PHPMailer\Exception')) {
require_once ABSPATH . WPINC . '/PHPMailer/Exception.php';
}
if (!class_exists('PHPMailer\PHPMailer\SMTP')) {
require_once ABSPATH . WPINC . '/PHPMailer/SMTP.php';
}
}
}
@@ -0,0 +1,129 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer\WordPress;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Mailer\MetaInfo;
use MailPoet\Subscribers\SubscribersRepository;
use MailPoetVendor\Html2Text\Html2Text;
use PHPMailer\PHPMailer\Exception as PHPMailerException;
use PHPMailer\PHPMailer\PHPMailer;
PHPMailerLoader::load();
class WordPressMailer extends PHPMailer {
/** @var MailerFactory */
private $mailerFactory;
/** @var MetaInfo */
private $mailerMetaInfo;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
MailerFactory $mailerFactory,
MetaInfo $mailerMetaInfo,
SubscribersRepository $subscribersRepository
) {
parent::__construct(true);
$this->mailerFactory = $mailerFactory;
$this->mailerMetaInfo = $mailerMetaInfo;
$this->subscribersRepository = $subscribersRepository;
}
public function send() {
// We need this so that the PHPMailer class will correctly prepare all the headers.
$originalMailer = $this->Mailer; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$this->Mailer = 'mail'; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
// Prepare everything (including the message) for sending.
$this->preSend();
$email = $this->getEmail();
$address = $this->formatAddress($this->getToAddresses());
$subscriber = $this->subscribersRepository->findOneBy(['email' => $address]);
$extraParams = [
'meta' => $this->mailerMetaInfo->getWordPressTransactionalMetaInfo($subscriber),
];
try {
// we need to build fresh mailer for every single WP e-mail to make sure reply-to is set
$replyTo = $this->getReplyToAddress();
$mailer = $this->mailerFactory->buildMailer(null, null, $replyTo);
$result = $mailer->send($email, $address, $extraParams);
if (!$result['response']) {
throw new \Exception($result['error']->getMessage());
}
} catch (\Exception $ePrimary) {
// In case the sending using MailPoet's mailer fails continue with sending using original parent PHPMailer::sent method.
// But if anything fails we still want tho throw the error from the primary MailPoet mailer.
try {
// Restore original settings for mailer. Some sites may use SMTP and we needed to reset it to mail
$this->Mailer = $originalMailer; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
return parent::send();
} catch (\Exception $eFallback) {
throw new PHPMailerException($ePrimary->getMessage(), $ePrimary->getCode(), $ePrimary);
}
}
return true;
}
private function getEmail() {
// phpcs:disable Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
$email = [
'subject' => $this->Subject,
'body' => [],
];
if (strpos($this->ContentType, 'text/plain') === 0) {
$email['body']['text'] = $this->Body;
} elseif (strpos($this->ContentType, 'text/html') === 0) {
$text = @Html2Text::convert(strtolower($this->CharSet) === 'utf-8' ? $this->Body : mb_convert_encoding($this->Body, 'UTF-8', mb_list_encodings()));
$email['body']['text'] = $text;
$email['body']['html'] = $this->Body;
} elseif (strpos($this->ContentType, 'multipart/alternative') === 0) {
$email['body']['text'] = $this->AltBody;
$email['body']['html'] = $this->Body;
} else {
throw new PHPMailerException('Unsupported email content type has been used. Please use only text or HTML emails.');
}
return $email;
// phpcs:enable
}
private function formatAddress($wordpressAddress) {
$data = $wordpressAddress[0];
$result = [
'address' => $data[0],
];
if (!empty($data[1])) {
$result['full_name'] = $data[1];
}
return $result;
}
private function getReplyToAddress(): ?array {
$replyToAddress = null;
$addresses = $this->getReplyToAddresses();
if (!empty($addresses)) {
// only one reply-to address supported by \MailPoet\Mailer
$address = array_shift($addresses);
$replyToAddress = [];
if ($address[1]) {
$replyToAddress['name'] = $address[1];
}
if ($address[0]) {
$replyToAddress['address'] = $address[0];
}
}
return $replyToAddress;
}
}
@@ -0,0 +1,57 @@
<?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing
namespace MailPoet\Mailer\WordPress;
if (!defined('ABSPATH')) exit;
use MailPoet\Mailer\MailerFactory;
use MailPoet\Mailer\MetaInfo;
use MailPoet\Settings\SettingsController;
use MailPoet\Subscribers\SubscribersRepository;
class WordpressMailerReplacer {
/** @var MailerFactory */
private $mailerFactory;
/** @var MetaInfo */
private $mailerMetaInfo;
/** @var SettingsController */
private $settings;
/** @var SubscribersRepository */
private $subscribersRepository;
public function __construct(
MailerFactory $mailerFactory,
MetaInfo $mailerMetaInfo,
SettingsController $settings,
SubscribersRepository $subscribersRepository
) {
$this->mailerFactory = $mailerFactory;
$this->mailerMetaInfo = $mailerMetaInfo;
$this->settings = $settings;
$this->subscribersRepository = $subscribersRepository;
}
public function replaceWordPressMailer() {
global $phpmailer;
// This code needs to be wrapped because it has to run in an early stage of plugin initialisation
// and in some cases on multisite instance it may run before DB migrator and settings table is not ready at that time
try {
$sendTransactional = $this->settings->get('send_transactional_emails', false);
} catch (\Exception $e) {
$sendTransactional = false;
}
if ($sendTransactional) {
$phpmailer = new WordPressMailer(
$this->mailerFactory,
$this->mailerMetaInfo,
$this->subscribersRepository
);
}
return $phpmailer;
}
}
@@ -0,0 +1 @@
<?php
@@ -0,0 +1 @@
<?php