init
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
+18
@@ -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);
|
||||
}
|
||||
}
|
||||
+18
@@ -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
|
||||
Reference in New Issue
Block a user