Merge pull request #1505 from mailpoet/sending-erros-levels

Multiple Sending Error Levels [MAILPOET-1154]
This commit is contained in:
Michelle Shull
2018-09-24 07:09:39 -04:00
committed by GitHub
38 changed files with 1045 additions and 256 deletions

View File

@ -21,10 +21,7 @@ class Mailer extends APIEndpoint {
(isset($data['sender'])) ? $data['sender'] : false,
(isset($data['reply_to'])) ? $data['reply_to'] : false
);
$extra_params = array(
'test_email' => true
);
$result = $mailer->send($data['newsletter'], $data['subscriber'], $extra_params);
$result = $mailer->send($data['newsletter'], $data['subscriber']);
} catch(\Exception $e) {
return $this->errorResponse(array(
$e->getCode() => $e->getMessage()
@ -34,7 +31,7 @@ class Mailer extends APIEndpoint {
if($result['response'] === false) {
$error = sprintf(
__('The email could not be sent: %s', 'mailpoet'),
$result['error_message']
$result['error']->getMessage()
);
return $this->errorResponse(array(APIError::BAD_REQUEST => $error));
} else {

View File

@ -131,6 +131,8 @@ class Migrator {
'task_id int(11) unsigned NOT NULL,',
'subscriber_id int(11) unsigned NOT NULL,',
'processed int(1) NOT NULL,',
'failed int(1) NOT NULL,',
'error text NULL,',
'created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,',
'PRIMARY KEY (task_id, subscriber_id),',
'KEY subscriber_id (subscriber_id)'

View File

@ -2,6 +2,7 @@
namespace MailPoet\Cron;
use MailPoet\Cron\Workers\Scheduler as SchedulerWorker;
use MailPoet\Cron\Workers\SendingQueue\Migration as MigrationWorker;
use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\Bounce as BounceWorker;
use MailPoet\Cron\Workers\KeyCheck\PremiumKeyCheck as PremiumKeyCheckWorker;
@ -91,7 +92,7 @@ class Daemon {
}
function executeQueueWorker() {
$queue = new SendingQueueWorker($this->timer);
$queue = new SendingQueueWorker(new SendingErrorHandler(), $this->timer);
return $queue->process();
}

View File

@ -0,0 +1,36 @@
<?php
namespace MailPoet\Cron\Workers\SendingQueue;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\MailerLog;
use MailPoet\Tasks\Sending as SendingTask;
class SendingErrorHandler {
function processError(
MailerError $error,
SendingTask $sending_task,
array $prepared_subscribers_ids,
array $prepared_subscribers
) {
if($error->getLevel() === MailerError::LEVEL_HARD) {
return $this->processHardError($error);
}
$this->processSoftError($error, $sending_task, $prepared_subscribers_ids, $prepared_subscribers);
}
private function processHardError(MailerError $error) {
if($error->getRetryInterval() !== null) {
MailerLog::processNonBlockingError($error->getOperation(), $error->getMessageWithFailedSubscribers(), $error->getRetryInterval());
} else {
MailerLog::processError($error->getOperation(), $error->getMessageWithFailedSubscribers());
}
}
private function processSoftError(MailerError $error, SendingTask $sending_task, $prepared_subscribers_ids, $prepared_subscribers) {
foreach($error->getSubscriberErrors() as $subscriber_error) {
$subscriber_id_index = array_search($subscriber_error->getEmail(), $prepared_subscribers);
$message = $subscriber_error->getMessage() ?: $error->getMessage();
$sending_task->saveSubscriberError($prepared_subscribers_ids[$subscriber_id_index], $message);
}
}
}

View File

@ -5,6 +5,7 @@ use MailPoet\Cron\CronHelper;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Links;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\MailerLog;
use MailPoet\Models\ScheduledTask as ScheduledTaskModel;
use MailPoet\Models\StatisticsNewsletters as StatisticsNewslettersModel;
@ -23,7 +24,11 @@ class SendingQueue {
const BATCH_SIZE = 20;
const TASK_BATCH_SIZE = 5;
function __construct($timer = false, $mailer_task = false, $newsletter_task = false) {
/** @var SendingErrorHandler */
private $error_handler;
function __construct(SendingErrorHandler $error_handler, $timer = false, $mailer_task = false, $newsletter_task = false) {
$this->error_handler = $error_handler;
$this->mailer_task = ($mailer_task) ? $mailer_task : new MailerTask();
$this->newsletter_task = ($newsletter_task) ? $newsletter_task : new NewsletterTask();
$this->timer = ($timer) ? $timer : microtime(true);
@ -124,12 +129,12 @@ class SendingQueue {
'queue_id' => $queue->id
);
if($processing_method === 'individual') {
$queue = $this->sendNewsletters(
$queue = $this->sendNewsletter(
$queue,
$prepared_subscribers_ids,
$prepared_subscribers_ids[0],
$prepared_newsletters[0],
$prepared_subscribers[0],
$statistics,
$statistics[0],
array('unsubscribe_url' => $unsubscribe_urls[0])
);
$prepared_newsletters = array();
@ -152,29 +157,64 @@ class SendingQueue {
return $queue;
}
function sendNewsletters(
$queue, $prepared_subscribers_ids, $prepared_newsletters,
$prepared_subscribers, $statistics, $extra_params = array()
function sendNewsletter(
SendingTask $sending_task, $prepared_subscriber_id, $prepared_newsletter,
$prepared_subscriber, $statistics, $extra_params = array()
) {
// send newsletter
$send_result = $this->mailer_task->send(
$prepared_newsletter,
$prepared_subscriber,
$extra_params
);
return $this->processSendResult(
$sending_task,
$send_result,
[$prepared_subscriber],
[$prepared_subscriber_id],
[$statistics]
);
}
function sendNewsletters(
SendingTask $sending_task, $prepared_subscribers_ids, $prepared_newsletters,
$prepared_subscribers, $statistics, $extra_params = array()
) {
// send newsletters
$send_result = $this->mailer_task->sendBulk(
$prepared_newsletters,
$prepared_subscribers,
$extra_params
);
return $this->processSendResult(
$sending_task,
$send_result,
$prepared_subscribers,
$prepared_subscribers_ids,
$statistics
);
}
private function processSendResult(
SendingTask $sending_task,
$send_result,
array $prepared_subscribers,
array $prepared_subscribers_ids,
array $statistics
) {
// log error message and schedule retry/pause sending
if($send_result['response'] === false) {
if(isset($send_result['retry_interval'])) {
MailerLog::processNonBlockingError($send_result['operation'], $send_result['error_message'], $send_result['retry_interval']);
} else {
MailerLog::processError($send_result['operation'], $send_result['error_message']);
}
$error = $send_result['error'];
assert($error instanceof MailerError);
// Always switch error level to hard until we implement UI for individual subscriber errors
$error->switchLevelToHard();
$this->error_handler->processError($error, $sending_task, $prepared_subscribers_ids, $prepared_subscribers);
}
// update processed/to process list
if(!$queue->updateProcessedSubscribers($prepared_subscribers_ids)) {
if(!$sending_task->updateProcessedSubscribers($prepared_subscribers_ids)) {
MailerLog::processError(
'processed_list_update',
sprintf('QUEUE-%d-PROCESSED-LIST-UPDATE', $queue->id),
sprintf('QUEUE-%d-PROCESSED-LIST-UPDATE', $sending_task->id),
null,
true
);
@ -184,10 +224,10 @@ class SendingQueue {
// update the sent count
$this->mailer_task->updateSentCount();
// enforce execution limits if queue is still being processed
if($queue->status !== ScheduledTaskModel::STATUS_COMPLETED) {
if($sending_task->status !== ScheduledTaskModel::STATUS_COMPLETED) {
$this->enforceSendingAndExecutionLimits();
}
return $queue;
return $sending_task;
}
function enforceSendingAndExecutionLimits() {

View File

@ -54,11 +54,25 @@ class Mailer {
return $this->mailer->formatSubscriberNameAndEmailAddress($subscriber);
}
function send($prepared_newsletters, $prepared_subscribers, $extra_params = array()) {
function sendBulk($prepared_newsletters, $prepared_subscribers, $extra_params = array()) {
if($this->getProcessingMethod() === 'individual') {
throw new \LogicException('Trying to send a batch with individual processing method');
}
return $this->mailer->mailer_instance->send(
$prepared_newsletters,
$prepared_subscribers,
$extra_params
);
}
function send($prepared_newsletter, $prepared_subscriber, $extra_params = array()) {
if($this->getProcessingMethod() === 'bulk') {
throw new \LogicException('Trying to send an individual email with a bulk processing method');
}
return $this->mailer->mailer_instance->send(
$prepared_newsletter,
$prepared_subscriber,
$extra_params
);
}
}

View File

@ -53,7 +53,7 @@ class Newsletter {
return $newsletter;
}
function preProcessNewsletter($newsletter, $queue) {
function preProcessNewsletter(\MailPoet\Models\Newsletter $newsletter, $queue) {
// return the newsletter if it was previously rendered
if(!is_null($queue->getNewsletterRenderedBody())) {
return (!$queue->validate()) ?

View File

@ -1,6 +1,11 @@
<?php
namespace MailPoet\Mailer;
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\Models\Setting;
if(!defined('ABSPATH')) exit;
@ -42,28 +47,32 @@ class Mailer {
$this->mailer_config['secret_key'],
$this->sender,
$this->reply_to,
$this->return_path
$this->return_path,
new AmazonSESMapper()
);
break;
case self::METHOD_MAILPOET:
$mailer_instance = new $this->mailer_config['class'](
$this->mailer_config['mailpoet_api_key'],
$this->sender,
$this->reply_to
$this->reply_to,
new MailPoetMapper()
);
break;
case self::METHOD_SENDGRID:
$mailer_instance = new $this->mailer_config['class'](
$this->mailer_config['api_key'],
$this->sender,
$this->reply_to
$this->reply_to,
new SendGridMapper()
);
break;
case self::METHOD_PHPMAIL:
$mailer_instance = new $this->mailer_config['class'](
$this->sender,
$this->reply_to,
$this->return_path
$this->return_path,
new PHPMailMapper()
);
break;
case self::METHOD_SMTP:
@ -76,7 +85,8 @@ class Mailer {
$this->mailer_config['encryption'],
$this->sender,
$this->reply_to,
$this->return_path
$this->return_path,
new SMTPMapper()
);
break;
default:
@ -166,25 +176,16 @@ class Mailer {
return sprintf('=?utf-8?B?%s?=', base64_encode($name));
}
static function formatMailerConnectionErrorResult($error_message) {
return array(
static function formatMailerErrorResult(MailerError $error) {
return [
'response' => false,
'operation' => 'connect',
'error_message' => $error_message
);
}
static function formatMailerSendErrorResult($error_message) {
return array(
'response' => false,
'operation' => 'send',
'error_message' => $error_message
);
'error' => $error,
];
}
static function formatMailerSendSuccessResult() {
return array(
return [
'response' => true
);
];
}
}

105
lib/Mailer/MailerError.php Normal file
View File

@ -0,0 +1,105 @@
<?php
namespace MailPoet\Mailer;
class MailerError {
const OPERATION_CONNECT = 'connect';
const OPERATION_SEND = 'send';
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 $retry_interval;
/** @var array */
private $subscribers_errors = [];
/**
* @param string $operation
* @param string $level
* @param null|string $message
* @param int|null $retry_interval
* @param array $subscribers_errors
*/
function __construct($operation, $level, $message = null, $retry_interval = null, array $subscribers_errors = []) {
$this->operation = $operation;
$this->level = $level;
$this->message = $message;
$this->retry_interval = $retry_interval;
$this->subscribers_errors = $subscribers_errors;
}
/**
* @return string
*/
function getOperation() {
return $this->operation;
}
/**
* @return string
*/
function getLevel() {
return $this->level;
}
/**
* @return null|string
*/
function getMessage() {
return $this->message;
}
/**
* @return int|null
*/
function getRetryInterval() {
return $this->retry_interval;
}
/**
* @return SubscriberError[]
*/
function getSubscriberErrors() {
return $this->subscribers_errors;
}
/**
* Temporary method until we implement UI for subscriber errors
*/
function switchLevelToHard() {
$this->level = self::LEVEL_HARD;
}
function getMessageWithFailedSubscribers() {
$message = $this->message ?: '';
if(!$this->subscribers_errors) {
return $message;
}
$message .= $this->message ? ' ' : '';
if(count($this->subscribers_errors) === 1) {
$message .= __('Unprocessed subscriber:', 'mailpoet') . ' ';
} else {
$message .= __('Unprocessed subscribers:', 'mailpoet') . ' ';
}
$message .= implode(
', ',
array_map(function (SubscriberError $subscriber_error) {
return "($subscriber_error)";
}, $this->subscribers_errors)
);
return $message;
}
}

View File

@ -2,6 +2,7 @@
namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\Methods\ErrorMappers\AmazonSESMapper;
use MailPoet\WP\Functions as WPFunctions;
if(!defined('ABSPATH')) exit;
@ -28,7 +29,18 @@ class AmazonSES {
'EU (Ireland)' => 'eu-west-1'
);
function __construct($region, $access_key, $secret_key, $sender, $reply_to, $return_path) {
/** @var AmazonSESMapper */
private $error_mapper;
function __construct(
$region,
$access_key,
$secret_key,
$sender,
$reply_to,
$return_path,
AmazonSESMapper $error_mapper
) {
$this->aws_access_key = $access_key;
$this->aws_secret_key = $secret_key;
$this->aws_region = (in_array($region, $this->available_regions)) ? $region : false;
@ -48,6 +60,7 @@ class AmazonSES {
$this->sender['from_email'];
$this->date = gmdate('Ymd\THis\Z');
$this->date_without_time = gmdate('Ymd');
$this->error_mapper = $error_mapper;
}
function send($newsletter, $subscriber, $extra_params = array()) {
@ -57,20 +70,17 @@ class AmazonSES {
$this->request($newsletter, $subscriber, $extra_params)
);
} catch(\Exception $e) {
return Mailer::formatMailerSendErrorResult($e->getMessage());
$error = $this->error_mapper->getErrorFromException($e, $subscriber);
return Mailer::formatMailerErrorResult($error);
}
if(is_wp_error($result)) {
return Mailer::formatMailerConnectionErrorResult($result->get_error_message());
$error = $this->error_mapper->getConnectionError($result->get_error_message());
return Mailer::formatMailerErrorResult($error);
}
if(WPFunctions::wpRemoteRetrieveResponseCode($result) !== 200) {
$response = simplexml_load_string(WPFunctions::wpRemoteRetrieveBody($result));
$response = ($response) ?
$response->Error->Message->__toString() :
sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_AMAZONSES);
if(empty($extra_params['test_email'])) {
$response .= sprintf(' %s: %s', __('Unprocessed subscriber', 'mailpoet'), $subscriber);
}
return Mailer::formatMailerSendErrorResult($response);
$error = $this->error_mapper->getErrorFromResponse($response, $subscriber);
return Mailer::formatMailerErrorResult($error);
}
return Mailer::formatMailerSendSuccessResult();
}

View File

@ -0,0 +1,36 @@
<?php
namespace MailPoet\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\SubscriberError;
class AmazonSESMapper {
use ConnectionErrorMapperTrait;
function getErrorFromException(\Exception $e, $subscriber) {
$level = MailerError::LEVEL_HARD;
if($e instanceof \Swift_RfcComplianceException) {
$level = MailerError::LEVEL_SOFT;
}
$subscriber_errors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, $level, $e->getMessage(), null, $subscriber_errors);
}
/**
* @see https://docs.aws.amazon.com/ses/latest/DeveloperGuide/api-error-codes.html
* @return MailerError
*/
function getErrorFromResponse($response, $subscriber) {
$message = ($response) ?
$response->Error->Message->__toString() :
sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_AMAZONSES);
$level = MailerError::LEVEL_HARD;
if($response && $response->Error->Code->__toString() === 'MessageRejected') {
$level = MailerError::LEVEL_SOFT;
}
$subscriber_errors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, $level, $message, null, $subscriber_errors);
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace MailPoet\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
trait ConnectionErrorMapperTrait {
function getConnectionError($message) {
return new MailerError(
MailerError::OPERATION_CONNECT,
MailerError::LEVEL_HARD,
$message
);
}
}

View File

@ -0,0 +1,77 @@
<?php
namespace MailPoet\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\SubscriberError;
use MailPoet\Services\Bridge\API;
use InvalidArgumentException;
if(!defined('ABSPATH')) exit;
class MailPoetMapper {
use ConnectionErrorMapperTrait;
const TEMPORARY_UNAVAILABLE_RETRY_INTERVAL = 300; // seconds
function getInvalidApiKeyError() {
return new MailerError(
MailerError::OPERATION_SEND,
MailerError::LEVEL_HARD,
__('MailPoet API key is invalid!', 'mailpoet')
);
}
function getErrorForResult(array $result, $subscribers) {
$level = MailerError::LEVEL_HARD;
$retry_interval = null;
$subscribers_errors = [];
$result_code = !empty($result['code']) ? $result['code'] : null;
switch($result_code) {
case API::RESPONSE_CODE_NOT_ARRAY:
$message = __('JSON input is not an array', 'mailpoet');
break;
case API::RESPONSE_CODE_PAYLOAD_ERROR:
$result_parsed = json_decode($result['message'], true);
$message = __('Error while sending.', 'mailpoet');
if(!is_array($result_parsed)) {
$message .= ' ' . $result['message'];
break;
}
try {
$subscribers_errors = $this->getSubscribersErrors($result_parsed, $subscribers);
$level = MailerError::LEVEL_SOFT;
} catch (InvalidArgumentException $e) {
$message .= ' ' . $e->getMessage();
}
break;
case API::RESPONSE_CODE_TEMPORARY_UNAVAILABLE:
$message = __('Email service is temporarily not available, please try again in a few minutes.', 'mailpoet');
$retry_interval = self::TEMPORARY_UNAVAILABLE_RETRY_INTERVAL;
break;
case API::RESPONSE_CODE_KEY_INVALID:
case API::RESPONSE_CODE_PAYLOAD_TOO_BIG:
default:
$message = $result['message'];
}
return new MailerError(MailerError::OPERATION_SEND, $level, $message, $retry_interval, $subscribers_errors);
}
private function getSubscribersErrors($result_parsed, $subscribers) {
$errors = [];
foreach($result_parsed as $result_error) {
if(!is_array($result_error) || !isset($result_error['index']) || !isset($subscribers[$result_error['index']])) {
throw new InvalidArgumentException( __('Invalid MSS response format.', 'mailpoet'));
}
$subscriber_errors = [];
if(isset($result_error['errors']) && is_array($result_error['errors'])) {
array_walk_recursive($result_error['errors'], function($item) use (&$subscriber_errors) {
$subscriber_errors[] = $item;
});
}
$message = join(', ', $subscriber_errors);
$errors[] = new SubscriberError($subscribers[$result_error['index']], $message);
}
return $errors;
}
}

View File

@ -0,0 +1,26 @@
<?php
namespace MailPoet\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\SubscriberError;
class PHPMailMapper {
use ConnectionErrorMapperTrait;
function getErrorFromException(\Exception $e, $subscriber) {
$level = MailerError::LEVEL_HARD;
if(strpos($e->getMessage(), 'Invalid address') === 0) {
$level = MailerError::LEVEL_SOFT;
}
$subscriber_errors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, $level, $e->getMessage(), null, $subscriber_errors);
}
function getErrorForSubscriber($subscriber) {
$message = sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_PHPMAIL);
$subscriber_errors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, MailerError::LEVEL_HARD, $message, null, $subscriber_errors);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace MailPoet\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\SubscriberError;
class SMTPMapper {
use ConnectionErrorMapperTrait;
/**
* @see https://swiftmailer.symfony.com/docs/sending.html
* @return MailerError
*/
function getErrorFromException(\Exception $e, $subscriber) {
// remove redundant information appended by Swift logger to exception messages
$message = explode(PHP_EOL, $e->getMessage());
$level = MailerError::LEVEL_HARD;
if($e instanceof \Swift_RfcComplianceException) {
$level = MailerError::LEVEL_SOFT;
}
$subscriber_errors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, $level, $message[0], null, $subscriber_errors);
}
function getErrorFromLog($log, $subscriber) {
// extract error message from log
preg_match('/!! (.*?)>>/ism', $log, $message);
if(!empty($message[1])) {
$message = $message[1];
// remove line breaks from the message due to how logger's dump() method works
$message = preg_replace('/\r|\n/', '', $message);
} else {
$message = sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_SMTP);
}
$subscriber_errors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, MailerError::LEVEL_HARD, $message, null, $subscriber_errors);
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace MailPoet\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\SubscriberError;
class SendGridMapper {
use ConnectionErrorMapperTrait;
function getErrorFromResponse($response, $subscriber) {
$response = (!empty($response['errors'][0])) ?
$response['errors'][0] :
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;
}
$subscriber_errors = [new SubscriberError($subscriber, null)];
return new MailerError(MailerError::OPERATION_SEND, $level, $response, null, $subscriber_errors);
}
}

View File

@ -3,31 +3,32 @@ namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
use MailPoet\Config\ServicesChecker;
use MailPoet\Mailer\Methods\ErrorMappers\MailPoetMapper;
use MailPoet\Services\Bridge;
use MailPoet\Services\Bridge\API;
if(!defined('ABSPATH')) exit;
class MailPoet {
const TEMPORARY_UNAVAILABLE_RETRY_INTERVAL = 300; // seconds
public $api;
public $sender;
public $reply_to;
public $services_checker;
function __construct($api_key, $sender, $reply_to) {
/** @var MailPoetMapper */
private $error_mapper;
function __construct($api_key, $sender, $reply_to, MailPoetMapper $error_mapper) {
$this->api = new API($api_key);
$this->sender = $sender;
$this->reply_to = $reply_to;
$this->services_checker = new ServicesChecker(false);
$this->services_checker = new ServicesChecker();
$this->error_mapper = $error_mapper;
}
function send($newsletter, $subscriber, $extra_params = array()) {
if($this->services_checker->isMailPoetAPIKeyValid() === false) {
$response = __('MailPoet API key is invalid!', 'mailpoet');
return Mailer::formatMailerSendErrorResult($response);
return Mailer::formatMailerErrorResult($this->error_mapper->getInvalidApiKeyError());
}
$message_body = $this->getBody($newsletter, $subscriber, $extra_params);
@ -35,9 +36,11 @@ class MailPoet {
switch($result['status']) {
case API::SENDING_STATUS_CONNECTION_ERROR:
return Mailer::formatMailerConnectionErrorResult($result['message']);
$error = $this->error_mapper->getConnectionError($result['message']);
return Mailer::formatMailerErrorResult($error);
case API::SENDING_STATUS_SEND_ERROR:
return $this->processSendError($result, $subscriber);
$error = $this->processSendError($result, $subscriber);
return Mailer::formatMailerErrorResult($error);
case API::SENDING_STATUS_OK:
default:
return Mailer::formatMailerSendSuccessResult();
@ -45,27 +48,10 @@ class MailPoet {
}
function processSendError($result, $subscriber) {
if(!empty($result['code'])) {
switch($result['code']) {
case API::RESPONSE_CODE_NOT_ARRAY:
return Mailer::formatMailerSendErrorResult(__('JSON input is not an array', 'mailpoet'));
case API::RESPONSE_CODE_PAYLOAD_TOO_BIG:
return Mailer::formatMailerSendErrorResult($result['message']);
case API::RESPONSE_CODE_PAYLOAD_ERROR:
$error = $this->parseErrorResponse($result['message'], $subscriber);
return Mailer::formatMailerSendErrorResult($error);
case API::RESPONSE_CODE_TEMPORARY_UNAVAILABLE:
$error = Mailer::formatMailerSendErrorResult(__('Email service is temporarily not available, please try again in a few minutes.', 'mailpoet'));
$error['retry_interval'] = self::TEMPORARY_UNAVAILABLE_RETRY_INTERVAL;
return $error;
case API::RESPONSE_CODE_KEY_INVALID:
Bridge::invalidateKey();
break;
default:
return Mailer::formatMailerSendErrorResult($result['message']);
}
if(!empty($result['code']) && $result['code'] === API::RESPONSE_CODE_KEY_INVALID) {
Bridge::invalidateKey();
}
return Mailer::formatMailerSendErrorResult($result['message']);
return $this->error_mapper->getErrorForResult($result, $subscriber);
}
function processSubscriber($subscriber) {
@ -128,37 +114,4 @@ class MailPoet {
}
return $body;
}
private function parseErrorResponse($result, $subscriber) {
$result_parsed = json_decode($result, true);
$errors = [];
if(is_array($result_parsed)) {
foreach($result_parsed as $result_error) {
$errors[] = $this->processSingleSubscriberError($result_error, $subscriber);
}
}
if(!empty($errors)) {
return __('Error while sending: ', 'mailpoet') . join(', ', $errors);
} else {
return __('Error while sending newsletters. ', 'mailpoet') . $result;
}
}
private function processSingleSubscriberError($result_error, $subscriber) {
$error = '';
if(is_array($result_error)) {
$subscriber_errors = [];
if(isset($result_error['errors']) && is_array($result_error['errors'])) {
array_walk_recursive($result_error['errors'], function($item) use (&$subscriber_errors) {
$subscriber_errors[] = $item;
});
}
$error .= join(', ', $subscriber_errors);
if(isset($result_error['index']) && isset($subscriber[$result_error['index']])) {
$error = '(' . $subscriber[$result_error['index']] . ': ' . $error . ')';
}
}
return $error;
}
}

View File

@ -3,6 +3,7 @@
namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\Methods\ErrorMappers\PHPMailMapper;
if(!defined('ABSPATH')) exit;
@ -14,13 +15,17 @@ class PHPMail {
public $return_path;
public $mailer;
function __construct($sender, $reply_to, $return_path) {
/** @var PHPMailMapper */
private $error_mapper;
function __construct($sender, $reply_to, $return_path, PHPMailMapper $error_mapper) {
$this->sender = $sender;
$this->reply_to = $reply_to;
$this->return_path = ($return_path) ?
$return_path :
$this->sender['from_email'];
$this->mailer = $this->buildMailer();
$this->error_mapper = $error_mapper;
}
function send($newsletter, $subscriber, $extra_params = array()) {
@ -28,16 +33,13 @@ class PHPMail {
$mailer = $this->configureMailerWithMessage($newsletter, $subscriber, $extra_params);
$result = $mailer->send();
} catch(\Exception $e) {
return Mailer::formatMailerSendErrorResult($e->getMessage());
return Mailer::formatMailerErrorResult($this->error_mapper->getErrorFromException($e, $subscriber));
}
if($result === true) {
return Mailer::formatMailerSendSuccessResult();
} else {
$result = sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_PHPMAIL);
if(empty($extra_params['test_email'])) {
$result .= sprintf(' %s: %s', __('Unprocessed subscriber', 'mailpoet'), $subscriber);
}
return Mailer::formatMailerSendErrorResult($result);
$error = $this->error_mapper->getErrorForSubscriber($subscriber);
return Mailer::formatMailerErrorResult($error);
}
}

View File

@ -2,6 +2,7 @@
namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\Methods\ErrorMappers\SMTPMapper;
use MailPoet\WP\Hooks;
if(!defined('ABSPATH')) exit;
@ -19,9 +20,12 @@ class SMTP {
public $mailer;
const SMTP_CONNECTION_TIMEOUT = 15; // seconds
/** @var SMTPMapper */
private $error_mapper;
function __construct(
$host, $port, $authentication, $login = null, $password = null, $encryption,
$sender, $reply_to, $return_path) {
$sender, $reply_to, $return_path, SMTPMapper $error_mapper) {
$this->host = $host;
$this->port = $port;
$this->authentication = $authentication;
@ -36,6 +40,7 @@ class SMTP {
$this->mailer = $this->buildMailer();
$this->mailer_logger = new \Swift_Plugins_Loggers_ArrayLogger();
$this->mailer->registerPlugin(new \Swift_Plugins_LoggerPlugin($this->mailer_logger));
$this->error_mapper = $error_mapper;
}
function send($newsletter, $subscriber, $extra_params = array()) {
@ -43,13 +48,16 @@ class SMTP {
$message = $this->createMessage($newsletter, $subscriber, $extra_params);
$result = $this->mailer->send($message);
} catch(\Exception $e) {
return Mailer::formatMailerSendErrorResult(
$this->processExceptionMessage($e->getMessage())
return Mailer::formatMailerErrorResult(
$this->error_mapper->getErrorFromException($e, $subscriber)
);
}
return ($result === 1) ?
Mailer::formatMailerSendSuccessResult() :
Mailer::formatMailerSendErrorResult($this->processLogMessage($subscriber, $extra_params));
if($result === 1) {
return Mailer::formatMailerSendSuccessResult();
} else {
$error = $this->error_mapper->getErrorFromLog($this->mailer_logger->dump(), $subscriber);
return Mailer::formatMailerErrorResult($error);
}
}
function buildMailer() {
@ -107,27 +115,4 @@ class SMTP {
(isset($subscriber_data['name'])) ? $subscriber_data['name'] : ''
);
}
function processLogMessage($subscriber, $extra_params = array(), $log = false) {
$log = ($log) ? $log : $this->mailer_logger->dump();
// extract error message from log
preg_match('/!! (.*?)>>/ism', $log, $message);
if(!empty($message[1])) {
$message = $message[1];
// remove line breaks from the message due to how logger's dump() method works
$message = preg_replace('/\r|\n/', '', $message);
} else {
$message = sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_SMTP);
}
if(empty($extra_params['test_email'])) {
$message .= sprintf(' %s: %s', __('Unprocessed subscriber', 'mailpoet'), $subscriber);
}
return $message;
}
function processExceptionMessage($message) {
// remove redundant information appended by Swift logger to exception messages
$message = explode(PHP_EOL, $message);
return $message[0];
}
}

View File

@ -3,6 +3,7 @@
namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\Methods\ErrorMappers\SendGridMapper;
use MailPoet\WP\Functions as WPFunctions;
if(!defined('ABSPATH')) exit;
@ -13,10 +14,14 @@ class SendGrid {
public $sender;
public $reply_to;
function __construct($api_key, $sender, $reply_to) {
/** @var SendGridMapper */
private $error_mapper;
function __construct($api_key, $sender, $reply_to, SendGridMapper $error_mapper) {
$this->api_key = $api_key;
$this->sender = $sender;
$this->reply_to = $reply_to;
$this->error_mapper = $error_mapper;
}
function send($newsletter, $subscriber, $extra_params = array()) {
@ -25,17 +30,13 @@ class SendGrid {
$this->request($newsletter, $subscriber, $extra_params)
);
if(is_wp_error($result)) {
return Mailer::formatMailerConnectionErrorResult($result->get_error_message());
$error = $this->error_mapper->getConnectionError($result->get_error_message());
return Mailer::formatMailerErrorResult($error);
}
if(WPFunctions::wpRemoteRetrieveResponseCode($result) !== 200) {
$response = json_decode($result['body'], true);
$response = (!empty($response['errors'][0])) ?
$response['errors'][0] :
sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_SENDGRID);
if(empty($extra_params['test_email'])) {
$response .= sprintf(' %s: %s', __('Unprocessed subscriber', 'mailpoet'), $subscriber);
}
return Mailer::formatMailerSendErrorResult($response);
$error = $this->error_mapper->getErrorFromResponse($response, $subscriber);
return Mailer::formatMailerErrorResult($error);
}
return Mailer::formatMailerSendSuccessResult();
}

View File

@ -0,0 +1,38 @@
<?php
namespace MailPoet\Mailer;
class SubscriberError {
/** @var string */
private $email;
/** @var string|null */
private $message;
/**
* @param string $email
* @param string $message|null
*/
function __construct($email, $message = null) {
$this->email = $email;
$this->message = $message;
}
/**
* @return string
*/
function getEmail() {
return $this->email;
}
/**
* @return null|string
*/
function getMessage() {
return $this->message;
}
function __toString() {
return $this->message ? $this->email . ': ' . $this->message : $this->email;
}
}

View File

@ -7,6 +7,9 @@ class ScheduledTaskSubscriber extends Model {
const STATUS_UNPROCESSED = 0;
const STATUS_PROCESSED = 1;
const FAIL_STATUS_OK = 0;
const FAIL_STATUS_FAILED = 1;
public static $_table = MP_SCHEDULED_TASK_SUBSCRIBERS_TABLE;
public static $_id_column = array('task_id', 'subscriber_id');
@ -19,6 +22,7 @@ class ScheduledTaskSubscriber extends Model {
return;
}
$data['processed'] = !empty($data['processed']) ? self::STATUS_PROCESSED : self::STATUS_UNPROCESSED;
$data['failed'] = !empty($data['failed']) ? self::FAIL_STATUS_FAILED : self::FAIL_STATUS_OK;
return parent::_createOrUpdate($data, array(
'subscriber_id' => $data['subscriber_id'],
'task_id' => $data['task_id']

View File

@ -166,6 +166,11 @@ class Sending {
return $this->updateCount()->getErrors() === false;
}
public function saveSubscriberError($subcriber_id, $error_message) {
$this->task_subscribers->saveSubscriberError($subcriber_id, $error_message);
return $this->updateCount()->getErrors() === false;
}
function updateCount() {
$this->queue->count_processed = ScheduledTaskSubscriber::getProcessedCount($this->task->id);
$this->queue->count_to_process = ScheduledTaskSubscriber::getUnprocessedCount($this->task->id);

View File

@ -53,6 +53,15 @@ class Subscribers {
$this->checkCompleted();
}
function saveSubscriberError($subcriber_id, $error_message) {
$this->getSubscribers()
->where('subscriber_id', $subcriber_id)
->findResultSet()
->set('failed', ScheduledTaskSubscriber::FAIL_STATUS_FAILED)
->set('error', $error_message)
->save();
}
private function checkCompleted($count = null) {
if(!$count && !ScheduledTaskSubscriber::getUnprocessedCount($this->task->id)) {
$this->task->complete();

View File

@ -0,0 +1,56 @@
<?php
namespace MailPoet\Test\Cron\Workers;
use Codeception\Stub;
use Codeception\Stub\Expected;
use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\SubscriberError;
use MailPoet\Tasks\Sending as SendingTask;
class SendingErrorHandlerTest extends \MailPoetTest {
/** @var SendingErrorHandler */
private $error_handler;
function _before() {
$this->error_handler = new SendingErrorHandler();
}
function testItShouldProcessSoftErrorCorrectly() {
$subscribers = [
'john@doe.com',
'john@rambo.com',
];
$subscriber_ids = [1, 2];
$subscriber_errors = [
new SubscriberError('john@doe.com', 'Subscriber Message'),
new SubscriberError('john@rambo.com', null),
];
$error = new MailerError(
MailerError::OPERATION_SEND,
MailerError::LEVEL_SOFT,
'Error Message',
null, $subscriber_errors
);
$sending_task = Stub::make(
SendingTask::class,
[
'saveSubscriberError' => Expected::exactly(
2,
function($id, $message) {
if($id === 2) {
expect($message)->equals('Error Message');
} else {
expect($message)->equals('Subscriber Message');
}
}
),
],
$this
);
$this->error_handler->processError($error, $sending_task, $subscriber_ids, $subscribers);
}
}

View File

@ -8,6 +8,7 @@ use Codeception\Util\Fixtures;
use Codeception\Stub;
use Codeception\Stub\Expected;
use MailPoet\Config\Populator;
use MailPoet\Cron\Workers\SendingQueue\SendingErrorHandler;
use MailPoet\Cron\Workers\SendingQueue\SendingQueue as SendingQueueWorker;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Mailer as MailerTask;
use MailPoet\Cron\Workers\SendingQueue\Tasks\Newsletter as NewsletterTask;
@ -32,6 +33,9 @@ use MailPoet\Tasks\Sending as SendingTask;
use MailPoet\WP\Hooks;
class SendingQueueTest extends \MailPoetTest {
/** @var SendingErrorHandler */
private $sending_error_handler;
function _before() {
$wp_users = get_users();
wp_set_current_user($wp_users[0]->ID);
@ -71,7 +75,8 @@ class SendingQueueTest extends \MailPoetTest {
$this->newsletter_link->url = '[link:subscription_unsubscribe_url]';
$this->newsletter_link->hash = 'abcde';
$this->newsletter_link->save();
$this->sending_queue_worker = new SendingQueueWorker();
$this->sending_error_handler = new SendingErrorHandler();
$this->sending_queue_worker = new SendingQueueWorker($this->sending_error_handler);
}
private function getDirectUnsubscribeURL() {
@ -101,20 +106,20 @@ class SendingQueueTest extends \MailPoetTest {
// constructor accepts timer argument
$timer = microtime(true) - 5;
$sending_queue_worker = new SendingQueueWorker($timer);
$sending_queue_worker = new SendingQueueWorker($this->sending_error_handler, $timer);
expect($sending_queue_worker->timer)->equals($timer);
}
function testItEnforcesExecutionLimitsBeforeQueueProcessing() {
$sending_queue_worker = Stub::make(
new SendingQueueWorker(),
new SendingQueueWorker($this->sending_error_handler),
array(
'processQueue' => Expected::never(),
'enforceSendingAndExecutionLimits' => Expected::exactly(1, function() {
throw new \Exception();
})
), $this);
$sending_queue_worker->__construct();
$sending_queue_worker->__construct($this->sending_error_handler);
try {
$sending_queue_worker->process();
self::fail('Execution limits function was not called.');
@ -125,24 +130,25 @@ class SendingQueueTest extends \MailPoetTest {
function testItEnforcesExecutionLimitsAfterSendingWhenQueueStatusIsNotSetToComplete() {
$sending_queue_worker = Stub::make(
new SendingQueueWorker(),
new SendingQueueWorker($this->sending_error_handler),
array(
'enforceSendingAndExecutionLimits' => Expected::exactly(1)
), $this);
$sending_queue_worker->__construct(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
array(
'send' => null
'sendBulk' => null
)
)
);
$sending_queue_worker->sendNewsletters(
$this->queue,
$prepared_subscribers = array(),
$prepared_newsletters = false,
$prepared_subscribers = false,
$prepared_newsletters = [],
$prepared_subscribers = [],
$statistics[] = array(
'newsletter_id' => 1,
'subscriber_id' => 1,
@ -158,16 +164,17 @@ class SendingQueueTest extends \MailPoetTest {
$queue = $this->queue;
$queue->status = SendingQueue::STATUS_COMPLETED;
$sending_queue_worker = Stub::make(
new SendingQueueWorker(),
new SendingQueueWorker($this->sending_error_handler),
array(
'enforceSendingAndExecutionLimits' => Expected::never()
), $this);
$sending_queue_worker->__construct(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
array(
'send' => null
'sendBulk' => null
)
)
);
@ -186,7 +193,7 @@ class SendingQueueTest extends \MailPoetTest {
function testItEnforcesExecutionLimitsAfterQueueProcessing() {
$sending_queue_worker = Stub::make(
new SendingQueueWorker(),
new SendingQueueWorker($this->sending_error_handler),
array(
'processQueue' => function() {
// this function returns a queue object
@ -194,7 +201,7 @@ class SendingQueueTest extends \MailPoetTest {
},
'enforceSendingAndExecutionLimits' => Expected::exactly(2)
), $this);
$sending_queue_worker->__construct();
$sending_queue_worker->__construct($this->sending_error_handler);
$sending_queue_worker->process();
}
@ -217,6 +224,7 @@ class SendingQueueTest extends \MailPoetTest {
Setting::setValue('tracking.enabled', false);
$directUnsubscribeURL = $this->getDirectUnsubscribeURL();
$sending_queue_worker = new SendingQueueWorker(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
@ -237,6 +245,7 @@ class SendingQueueTest extends \MailPoetTest {
Setting::setValue('tracking.enabled', true);
$trackedUnsubscribeURL = $this->getTrackedUnsubscribeURL();
$sending_queue_worker = new SendingQueueWorker(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
@ -255,6 +264,7 @@ class SendingQueueTest extends \MailPoetTest {
function testItCanProcessSubscribersOneByOne() {
$sending_queue_worker = new SendingQueueWorker(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
@ -298,11 +308,12 @@ class SendingQueueTest extends \MailPoetTest {
function testItCanProcessSubscribersInBulk() {
$sending_queue_worker = new SendingQueueWorker(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
array(
'send' => Expected::exactly(1, function($newsletter, $subscriber) {
'sendBulk' => Expected::exactly(1, function($newsletter, $subscriber) {
// newsletter body should not be empty
expect(!empty($newsletter[0]['body']['html']))->true();
expect(!empty($newsletter[0]['body']['text']))->true();
@ -344,6 +355,7 @@ class SendingQueueTest extends \MailPoetTest {
function testItProcessesStandardNewsletters() {
$sending_queue_worker = new SendingQueueWorker(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
@ -397,6 +409,7 @@ class SendingQueueTest extends \MailPoetTest {
$this->newsletter_segment->delete();
$sending_queue_worker = new SendingQueueWorker(
$this->sending_error_handler,
$timer = false,
Stub::makeEmpty(new MailerTask(), array(), $this)
);
@ -411,6 +424,7 @@ class SendingQueueTest extends \MailPoetTest {
$this->newsletter_segment->delete();
$sending_queue_worker = new SendingQueueWorker(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
@ -573,27 +587,28 @@ class SendingQueueTest extends \MailPoetTest {
}
function testItPausesSendingWhenProcessedSubscriberListCannotBeUpdated() {
$queue = Mock::double(new \stdClass(), array(
$sending_task = Mock::double(SendingTask::create(), array(
'updateProcessedSubscribers' => false
));
$queue->id = 100;
$sending_queue_worker = Stub::make(new SendingQueueWorker());
$sending_task->id = 100;
$sending_queue_worker = Stub::make(new SendingQueueWorker($this->sending_error_handler));
$sending_queue_worker->__construct(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
array(
'send' => true
'sendBulk' => true
)
)
);
try {
$sending_queue_worker->sendNewsletters(
$queue,
$prepared_subscribers = array(),
$prepared_newsletters = false,
$prepared_subscribers = false,
$statistics = false
$sending_task->getObject(),
$prepared_subscribers = [],
$prepared_newsletters = [],
$prepared_subscribers = [],
$statistics = []
);
$this->fail('Paused sending exception was not thrown.');
} catch(\Exception $e) {
@ -611,6 +626,7 @@ class SendingQueueTest extends \MailPoetTest {
function testItDoesNotUpdateNewsletterHashDuringSending() {
$sending_queue_worker = new SendingQueueWorker(
$this->sending_error_handler,
$timer = false,
Stub::make(
new MailerTask(),
@ -634,7 +650,7 @@ class SendingQueueTest extends \MailPoetTest {
return $custom_batch_size_value;
};
Hooks::addFilter('mailpoet_cron_worker_sending_queue_batch_size', $filter);
$sending_queue_worker = new SendingQueueWorker();
$sending_queue_worker = new SendingQueueWorker($this->sending_error_handler);
expect($sending_queue_worker->batch_size)->equals($custom_batch_size_value);
Hooks::removeFilter('mailpoet_cron_worker_sending_queue_batch_size', $filter);
}

View File

@ -114,7 +114,10 @@ class MailerTest extends \MailPoetTest {
return true;
})),
$this
)
),
'mailer_config' => [
'method' => null,
]
)
);
// mailer instance should be properly configured

View File

@ -0,0 +1,40 @@
<?php
namespace MailPoet\Test\Mailer;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\SubscriberError;
class MailerErrorTest extends \MailPoetTest {
function testItCanComposeErrorMessageWithoutSubscribers() {
$error = new MailerError(MailerError::OPERATION_SEND, MailerError::LEVEL_HARD, 'Some Message');
expect($error->getMessageWithFailedSubscribers())->equals('Some Message');
}
function testItCanComposeErrorMessageWithOneSubscriber() {
$subscriber_error = new SubscriberError('email@example.com', 'Subscriber message');
$error = new MailerError(
MailerError::OPERATION_SEND,
MailerError::LEVEL_HARD,
'Some Message',
null,
[$subscriber_error]
);
expect($error->getMessageWithFailedSubscribers())->equals('Some Message Unprocessed subscriber: (email@example.com: Subscriber message)');
}
function testItCanComposeErrorMessageWithMultipleSubscriberErrors() {
$subscriber_error_1 = new SubscriberError('email1@example.com', 'Subscriber 1 message');
$subscriber_error_2 = new SubscriberError('email2@example.com', null);
$error = new MailerError(
MailerError::OPERATION_SEND,
MailerError::LEVEL_HARD,
'Some Message',
null,
[$subscriber_error_1, $subscriber_error_2]
);
expect($error->getMessageWithFailedSubscribers())->equals(
'Some Message Unprocessed subscribers: (email1@example.com: Subscriber 1 message), (email2@example.com)'
);
}
}

View File

@ -1,7 +1,9 @@
<?php
namespace MailPoet\Test\Mailer\Methods;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Methods\AmazonSES;
use MailPoet\Mailer\Methods\ErrorMappers\AmazonSESMapper;
class AmazonSESTest extends \MailPoetTest {
function _before() {
@ -34,7 +36,8 @@ class AmazonSESTest extends \MailPoetTest {
$this->settings['secret_key'],
$this->sender,
$this->reply_to,
$this->return_path
$this->return_path,
new AmazonSESMapper()
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(
@ -69,7 +72,8 @@ class AmazonSESTest extends \MailPoetTest {
$this->settings['secret_key'],
$this->sender,
$this->reply_to,
$return_path = false
$return_path = false,
new AmazonSESMapper()
);
expect($mailer->return_path)->equals($this->sender['from_email']);
}
@ -82,7 +86,8 @@ class AmazonSESTest extends \MailPoetTest {
$this->settings['secret_key'],
$this->sender,
$this->reply_to,
$this->return_path
$this->return_path,
new AmazonSESMapper()
);
$this->fail('Unsupported region exception was not thrown');
} catch(\Exception $e) {
@ -223,7 +228,8 @@ class AmazonSESTest extends \MailPoetTest {
$invalid_subscriber
);
expect($result['response'])->false();
expect($result['error_message'])->contains('does not comply with RFC 2822');
expect($result['error'])->isInstanceOf(MailerError::class);
expect($result['error']->getMessage())->contains('does not comply with RFC 2822');
}
function testItCanSend() {

View File

@ -0,0 +1,56 @@
<?php
namespace MailPoet\Test\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Methods\ErrorMappers\AmazonSESMapper;
use SimpleXMLElement;
class AmazonSESMapperTest extends \MailPoetTest {
/** @var AmazonSESMapper*/
private $mapper;
/** @var array */
private $response_data = [];
function _before() {
$this->mapper = new AmazonSESMapper();
$this->response_data = [
'Error' => [
'Type' => 'Sender',
'Code' => 'ConfigurationSetDoesNotExist',
'Message' => 'Some message',
],
'RequestId' => '01ca93ec-b5a3-11e8-bff8-49dd5ddf8019',
];
}
function testGetProperError() {
$response = $this->buildXmlResponseFromArray($this->response_data, new SimpleXMLElement('<response/>'));
$error = $this->mapper->getErrorFromResponse($response, 'john@rambo.com');
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getMessage())->equals('Some message');
expect($error->getSubscriberErrors()[0]->getEmail())->equals('john@rambo.com');
}
function testGetSoftErrorForRejectedMessage() {
$this->response_data['Error']['Code'] = 'MessageRejected';
$response = $this->buildXmlResponseFromArray($this->response_data, new SimpleXMLElement('<response/>'));
$error = $this->mapper->getErrorFromResponse($response, 'john@rambo.com');
expect($error->getLevel())->equals(MailerError::LEVEL_SOFT);
}
/**
* @return SimpleXMLElement
*/
private function buildXmlResponseFromArray($response_data, SimpleXMLElement $xml) {
foreach($response_data as $tag => $value) {
if(is_array($value)) {
$this->buildXmlResponseFromArray($value, $xml->addChild($tag));
} else {
$xml->addChild($tag, $value);
}
}
return $xml;
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace MailPoet\Test\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Methods\ErrorMappers\MailPoetMapper;
use MailPoet\Services\Bridge\API;
class MailPoetMapperTest extends \MailPoetTest {
/** @var MailPoetMapper */
private $mapper;
/** @var array */
private $subscribers;
function _before() {
$this->mapper = new MailPoetMapper();
$this->subscribers = ['a@example.com', 'c d <b@example.com>'];
}
function testCreateConnectionError() {
$error = $this->mapper->getConnectionError('connection error');
expect($error)->isInstanceOf(MailerError::class);
expect($error->getOperation())->equals(MailerError::OPERATION_CONNECT);
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getMessage())->equals('connection error');
}
function testGetErrorNotArray() {
$api_result = [
'code' => API::RESPONSE_CODE_NOT_ARRAY,
'status' => API::SENDING_STATUS_SEND_ERROR,
'message' => 'error not array',
];
$error = $this->mapper->getErrorForResult($api_result, $this->subscribers);
expect($error)->isInstanceOf(MailerError::class);
expect($error->getOperation())->equals(MailerError::OPERATION_SEND);
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getMessage())->equals('JSON input is not an array');
}
function testGetErrorPayloadTooBig() {
$api_result = [
'code' => API::RESPONSE_CODE_PAYLOAD_TOO_BIG,
'status' => API::SENDING_STATUS_SEND_ERROR,
'message' => 'error too big',
];
$error = $this->mapper->getErrorForResult($api_result, $this->subscribers);
expect($error)->isInstanceOf(MailerError::class);
expect($error->getOperation())->equals(MailerError::OPERATION_SEND);
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getMessage())->equals('error too big');
}
function testGetPayloadError() {
$api_result = [
'code' => API::RESPONSE_CODE_PAYLOAD_ERROR,
'status' => API::SENDING_STATUS_SEND_ERROR,
'message' => 'Api Error',
];
$error = $this->mapper->getErrorForResult($api_result, $this->subscribers);
expect($error)->isInstanceOf(MailerError::class);
expect($error->getOperation())->equals(MailerError::OPERATION_SEND);
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getMessage())->equals('Error while sending. Api Error');
}
function testGetPayloadErrorWithErrorMessage() {
$api_result = [
'code' => API::RESPONSE_CODE_PAYLOAD_ERROR,
'status' => API::SENDING_STATUS_SEND_ERROR,
'message' => '[{"index":0,"errors":{"subject":"subject is missing"}},{"index":1,"errors":{"subject":"subject is missing"}}]'
];
$error = $this->mapper->getErrorForResult($api_result, $this->subscribers);
expect($error)->isInstanceOf(MailerError::class);
expect($error->getOperation())->equals(MailerError::OPERATION_SEND);
expect($error->getLevel())->equals(MailerError::LEVEL_SOFT);
$subscriber_errors = $error->getSubscriberErrors();
expect(count($subscriber_errors))->equals(2);
expect($subscriber_errors[0]->getEmail())->equals('a@example.com');
expect($subscriber_errors[0]->getMessage())->equals('subject is missing');
expect($subscriber_errors[1]->getEmail())->equals('c d <b@example.com>');
expect($subscriber_errors[1]->getMessage())->equals('subject is missing');
}
function testGetPayloadErrorForMalformedMSSResponse() {
$api_result = [
'code' => API::RESPONSE_CODE_PAYLOAD_ERROR,
'status' => API::SENDING_STATUS_SEND_ERROR,
'message' => '[{"errors":{"subject":"subject is missing"}},{"errors":{"subject":"subject is missing"}}]'
];
$error = $this->mapper->getErrorForResult($api_result, $this->subscribers);
expect($error)->isInstanceOf(MailerError::class);
expect($error->getOperation())->equals(MailerError::OPERATION_SEND);
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getMessage())->equals('Error while sending. Invalid MSS response format.');
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace MailPoet\Test\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Methods\ErrorMappers\PHPMailMapper;
class PHPMailMapperTest extends \MailPoetTest {
/** @var PHPMailMapper*/
private $mapper;
function _before() {
$this->mapper = new PHPMailMapper();
}
function testGetProperErrorForSubscriber() {
$error = $this->mapper->getErrorForSubscriber('john@rambo.com');
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getMessage())->equals('PHPMail has returned an unknown error.');
expect($error->getSubscriberErrors()[0]->getEmail())->equals('john@rambo.com');
}
function testGetProperErrorFromException() {
$error = $this->mapper->getErrorFromException(new \Exception('Some message'), 'john@rambo.com');
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getMessage())->equals('Some message');
expect($error->getSubscriberErrors()[0]->getEmail())->equals('john@rambo.com');
}
function testGetSoftErrorFromExceptionForInvalidEmail() {
$error = $this->mapper->getErrorFromException(new \Exception('Invalid address. (Add ...'), 'john@rambo.com');
expect($error->getLevel())->equals(MailerError::LEVEL_SOFT);
}
}

View File

@ -0,0 +1,57 @@
<?php
namespace MailPoet\Test\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Methods\ErrorMappers\SMTPMapper;
class SMTPMapperTest extends \MailPoetTest {
/** @var SMTPMapper */
private $mapper;
function _before() {
$this->mapper = new SMTPMapper();
}
function testItCanProcessExceptionMessage() {
$message = 'Connection could not be established with host localhost [Connection refused #111]' . PHP_EOL
. 'Log data:' . PHP_EOL
. '++ Starting Swift_SmtpTransport' . PHP_EOL
. '!! Connection could not be established with host localhost [Connection refused #111] (code: 0)';
$error = $this->mapper->getErrorFromException(new \Exception($message), 'john@rambo.com');
expect($error->getMessage())
->equals('Connection could not be established with host localhost [Connection refused #111]');
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getSubscriberErrors()[0]->getEmail())->equals('john@rambo.com');
}
function testItCreatesSoftErrorForInvalidEmail() {
$message = 'Invalid email';
$error = $this->mapper->getErrorFromException(new \Swift_RfcComplianceException($message), 'john@rambo.com');
expect($error->getLevel())->equals(MailerError::LEVEL_SOFT);
}
function testItCanProcessLogMessageWhenOneExists() {
$log = '++ Swift_SmtpTransport started' . PHP_EOL
. '>> MAIL FROM:<moi@mrcasual.com>' . PHP_EOL
. '<< 250 OK' . PHP_EOL
. '>> RCPT TO:<test2@ietsdoenofferte.nl>' . PHP_EOL
. '<< 550 No such recipient here' . PHP_EOL
. '!! Expected response code 250/251/252 but got code "550", with message "550 No such recipient here' . PHP_EOL
. '" (code: 550)' . PHP_EOL
. '>> RSET' . PHP_EOL
. '<< 250 Reset OK' . PHP_EOL;
$error = $this->mapper->getErrorFromLog($log, 'test@example.com');
expect($error->getMessage())
->equals('Expected response code 250/251/252 but got code "550", with message "550 No such recipient here" (code: 550)');
expect($error->getSubscriberErrors()[0]->getEmail('moi@mrcasual.com'));
}
function testItReturnsGenericMessageWhenLogMessageDoesNotExist() {
$error = $this->mapper->getErrorFromLog(null, 'test@example.com');
expect($error->getMessage())
->equals(Mailer::METHOD_SMTP . ' has returned an unknown error.');
expect($error->getSubscriberErrors()[0]->getEmail('moi@mrcasual.com'));
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace MailPoet\Test\Mailer\Methods\ErrorMappers;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Methods\ErrorMappers\SendGridMapper;
class SendGridMapperTest extends \MailPoetTest {
/** @var SendGridMapper*/
private $mapper;
/** @var array */
private $response = [];
function _before() {
$this->mapper = new SendGridMapper();
$this->response = [
'errors' => [
'Some message',
]
];
}
function testGetProperError() {
$error = $this->mapper->getErrorFromResponse($this->response, 'john@rambo.com');
expect($error->getLevel())->equals(MailerError::LEVEL_HARD);
expect($error->getMessage())->equals('Some message');
expect($error->getSubscriberErrors()[0]->getEmail())->equals('john@rambo.com');
}
function testGetSoftErrorForInvalidEmail() {
$this->response['errors'][0] = 'Invalid email address ,,@';
$error = $this->mapper->getErrorFromResponse($this->response, ',,@');
expect($error->getLevel())->equals(MailerError::LEVEL_SOFT);
}
}

View File

@ -3,6 +3,8 @@ namespace MailPoet\Test\Mailer\Methods;
use Codeception\Util\Stub;
use MailPoet\Config\ServicesChecker;
use MailPoet\Mailer\MailerError;
use MailPoet\Mailer\Methods\ErrorMappers\MailPoetMapper;
use MailPoet\Mailer\Methods\MailPoet;
use MailPoet\Services\Bridge\API;
@ -27,7 +29,8 @@ class MailPoetAPITest extends \MailPoetTest {
$this->mailer = new MailPoet(
$this->settings['api_key'],
$this->sender,
$this->reply_to
$this->reply_to,
new MailPoetMapper()
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(
@ -161,11 +164,9 @@ class MailPoetAPITest extends \MailPoetTest {
$this
);
$result = $this->mailer->send($this->newsletter, $this->subscriber);
expect($result)->equals([
'response' => false,
'operation' => 'connect',
'error_message' => 'connection error',
]);
expect($result['response'])->false();
expect($result['error'])->isInstanceOf(MailerError::class);
expect($result['error']->getOperation())->equals(MailerError::OPERATION_CONNECT);
}
function testFormatErrorNotArray() {
@ -179,11 +180,9 @@ class MailPoetAPITest extends \MailPoetTest {
$this
);
$result = $this->mailer->send($this->newsletter, $this->subscriber);
expect($result)->equals([
'response' => false,
'operation' => 'send',
'error_message' => 'JSON input is not an array',
]);
expect($result['response'])->false();
expect($result['error'])->isInstanceOf(MailerError::class);
expect($result['error']->getOperation())->equals(MailerError::OPERATION_SEND);
}
function testFormatErrorTooBig() {
@ -197,11 +196,8 @@ class MailPoetAPITest extends \MailPoetTest {
$this
);
$result = $this->mailer->send($this->newsletter, $this->subscriber);
expect($result)->equals([
'response' => false,
'operation' => 'send',
'error_message' => 'error too big',
]);
expect($result['response'])->false();
expect($result['error'])->isInstanceOf(MailerError::class);
}
function testFormatPayloadError() {
@ -215,11 +211,9 @@ class MailPoetAPITest extends \MailPoetTest {
$this
);
$result = $this->mailer->send([$this->newsletter, $this->newsletter], ['a@example.com', 'c d <b@example.com>']);
expect($result)->equals([
'response' => false,
'operation' => 'send',
'error_message' => 'Error while sending newsletters. Api Error',
]);
expect($result['response'])->false();
expect($result['error'])->isInstanceOf(MailerError::class);
expect($result['error']->getOperation())->equals(MailerError::OPERATION_SEND);
}
function testFormatPayloadErrorWithErrorMessage() {
@ -233,12 +227,8 @@ class MailPoetAPITest extends \MailPoetTest {
$this
);
$result = $this->mailer->send([$this->newsletter, $this->newsletter], ['a@example.com', 'c d <b@example.com>']);
expect($result)->equals([
'response' => false,
'operation' => 'send',
'error_message' => 'Error while sending: (a@example.com: subject is missing), (c d <b@example.com>: subject is missing)',
]);
expect($result['response'])->false();
expect($result['error'])->isInstanceOf(MailerError::class);
expect($result['error']->getOperation())->equals(MailerError::OPERATION_SEND);
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace MailPoet\Test\Mailer\Methods;
use MailPoet\Mailer\Methods\ErrorMappers\PHPMailMapper;
use MailPoet\Mailer\Methods\PHPMail;
class PHPMailTest extends \MailPoetTest {
@ -19,7 +20,8 @@ class PHPMailTest extends \MailPoetTest {
$this->mailer = new PHPMail(
$this->sender,
$this->reply_to,
$this->return_path
$this->return_path,
new PHPMailMapper()
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(
@ -44,7 +46,8 @@ class PHPMailTest extends \MailPoetTest {
$mailer = new PHPMail(
$this->sender,
$this->reply_to,
$return_path = false
$return_path = false,
new PHPMailMapper()
);
expect($mailer->return_path)->equals($this->sender['from_email']);
}

View File

@ -3,6 +3,7 @@ namespace MailPoet\Test\Mailer\Methods;
use Helper\WordPressHooks as WPHooksHelper;
use MailPoet\Mailer\Mailer;
use MailPoet\Mailer\Methods\ErrorMappers\SMTPMapper;
use MailPoet\Mailer\Methods\SMTP;
use MailPoet\WP\Hooks;
@ -43,7 +44,8 @@ class SMTPTest extends \MailPoetTest {
$this->settings['encryption'],
$this->sender,
$this->reply_to,
$this->return_path
$this->return_path,
new SMTPMapper()
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(
@ -82,7 +84,8 @@ class SMTPTest extends \MailPoetTest {
$this->settings['encryption'],
$this->sender,
$this->reply_to,
$return_path = false
$return_path = false,
new SMTPMapper()
);
expect($mailer->return_path)->equals($this->sender['from_email']);
}
@ -128,36 +131,6 @@ class SMTPTest extends \MailPoetTest {
expect($result['response'])->false();
}
function testItCanProcessExceptionMessage() {
$message = 'Connection could not be established with host localhost [Connection refused #111]' . PHP_EOL
. 'Log data:' . PHP_EOL
. '++ Starting Swift_SmtpTransport' . PHP_EOL
. '!! Connection could not be established with host localhost [Connection refused #111] (code: 0)';
expect($this->mailer->processExceptionMessage($message))
->equals('Connection could not be established with host localhost [Connection refused #111]');
}
function testItCanProcessLogMessageWhenOneExists() {
$message = '++ Swift_SmtpTransport started' . PHP_EOL
. '>> MAIL FROM:<moi@mrcasual.com>' . PHP_EOL
. '<< 250 OK' . PHP_EOL
. '>> RCPT TO:<test2@ietsdoenofferte.nl>' . PHP_EOL
. '<< 550 No such recipient here' . PHP_EOL
. '!! Expected response code 250/251/252 but got code "550", with message "550 No such recipient here' . PHP_EOL
. '" (code: 550)' . PHP_EOL
. '>> RSET' . PHP_EOL
. '<< 250 Reset OK' . PHP_EOL;
expect($this->mailer->processLogMessage('test@example.com', $extra_params = array(), $message))
->equals('Expected response code 250/251/252 but got code "550", with message "550 No such recipient here" (code: 550) Unprocessed subscriber: test@example.com');
expect($this->mailer->processLogMessage('test@example.com', $extra_params = array(), $message))
->equals('Expected response code 250/251/252 but got code "550", with message "550 No such recipient here" (code: 550) Unprocessed subscriber: test@example.com');
}
function testItReturnsGenericMessageWhenLogMessageDoesNotExist() {
expect($this->mailer->processLogMessage('test@example.com'))
->equals(Mailer::METHOD_SMTP . ' has returned an unknown error. Unprocessed subscriber: test@example.com');
}
function testItAppliesTransportFilter() {
$mailer = $this->mailer->buildMailer();
expect($mailer->getTransport()->getStreamOptions())->isEmpty();

View File

@ -1,6 +1,7 @@
<?php
namespace MailPoet\Test\Mailer\Methods;
use MailPoet\Mailer\Methods\ErrorMappers\SendGridMapper;
use MailPoet\Mailer\Methods\SendGrid;
class SendGridTest extends \MailPoetTest {
@ -24,7 +25,8 @@ class SendGridTest extends \MailPoetTest {
$this->mailer = new SendGrid(
$this->settings['api_key'],
$this->sender,
$this->reply_to
$this->reply_to,
new SendGridMapper()
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(