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