Merge pull request #709 from mailpoet/sending_restart

Sending retry/pause/restart [MAILPOET-653]
This commit is contained in:
Jonathan Labreuille
2016-11-29 13:51:53 +01:00
committed by GitHub
33 changed files with 671 additions and 688 deletions

View File

@ -298,7 +298,8 @@ const Listing = React.createClass({
filters: {},
filter: {},
selected_ids: [],
selection: false
selection: false,
meta: {}
};
},
getParam: function(param) {
@ -463,15 +464,21 @@ const Listing = React.createClass({
items: response.data || [],
filters: response.meta.filters || {},
groups: response.meta.groups || [],
count: response.meta.count || 0
count: response.meta.count || 0,
meta: _.omit(response.meta, ['filters', 'groups', 'count'])
}, () => {
// if viewing an empty trash
if (this.state.group === 'trash' && response.meta.count === 0) {
// redirect to default group
this.handleGroup('all');
}
// trigger afterGetItems callback if specified
if (this.props.afterGetItems !== undefined) {
this.props.afterGetItems(this.state);
}
});
}).fail(function(response) {
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function(error) { return error.message; }),
@ -711,7 +718,7 @@ const Listing = React.createClass({
}.bind(this));
},
handleRenderItem: function(item, actions) {
const render = this.props.onRenderItem(item, actions);
const render = this.props.onRenderItem(item, actions, this.state.meta);
return render.props.children;
},
handleRefreshItems: function() {

View File

@ -1,4 +1,5 @@
import React from 'react'
import ReactDOM from 'react-dom'
import MailPoet from 'mailpoet'
import classNames from 'classnames'
import jQuery from 'jquery'
@ -42,11 +43,15 @@ const _QueueMixin = {
}
});
},
renderQueueStatus: function(newsletter) {
renderQueueStatus: function(newsletter, mailer_log) {
if (!newsletter.queue) {
return (
<span>{MailPoet.I18n.t('notSentYet')}</span>
);
} else if (mailer_log.status === 'paused') {
return (
<span>{MailPoet.I18n.t('paused')}</span>
)
} else {
if (newsletter.queue.status === 'scheduled') {
return (
@ -72,14 +77,8 @@ const _QueueMixin = {
<span>
{
MailPoet.I18n.t('newsletterQueueCompleted')
.replace(
"%$1d",
newsletter.queue.count_processed - newsletter.queue.count_failed
)
.replace(
"%$2d",
newsletter.queue.count_total
)
.replace("%$1d",newsletter.queue.count_processed)
.replace("%$2d", newsletter.queue.count_total)
}
</span>
);
@ -175,5 +174,66 @@ const _StatisticsMixin = {
}
}
const _MailerMixin = {
checkMailerStatus: function(state) {
if (state.meta.mta_log.error && state.meta.mta_log.status === 'paused') {
MailPoet.Notice.error(
'',
{ static: true, id: 'mailpoet_mailer_error' }
);
ReactDOM.render(
this.getMailerError(state),
jQuery('[data-id="mailpoet_mailer_error"]')[0]
);
} else {
MailPoet.Notice.hide('mailpoet_mailer_error');
}
},
getMailerError(state) {
let mailer_error_notice;
if (state.meta.mta_log.error.operation === 'send') {
mailer_error_notice =
MailPoet.I18n.t('mailerSendErrorNotice')
.replace('%$1s', state.meta.mta_method)
.replace('%$2s', state.meta.mta_log.error.error_message);
} else {
mailer_error_notice =
MailPoet.I18n.t('mailerConnectionErrorNotice')
.replace('%$1s', state.meta.mta_log.error.error_message);
}
return (
<div>
<p>{ mailer_error_notice }</p>
<p>{ MailPoet.I18n.t('mailerResumeSendingNotice') }</p>
<p>
<a href="javascript:;"
className="button"
onClick={ this.resumeMailerSending }
>{ MailPoet.I18n.t('mailerResumeSendingButton') }</a>
</p>
</div>
);
},
resumeMailerSending() {
MailPoet.Ajax.post({
endpoint: 'mailer',
action: 'resumeSending'
}).done(function() {
MailPoet.Notice.hide('mailpoet_mailer_error');
MailPoet.Notice.success(MailPoet.I18n.t('mailerSendingResumedNotice'));
window.mailpoet_listing.forceUpdate();
}).fail((response) => {
if (response.errors.length > 0) {
MailPoet.Notice.error(
response.errors.map(function(error) { return error.message; }),
{ scroll: true }
);
}
});
}
}
export { _QueueMixin as QueueMixin };
export { _StatisticsMixin as StatisticsMixin };
export { _StatisticsMixin as StatisticsMixin };
export { _MailerMixin as MailerMixin };

View File

@ -5,6 +5,8 @@ import { createHashHistory } from 'history'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
import { MailerMixin } from 'newsletters/listings/mixins.jsx'
import classNames from 'classnames'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
@ -16,6 +18,8 @@ import {
nthWeekDayValues
} from 'newsletters/scheduling/common.jsx'
const mailpoet_settings = window.mailpoet_settings || {};
const messages = {
onTrash: (response) => {
const count = ~~response.meta.count;
@ -153,6 +157,7 @@ const newsletter_actions = [
];
const NewsletterListNotification = React.createClass({
mixins: [ MailerMixin ],
updateStatus: function(e) {
// make the event persist so that we can still override the selected value
// in the ajax callback
@ -328,6 +333,7 @@ const NewsletterListNotification = React.createClass({
auto_refresh={ true }
sort_by="updated_at"
sort_order="desc"
afterGetItems={ this.checkMailerStatus }
/>
</div>
);

View File

@ -7,9 +7,14 @@ import MailPoet from 'mailpoet'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
import { QueueMixin, StatisticsMixin } from 'newsletters/listings/mixins.jsx'
import {
QueueMixin,
StatisticsMixin,
MailerMixin
} from 'newsletters/listings/mixins.jsx'
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
const mailpoet_settings = window.mailpoet_settings || {};
const columns = [
{
@ -49,7 +54,7 @@ const newsletter_actions = [
];
const NewsletterListNotificationHistory = React.createClass({
mixins: [QueueMixin, StatisticsMixin],
mixins: [ QueueMixin, StatisticsMixin, MailerMixin ],
renderItem: function(newsletter, actions) {
const rowClasses = classNames(
'manage-column',
@ -61,6 +66,8 @@ const NewsletterListNotificationHistory = React.createClass({
return segment.name
}).join(', ');
const mailer_log = window.mailpoet_settings.mta_log || {};
return (
<div>
<td className={ rowClasses }>
@ -73,7 +80,7 @@ const NewsletterListNotificationHistory = React.createClass({
{ actions }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderQueueStatus(newsletter) }
{ this.renderQueueStatus(newsletter, mailpoet_mailer_log) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('lists') }>
{ segments }
@ -116,6 +123,7 @@ const NewsletterListNotificationHistory = React.createClass({
auto_refresh={ true }
sort_by="updated_at"
sort_order="desc"
afterGetItems={ this.checkMailerStatus }
/>
</div>
);

View File

@ -7,9 +7,14 @@ import MailPoet from 'mailpoet'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
import { QueueMixin, StatisticsMixin } from 'newsletters/listings/mixins.jsx'
import {
QueueMixin,
StatisticsMixin,
MailerMixin
} from 'newsletters/listings/mixins.jsx'
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
const mailpoet_settings = window.mailpoet_settings || {};
const messages = {
onTrash: (response) => {
@ -85,7 +90,6 @@ const columns = [
}
];
const bulk_actions = [
{
name: 'trash',
@ -148,8 +152,8 @@ const newsletter_actions = [
];
const NewsletterListStandard = React.createClass({
mixins: [QueueMixin, StatisticsMixin],
renderItem: function(newsletter, actions) {
mixins: [ QueueMixin, StatisticsMixin, MailerMixin ],
renderItem: function(newsletter, actions, meta) {
const rowClasses = classNames(
'manage-column',
'column-primary',
@ -172,7 +176,7 @@ const NewsletterListStandard = React.createClass({
{ actions }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('status') }>
{ this.renderQueueStatus(newsletter) }
{ this.renderQueueStatus(newsletter, meta.mta_log) }
</td>
<td className="column" data-colname={ MailPoet.I18n.t('lists') }>
{ segments }
@ -212,6 +216,7 @@ const NewsletterListStandard = React.createClass({
auto_refresh={ true }
sort_by="updated_at"
sort_order="desc"
afterGetItems={ this.checkMailerStatus }
/>
</div>
);

View File

@ -5,6 +5,8 @@ import { createHashHistory } from 'history'
import Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx'
import { MailerMixin } from 'newsletters/listings/mixins.jsx'
import classNames from 'classnames'
import jQuery from 'jquery'
import MailPoet from 'mailpoet'
@ -13,6 +15,7 @@ import _ from 'underscore'
const mailpoet_roles = window.mailpoet_roles || {};
const mailpoet_segments = window.mailpoet_segments || {};
const mailpoet_tracking_enabled = (!!(window['mailpoet_tracking_enabled']));
const mailpoet_settings = window.mailpoet_settings || {};
const messages = {
onTrash: (response) => {
@ -151,6 +154,7 @@ const newsletter_actions = [
];
const NewsletterListWelcome = React.createClass({
mixins: [ MailerMixin ],
updateStatus: function(e) {
// make the event persist so that we can still override the selected value
// in the ajax callback
@ -358,6 +362,7 @@ const NewsletterListWelcome = React.createClass({
auto_refresh={ true }
sort_by="updated_at"
sort_order="desc"
afterGetItems={ this.checkMailerStatus }
/>
</div>
);

View File

@ -27,7 +27,7 @@ const App = React.createClass({
const container = document.getElementById('newsletters_container');
if(container) {
ReactDOM.render((
const mailpoet_listing = ReactDOM.render((
<Router history={ history }>
<Route path="/" component={ App }>
<IndexRedirect to="standard" />
@ -49,4 +49,6 @@ if(container) {
</Route>
</Router>
), container);
window.mailpoet_listing = mailpoet_listing;
}

View File

@ -5,14 +5,14 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
MailPoet Notice:
description: Handles notices
version: 0.2
version: 1.0
author: Jonathan Labreuille
company: Wysija
dependencies: jQuery
Usage:
// success message (static: false)
// success message (static: false)
MailPoet.Notice.success('Yatta!');
// error message (static: false)
@ -21,199 +21,206 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
// system message (static: true)
MailPoet.Notice.system('You need to updated ASAP!');
Examples:
MailPoet.Notice.success('- success #1 -');
setTimeout(function() {
MailPoet.Notice.success('- success #2 -');
setTimeout(function() {
MailPoet.Notice.error('- error -');
setTimeout(function() {
MailPoet.Notice.system('- system -');
setTimeout(function() {
MailPoet.Notice.hide();
}, 2500);
}, 300);
}, 400);
}, 500);
==================================================================================================*/
MailPoet.Notice = {
version: 0.2,
// default options
defaults: {
type: 'success',
message: '',
static: false,
hideClose: false,
id: null,
positionAfter: false,
scroll: false,
timeout: 5000,
onOpen: null,
onClose: null
},
options: {},
init: function(options) {
// set options
this.options = jQuery.extend({}, this.defaults, options);
version: 1.0,
// default options
defaults: {
type: 'success',
message: '',
static: false,
hideClose: false,
id: null,
positionAfter: false,
scroll: false,
timeout: 5000,
onOpen: null,
onClose: null
},
options: {},
init: function(options) {
// set options
this.options = jQuery.extend({}, this.defaults, options);
// clone element
this.element = jQuery('#mailpoet_notice_'+this.options.type).clone();
return this;
},
createNotice: function() {
// clone element
this.element = jQuery('#mailpoet_notice_'+this.options.type).clone();
// add data-id to the element
if (this.options.id) this.element.attr('data-id', 'notice_' + this.options.id);
// remove id from clone
this.element.removeAttr('id');
// insert notice after its parent
var positionAfter;
if (typeof this.options.positionAfter === 'object') {
positionAfter = this.options.positionAfter;
} else if (typeof this.options.positionAfter === 'string') {
positionAfter = jQuery(this.options.positionAfter);
} else {
positionAfter = jQuery('#mailpoet_notice_'+this.options.type);
}
positionAfter.after(this.element);
// setup onClose callback
var onClose = null;
if(this.options.onClose !== null) {
onClose = this.options.onClose;
}
// listen to remove event
jQuery(this.element).on('close', function() {
jQuery(this).fadeOut(200, function() {
// on close callback
if(onClose !== null) {
onClose();
}
// remove notice
jQuery(this).remove();
});
}.bind(this.element));
// listen to message event
jQuery(this.element).on('message', function(e, message) {
MailPoet.Notice.setMessage(message);
}.bind(this.element));
return this;
},
isHTML: function(str) {
var a = document.createElement('div');
a.innerHTML = str;
for(var c = a.childNodes, i = c.length; i--;) {
if(c[i].nodeType == 1) return true;
}
return false;
},
setMessage: function(message) {
// if it's not an html message, let's sugar coat the message with a fancy <p>
if(this.isHTML(message) === false) {
message = '<p>'+message+'</p>';
}
// set message
return this.element.html(message);
},
show: function(options) {
// initialize
this.init(options);
// show notice
this.showNotice();
// return this;
},
showNotice: function() {
// set message
this.setMessage(this.options.message);
// position notice
this.element.insertAfter(jQuery('h2.title'));
// set class name
switch(this.options.type) {
case 'success':
this.element.addClass('updated');
break;
case 'system':
this.element.addClass('update-nag');
break;
case 'error':
this.element.addClass('error');
break;
}
// make the notice appear
this.element.fadeIn(200);
// if scroll option is enabled, scroll to the notice
if(this.options.scroll === true) {
this.element.get(0).scrollIntoView(false);
}
// if the notice is not static, it has to disappear after a timeout
if(this.options.static === false) {
this.element.delay(this.options.timeout).trigger('close');
} else if (this.options.hideClose === false) {
this.element.append('<a href="javascript:;" class="mailpoet_notice_close"><span class="dashicons dashicons-dismiss"></span></a>');
this.element.find('.mailpoet_notice_close').on('click', function() {
jQuery(this).trigger('close');
});
}
// call onOpen callback
if(this.options.onOpen !== null) {
this.options.onOpen(this.element);
}
},
hide: function(all) {
if(all !== undefined && all === true) {
jQuery('.mailpoet_notice:not([id])').trigger('close');
} else if (all !== undefined && jQuery.isArray(all)) {
for (var id in all) {
jQuery('[data-id="notice_' + all[id] + '"]')
.trigger('close');
}
} if (all !== undefined) {
jQuery('[data-id="notice_' + all + '"]')
.trigger('close');
} else {
jQuery('.mailpoet_notice.updated:not([id]), .mailpoet_notice.error:not([id])')
.trigger('close');
}
},
error: function(message, options) {
this.show(jQuery.extend({}, {
type: 'error',
message: '<p>'+this.formatMessage(message)+'</p>'
}, options));
},
success: function(message, options) {
this.show(jQuery.extend({}, {
type: 'success',
message: '<p>'+this.formatMessage(message)+'</p>'
}, options));
},
system: function(message, options) {
this.show(jQuery.extend({}, {
type: 'system',
static: true,
message: '<p>'+this.formatMessage(message)+'</p>'
}, options));
},
formatMessage: function(message) {
if(Array.isArray(message)) {
return message.join('<br />');
} else {
return message;
}
// add data-id to the element
if (this.options.id) {
this.element.attr(
'data-id',
this.options.id
);
}
// remove id from clone
this.element.removeAttr('id');
// insert notice after its parent
var positionAfter;
if (typeof this.options.positionAfter === 'object') {
positionAfter = this.options.positionAfter;
} else if (typeof this.options.positionAfter === 'string') {
positionAfter = jQuery(this.options.positionAfter);
} else {
positionAfter = jQuery('#mailpoet_notice_'+this.options.type);
}
positionAfter.after(this.element);
// setup onClose callback
var onClose = null;
if (this.options.onClose !== null) {
onClose = this.options.onClose;
}
// listen to remove event
jQuery(this.element).on('close', function() {
jQuery(this).fadeOut(200, function() {
// on close callback
if (onClose !== null) {
onClose();
}
// remove notice
jQuery(this).remove();
});
}.bind(this.element));
// listen to message event
jQuery(this.element).on('setMessage', function(e, message) {
MailPoet.Notice.setMessage(message);
}.bind(this.element));
return this;
},
updateNotice: function() {
// update notice's message
jQuery('[data-id="'+this.options.id+'"').first().trigger(
'setMessage', this.options.message
);
},
isHTML: function(str) {
var a = document.createElement('div');
a.innerHTML = str;
for (var c = a.childNodes, i = c.length; i--;) {
if (c[i].nodeType == 1) return true;
}
return false;
},
setMessage: function(message) {
message = this.formatMessage(message);
// if it's not an html message
// let's sugar coat the message with a fancy <p>
if (this.isHTML(message) === false) {
message = '<p>'+message+'</p>';
}
// set message
return this.element.html(message);
},
formatMessage: function(message) {
if (Array.isArray(message)) {
return message.join('<br />');
} else {
return message;
}
},
show: function(options) {
// initialize
this.init(options);
if (
this.options.id !== null
&&
jQuery('[data-id="'+this.options.id+'"').length > 0
) {
this.updateNotice();
} else {
this.createNotice();
}
this.showNotice();
},
showNotice: function() {
// set message
this.setMessage(this.options.message);
// position notice
this.element.insertAfter(jQuery('h2.title'));
// set class name
switch (this.options.type) {
case 'success':
this.element.addClass('updated');
break;
case 'system':
this.element.addClass('update-nag');
break;
case 'error':
this.element.addClass('error');
break;
}
// make the notice appear
this.element.fadeIn(200);
// if scroll option is enabled, scroll to the notice
if (this.options.scroll === true) {
this.element.get(0).scrollIntoView(false);
}
// if the notice is not static, it has to disappear after a timeout
if (this.options.static === false) {
this.element.delay(this.options.timeout).trigger('close');
} else if (this.options.hideClose === false) {
this.element.append('<a href="javascript:;" class="mailpoet_notice_close"><span class="dashicons dashicons-dismiss"></span></a>');
this.element.find('.mailpoet_notice_close').on('click', function() {
jQuery(this).trigger('close');
});
}
// call onOpen callback
if (this.options.onOpen !== null) {
this.options.onOpen(this.element);
}
},
hide: function(all) {
if (all !== undefined && all === true) {
// all notices
jQuery('.mailpoet_notice:not([id])').trigger('close');
} else if (all !== undefined && jQuery.isArray(all)) {
// array of ids
for (var id in all) {
jQuery('[data-id="' + all[id] + '"]').trigger('close');
}
} if (all !== undefined) {
// single id
jQuery('[data-id="' + all + '"]').trigger('close');
} else {
jQuery('.mailpoet_notice.updated:not([id]), .mailpoet_notice.error:not([id])')
.trigger('close');
}
},
error: function(message, options) {
this.show(jQuery.extend({}, {
type: 'error',
message: message
}, options));
},
success: function(message, options) {
this.show(jQuery.extend({}, {
type: 'success',
message: message
}, options));
},
system: function(message, options) {
this.show(jQuery.extend({}, {
type: 'system',
static: true,
message: message
}, options));
}
};
});

View File

@ -2,6 +2,7 @@
namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
use MailPoet\Mailer\MailerLog;
if(!defined('ABSPATH')) exit;
@ -20,12 +21,19 @@ class Mailer extends APIEndpoint {
));
}
if($result === false) {
return $this->errorResponse(array(
APIError::BAD_REQUEST => __("The email could not be sent. Please check your settings.", 'mailpoet')
));
if($result['response'] === false) {
$error = sprintf(
__('The email could not be sent: %s', 'mailpoet'),
$result['error']
);
return $this->errorResponse(array(APIError::BAD_REQUEST => $error));
} else {
return $this->successResponse(null);
}
}
function resumeSending() {
MailerLog::resumeSending();
return $this->successResponse(null);
}
}

View File

@ -4,6 +4,7 @@ namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError;
use MailPoet\Listing;
use MailPoet\Models\Setting;
use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterTemplate;
use MailPoet\Models\NewsletterSegment;
@ -275,10 +276,19 @@ class Newsletters extends APIEndpoint {
$sender = false,
$reply_to = false
);
$mailer->send($newsletter, $data['subscriber']);
return $this->successResponse(
Newsletter::findOne($id)->asArray()
);
$result = $mailer->send($newsletter, $data['subscriber']);
if($result['response'] === false) {
$error = sprintf(
__('The email could not be sent: %s', 'mailpoet'),
$result['error']
);
return $this->errorResponse(array(APIError::BAD_REQUEST => $error));
} else {
return $this->successResponse(
Newsletter::findOne($id)->asArray()
);
}
} catch(\Exception $e) {
return $this->errorResponse(array(
$e->getCode() => $e->getMessage()
@ -337,7 +347,9 @@ class Newsletters extends APIEndpoint {
return $this->successResponse($data, array(
'count' => $listing_data['count'],
'filters' => $listing_data['filters'],
'groups' => $listing_data['groups']
'groups' => $listing_data['groups'],
'mta_log' => Setting::getValue('mta_log'),
'mta_method' => Setting::getValue('mta.method')
));
}

View File

@ -115,7 +115,6 @@ class Migrator {
'count_total mediumint(9) NOT NULL DEFAULT 0,',
'count_processed mediumint(9) NOT NULL DEFAULT 0,',
'count_to_process mediumint(9) NOT NULL DEFAULT 0,',
'count_failed mediumint(9) NOT NULL DEFAULT 0,',
'scheduled_at TIMESTAMP NULL,',
'processed_at TIMESTAMP NULL,',
'created_at TIMESTAMP NULL,',

View File

@ -2,6 +2,7 @@
namespace MailPoet\Config;
use MailPoet\Cron\CronTrigger;
use MailPoet\Mailer\MailerLog;
use \MailPoet\Models\Segment;
use \MailPoet\Segments\WP;
use \MailPoet\Models\Setting;
@ -85,26 +86,26 @@ class Populator {
private function createDefaultSettings() {
$current_user = wp_get_current_user();
// set cron trigger option to default method
if(!Setting::getValue(CronTrigger::SETTING_NAME)) {
// disable task scheduler (cron) be default
Setting::setValue(CronTrigger::SETTING_NAME, array(
'method' => CronTrigger::DEFAULT_METHOD
));
}
// default sender info based on current user
// set default sender info based on current user
$sender = array(
'name' => $current_user->display_name,
'address' => $current_user->user_email
);
// set default from name & address
if(!Setting::getValue('sender')) {
// default from name & address
Setting::setValue('sender', $sender);
}
// enable signup confirmation by default
if(!Setting::getValue('signup_confirmation')) {
// enable signup confirmation by default
Setting::setValue('signup_confirmation', array(
'enabled' => true,
'from' => array(
@ -115,9 +116,13 @@ class Populator {
));
}
// set installation date
if(!Setting::getValue('installed_at')) {
Setting::setValue('installed_at', date("Y-m-d H:i:s"));
}
// reset mailer log
MailerLog::resetMailerLog();
}
private function createDefaultSegments() {

View File

@ -9,9 +9,9 @@ use MailPoet\Util\Security;
if(!defined('ABSPATH')) exit;
class CronHelper {
const DAEMON_EXECUTION_LIMIT = 20;
const DAEMON_EXECUTION_TIMEOUT = 35;
const DAEMON_REQUEST_TIMEOUT = 2;
const DAEMON_EXECUTION_LIMIT = 20; // seconds
const DAEMON_EXECUTION_TIMEOUT = 35; // seconds
const DAEMON_REQUEST_TIMEOUT = 2; // seconds
const DAEMON_SETTING = 'cron_daemon';
static function createDaemon($token) {

View File

@ -10,7 +10,7 @@ class Daemon {
public $daemon;
public $request_data;
public $timer;
const REQUEST_TIMEOUT = 5;
const REQUEST_TIMEOUT = 5; // seconds
function __construct($request_data = false) {
$this->request_data = $request_data;

View File

@ -21,8 +21,10 @@ class SendingQueue {
$this->mailer_task = ($mailer_task) ? $mailer_task : new MailerTask();
$this->newsletter_task = ($newsletter_task) ? $newsletter_task : new NewsletterTask();
$this->timer = ($timer) ? $timer : microtime(true);
// abort if execution or sending limit are reached
// abort if execution limit is reached
CronHelper::enforceExecutionLimit($this->timer);
// abort if mailing is paused or sending limit has been reached
MailerLog::enforceExecutionRequirements();
}
function process() {
@ -70,8 +72,8 @@ class SendingQueue {
if($queue->status === SendingQueueModel::STATUS_COMPLETED) {
$this->newsletter_task->markNewsletterAsSent($newsletter);
}
// abort if sending limit is reached
MailerLog::enforceSendingLimit();
// abort if sending limit has been reached
MailerLog::enforceExecutionRequirements();
}
}
}
@ -118,8 +120,8 @@ class SendingQueue {
$prepared_subscribers_ids = array();
$statistics = array();
}
// abort if sending limit is reached
MailerLog::enforceSendingLimit();
// abort if sending limit has been reached
MailerLog::enforceExecutionRequirements();
}
if($processing_method === 'bulk') {
$queue = $this->sendNewsletters(
@ -142,20 +144,22 @@ class SendingQueue {
$prepared_newsletters,
$prepared_subscribers
);
if(!$send_result) {
// update failed/to process list
$queue->updateFailedSubscribers($prepared_subscribers_ids);
} else {
// update processed/to process list
$queue->updateProcessedSubscribers($prepared_subscribers_ids);
// log statistics
StatisticsNewslettersModel::createMultiple($statistics);
// update the sent count
$this->mailer_task->updateSentCount();
// enforce sending limit if there are still subscribers left to process
if($queue->count_to_process) {
MailerLog::enforceSendingLimit();
}
// log error message and schedule retry/pause sending
if($send_result['response'] === false) {
MailerLog::processSendingError(
$send_result['operation'],
$send_result['error_message']
);
}
// update processed/to process list
$queue->updateProcessedSubscribers($prepared_subscribers_ids);
// log statistics
StatisticsNewslettersModel::createMultiple($statistics);
// update the sent count
$this->mailer_task->updateSentCount();
// abort if sending limit has been reached
if($queue->count_to_process) {
MailerLog::enforceExecutionRequirements();
}
return $queue;
}

View File

@ -14,8 +14,6 @@ class Mailer {
const MAILER_CONFIG_SETTING_NAME = 'mta';
const SENDING_LIMIT_INTERVAL_MULTIPLIER = 60;
const METHOD_MAILPOET = 'MailPoet';
const METHOD_MAILGUN = 'MailGun';
const METHOD_ELASTICEMAIL = 'ElasticEmail';
const METHOD_AMAZONSES = 'AmazonSES';
const METHOD_SENDGRID = 'SendGrid';
const METHOD_PHPMAIL = 'PHPMail';
@ -44,21 +42,6 @@ class Mailer {
$this->reply_to
);
break;
case self::METHOD_ELASTICEMAIL:
$mailer_instance = new $this->mailer_config['class'](
$this->mailer_config['api_key'],
$this->sender,
$this->reply_to
);
break;
case self::METHOD_MAILGUN:
$mailer_instance = new $this->mailer_config['class'](
$this->mailer_config['domain'],
$this->mailer_config['api_key'],
$this->sender,
$this->reply_to
);
break;
case self::METHOD_MAILPOET:
$mailer_instance = new $this->mailer_config['class'](
$this->mailer_config['mailpoet_api_key'],
@ -168,7 +151,29 @@ class Mailer {
function encodeAddressNamePart($name) {
if(mb_detect_encoding($name) === 'ASCII') return $name;
// bse64_encode non-ASCII string as per RFC 2047 (https://www.ietf.org/rfc/rfc2047.txt)
// encode non-ASCII string as per RFC 2047 (https://www.ietf.org/rfc/rfc2047.txt)
return sprintf('=?utf-8?B?%s?=', base64_encode($name));
}
static function formatMailerConnectionErrorResult($error_message) {
return array(
'response' => false,
'operation' => 'connect',
'error_message' => $error_message
);
}
static function formatMailerSendErrorResult($error_message) {
return array(
'response' => false,
'operation' => 'send',
'error_message' => $error_message
);
}
static function formatMailerSendSuccessResult() {
return array(
'response' => true
);
}
}

View File

@ -7,8 +7,12 @@ if(!defined('ABSPATH')) exit;
class MailerLog {
const SETTING_NAME = 'mta_log';
const STATUS_PAUSED = 'paused';
const RETRY_ATTEMPTS_LIMIT = 3;
const RETRY_INTERVAL = 120; // seconds
static function getMailerLog() {
static function getMailerLog($mailer_log = false) {
if($mailer_log) return $mailer_log;
$mailer_log = Setting::getValue(self::SETTING_NAME);
if(!$mailer_log) {
$mailer_log = self::createMailerLog();
@ -18,8 +22,12 @@ class MailerLog {
static function createMailerLog() {
$mailer_log = array(
'sent' => 0,
'started' => time()
'sent' => null,
'started' => time(),
'status' => null,
'retry_attempt' => null,
'retry_at' => null,
'error' => null
);
Setting::setValue(self::SETTING_NAME, $mailer_log);
return $mailer_log;
@ -34,15 +42,73 @@ class MailerLog {
return $mailer_log;
}
static function enforceExecutionRequirements($mailer_log = false) {
$mailer_log = self::getMailerLog($mailer_log);
if($mailer_log['retry_attempt'] === self::RETRY_ATTEMPTS_LIMIT) {
$mailer_log = self::pauseSending($mailer_log);
}
if($mailer_log['status'] === self::STATUS_PAUSED) {
throw new \Exception(__('Sending has been paused.', 'mailpoet'));
}
if(!is_null($mailer_log['retry_at'])) {
if(time() <= $mailer_log['retry_at']) {
throw new \Exception(__('Sending is waiting to be retried.', 'mailpoet'));
} else {
$mailer_log['retry_at'] = null;
self::updateMailerLog($mailer_log);
}
}
// ensure that sending frequency has not been reached
if(self::isSendingLimitReached($mailer_log)) {
throw new \Exception(__('Sending frequency limit has been reached.', 'mailpoet'));
}
}
static function pauseSending($mailer_log) {
$mailer_log['status'] = self::STATUS_PAUSED;
$mailer_log['retry_attempt'] = null;
$mailer_log['retry_at'] = null;
return self::updateMailerLog($mailer_log);
}
static function resumeSending() {
return self::resetMailerLog();
}
static function processSendingError($operation, $error_message) {
$mailer_log = self::getMailerLog();
(int)$mailer_log['retry_attempt']++;
$mailer_log['retry_at'] = time() + self::RETRY_INTERVAL;
$mailer_log['error'] = array(
'operation' => $operation,
'error_message' => $error_message
);
self::updateMailerLog($mailer_log);
return self::enforceExecutionRequirements();
}
static function incrementSentCount() {
$mailer_log = self::getMailerLog();
// clear previous retry count, errors, etc.
if($mailer_log['error']) {
$mailer_log = self::clearSendingErrorLog($mailer_log);
}
(int)$mailer_log['sent']++;
return self::updateMailerLog($mailer_log);
}
static function isSendingLimitReached() {
static function clearSendingErrorLog($mailer_log) {
$mailer_log['retry_attempt'] = null;
$mailer_log['retry_at'] = null;
$mailer_log['error'] = null;
return self::updateMailerLog($mailer_log);
}
static function isSendingLimitReached($mailer_log = false) {
$mailer_config = Mailer::getMailerConfig();
$mailer_log = self::getMailerLog();
// do not enforce sending limit for MailPoet's sending method
if($mailer_config['method'] === Mailer::METHOD_MAILPOET) return false;
$mailer_log = self::getMailerLog($mailer_log);
$elapsed_time = time() - (int)$mailer_log['started'];
if($mailer_log['sent'] === $mailer_config['frequency_limit']) {
if($elapsed_time <= $mailer_config['frequency_interval']) return true;
@ -51,10 +117,4 @@ class MailerLog {
}
return false;
}
static function enforceSendingLimit() {
if(self::isSendingLimitReached()) {
throw new \Exception(__('Sending frequency limit has been reached.', 'mailpoet'));
}
}
}

View File

@ -1,6 +1,8 @@
<?php
namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit;
class AmazonSES {
@ -48,10 +50,17 @@ class AmazonSES {
$this->url,
$this->request($newsletter, $subscriber)
);
return (
!is_wp_error($result) === true &&
wp_remote_retrieve_response_code($result) === 200
);
if(is_wp_error($result)) {
return Mailer::formatMailerConnectionErrorResult($result->get_error_message());
}
if(wp_remote_retrieve_response_code($result) !== 200) {
$response = simplexml_load_string(wp_remote_retrieve_body($result));
$response = ($response) ?
$response->Error->Message->__toString() :
sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_AMAZONSES);
return Mailer::formatMailerSendErrorResult($response);
}
return Mailer::formatMailerSendSuccessResult();
}
function getBody($newsletter, $subscriber) {

View File

@ -1,56 +0,0 @@
<?php
namespace MailPoet\Mailer\Methods;
if(!defined('ABSPATH')) exit;
class ElasticEmail {
public $url = 'https://api.elasticemail.com/mailer/send';
public $api_key;
public $sender;
public $reply_to;
function __construct($api_key, $sender, $reply_to) {
$this->api_key = $api_key;
$this->sender = $sender;
$this->reply_to = $reply_to;
}
function send($newsletter, $subscriber) {
$result = wp_remote_post(
$this->url,
$this->request($newsletter, $subscriber));
return (
!is_wp_error($result) === true &&
!preg_match('/\w{8}-\w{4}-\w{4}-\w{4}-\w{12}/', $result['body']) === false
);
}
function getBody($newsletter, $subscriber) {
$body = array(
'api_key' => $this->api_key,
'to' => $subscriber,
'from' => $this->sender['from_email'],
'from_name' => $this->sender['from_name'],
'reply_to' => $this->reply_to['reply_to_email'],
'reply_to_name' => $this->reply_to['reply_to_name'],
'subject' => $newsletter['subject']
);
if(!empty($newsletter['body']['html'])) {
$body['body_html'] = $newsletter['body']['html'];
}
if(!empty($newsletter['body']['text'])) {
$body['body_text'] = $newsletter['body']['text'];
}
return $body;
}
function request($newsletter, $subscriber) {
$body = $this->getBody($newsletter, $subscriber);
return array(
'timeout' => 10,
'httpversion' => '1.0',
'method' => 'POST',
'body' => urldecode(http_build_query($body))
);
}
}

View File

@ -1,63 +0,0 @@
<?php
namespace MailPoet\Mailer\Methods;
if(!defined('ABSPATH')) exit;
class MailGun {
public $url;
public $api_key;
public $sender;
public $reply_to;
function __construct($domain, $api_key, $sender, $reply_to) {
$this->url = sprintf('https://api.mailgun.net/v3/%s/messages', $domain);
$this->api_key = $api_key;
$this->sender = $sender;
$this->reply_to = $reply_to;
}
function send($newsletter, $subscriber) {
$result = wp_remote_post(
$this->url,
$this->request($newsletter, $subscriber)
);
return (
!is_wp_error($result) === true &&
wp_remote_retrieve_response_code($result) === 200
);
}
function getBody($newsletter, $subscriber) {
$body = array(
'to' => $subscriber,
'from' => $this->sender['from_name_email'],
'h:Reply-To' => $this->reply_to['reply_to_name_email'],
'subject' => $newsletter['subject']
);
if(!empty($newsletter['body']['html'])) {
$body['html'] = $newsletter['body']['html'];
}
if(!empty($newsletter['body']['text'])) {
$body['text'] = $newsletter['body']['text'];
}
return $body;
}
function auth() {
return 'Basic ' . base64_encode('api:' . $this->api_key);
}
function request($newsletter, $subscriber) {
$body = $this->getBody($newsletter, $subscriber);
return array(
'timeout' => 10,
'httpversion' => '1.0',
'method' => 'POST',
'headers' => array(
'Content-Type' => 'application/x-www-form-urlencoded',
'Authorization' => $this->auth()
),
'body' => urldecode(http_build_query($body))
);
}
}

View File

@ -1,6 +1,8 @@
<?php
namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit;
class MailPoet {
@ -21,10 +23,16 @@ class MailPoet {
$this->url,
$this->request($message_body)
);
return (
!is_wp_error($result) === true &&
wp_remote_retrieve_response_code($result) === 201
);
if(is_wp_error($result)) {
return Mailer::formatMailerConnectionErrorResult($result->get_error_message());
}
if(wp_remote_retrieve_response_code($result) !== 201) {
$response = (wp_remote_retrieve_body($result)) ?
wp_remote_retrieve_body($result) :
wp_remote_retrieve_response_message($result);
return Mailer::formatMailerSendErrorResult($response);
}
return Mailer::formatMailerSendSuccessResult();
}
function processSubscriber($subscriber) {
@ -41,7 +49,7 @@ class MailPoet {
}
function getBody($newsletter, $subscriber) {
$composeBody = function ($newsletter, $subscriber) {
$composeBody = function($newsletter, $subscriber) {
$body = array(
'to' => (array(
'address' => $subscriber['email'],

View File

@ -1,6 +1,8 @@
<?php
namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit;
class PHPMail {
@ -19,9 +21,13 @@ class PHPMail {
$message = $this->createMessage($newsletter, $subscriber);
$result = $this->mailer->send($message);
} catch(\Exception $e) {
$result = false;
return Mailer::formatMailerSendErrorResult($e->getMessage());
}
return ($result === 1);
return ($result === 1) ?
Mailer::formatMailerSendSuccessResult() :
Mailer::formatMailerSendErrorResult(
sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_PHPMAIL)
);
}
function buildMailer() {

View File

@ -1,6 +1,8 @@
<?php
namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit;
class SMTP {
@ -33,9 +35,13 @@ class SMTP {
$message = $this->createMessage($newsletter, $subscriber);
$result = $this->mailer->send($message);
} catch(\Exception $e) {
$result = false;
return Mailer::formatMailerSendErrorResult($e->getMessage());
}
return ($result === 1);
return ($result === 1) ?
Mailer::formatMailerSendSuccessResult() :
Mailer::formatMailerSendErrorResult(
sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_SMTP)
);
}
function buildMailer() {

View File

@ -1,6 +1,8 @@
<?php
namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit;
class SendGrid {
@ -20,13 +22,17 @@ class SendGrid {
$this->url,
$this->request($newsletter, $subscriber)
);
$result_body = json_decode($result['body'], true);
return (
!is_wp_error($result) === true &&
!preg_match('!invalid!', $result['body']) === true &&
!isset($result_body['errors']) === true &&
wp_remote_retrieve_response_code($result) === 200
);
if(is_wp_error($result)) {
return Mailer::formatMailerConnectionErrorResult($result->get_error_message());
}
if(wp_remote_retrieve_response_code($result) !== 200) {
$response = json_decode($result['body'], true);
$response = (!empty($response['errors'])) ?
$response['errors'] :
sprintf(__('%s has returned an unknown error.', 'mailpoet'), Mailer::METHOD_SENDGRID);
return Mailer::formatMailerSendErrorResult($response);
}
return Mailer::formatMailerSendSuccessResult();
}
function getBody($newsletter, $subscriber) {

View File

@ -67,9 +67,6 @@ class SendingQueue extends Model {
if(empty($subscribers['processed'])) {
$subscribers['processed'] = array();
}
if(empty($subscribers['failed'])) {
$subscribers['failed'] = array();
}
return $subscribers;
}
@ -104,22 +101,6 @@ class SendingQueue extends Model {
$this->updateCount();
}
function updateFailedSubscribers($failed_subscribers) {
$subscribers = $this->getSubscribers();
$subscribers['failed'] = array_merge(
$subscribers['failed'],
$failed_subscribers
);
$subscribers['to_process'] = array_values(
array_diff(
$subscribers['to_process'],
$failed_subscribers
)
);
$this->subscribers = $subscribers;
$this->updateCount();
}
function updateProcessedSubscribers($processed_subscribers) {
$subscribers = $this->getSubscribers();
$subscribers['processed'] = array_merge(
@ -138,10 +119,8 @@ class SendingQueue extends Model {
function updateCount() {
$this->subscribers = $this->getSubscribers();
$this->count_processed =
count($this->subscribers['processed']) + count($this->subscribers['failed']);
$this->count_processed = count($this->subscribers['processed']);
$this->count_to_process = count($this->subscribers['to_process']);
$this->count_failed = count($this->subscribers['failed']);
$this->count_total = $this->count_processed + $this->count_to_process;
if(!$this->count_to_process) {
$this->processed_at = current_time('mysql');

View File

@ -0,0 +1,21 @@
<?php
use MailPoet\API\Endpoints\Mailer;
use MailPoet\API\Response as APIResponse;
use MailPoet\Mailer\MailerLog;
class MailerEndpointTest extends MailPoetTest {
function testItResumesSending() {
// create mailer log with a "paused" status
$mailer_log = array('status' => MailerLog::STATUS_PAUSED);
MailerLog::updateMailerLog($mailer_log);
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['status'])->equals(MailerLog::STATUS_PAUSED);
// resumeSending() method should clear the mailer log's status
$mailer_endpoint = new Mailer();
$response = $mailer_endpoint->resumeSending();
expect($response->status)->equals(APIResponse::STATUS_OK);
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['status'])->null();
}
}

View File

@ -37,8 +37,7 @@ class SendingQueueTest extends MailPoetTest {
$this->queue->subscribers = serialize(
array(
'to_process' => array($this->subscriber->id),
'processed' => array(),
'failed' => array()
'processed' => array()
)
);
$this->queue->count_total = 1;
@ -118,18 +117,16 @@ class SendingQueueTest extends MailPoetTest {
$updated_queue = SendingQueue::findOne($this->queue->id);
expect($updated_queue->status)->equals(SendingQueue::STATUS_COMPLETED);
// queue subscriber processed/failed/to process count is updated
// queue subscriber processed/to process count is updated
$updated_queue->subscribers = $updated_queue->getSubscribers();
expect($updated_queue->subscribers)->equals(
array(
'to_process' => array(),
'failed' => array(),
'processed' => array($this->subscriber->id)
)
);
expect($updated_queue->count_total)->equals(1);
expect($updated_queue->count_processed)->equals(1);
expect($updated_queue->count_failed)->equals(0);
expect($updated_queue->count_to_process)->equals(0);
// statistics entry should be created
@ -166,18 +163,16 @@ class SendingQueueTest extends MailPoetTest {
$updated_queue = SendingQueue::findOne($this->queue->id);
expect($updated_queue->status)->equals(SendingQueue::STATUS_COMPLETED);
// queue subscriber processed/failed/to process count is updated
// queue subscriber processed/to process count is updated
$updated_queue->subscribers = $updated_queue->getSubscribers();
expect($updated_queue->subscribers)->equals(
array(
'to_process' => array(),
'failed' => array(),
'processed' => array($this->subscriber->id)
)
);
expect($updated_queue->count_total)->equals(1);
expect($updated_queue->count_processed)->equals(1);
expect($updated_queue->count_failed)->equals(0);
expect($updated_queue->count_to_process)->equals(0);
// statistics entry should be created
@ -196,8 +191,7 @@ class SendingQueueTest extends MailPoetTest {
$this->subscriber->id(),
123
),
'processed' => array(),
'failed' => array()
'processed' => array()
)
);
$queue->count_total = 2;
@ -210,18 +204,16 @@ class SendingQueueTest extends MailPoetTest {
$sending_queue_worker->process();
$updated_queue = SendingQueue::findOne($queue->id);
// queue subscriber processed/failed/to process count is updated
// queue subscriber processed/to process count is updated
$updated_queue->subscribers = $updated_queue->getSubscribers();
expect($updated_queue->subscribers)->equals(
array(
'to_process' => array(),
'failed' => array(),
'processed' => array($this->subscriber->id)
)
);
expect($updated_queue->count_total)->equals(1);
expect($updated_queue->count_processed)->equals(1);
expect($updated_queue->count_failed)->equals(0);
expect($updated_queue->count_to_process)->equals(0);
// statistics entry should be created only for 1 subscriber
@ -237,8 +229,7 @@ class SendingQueueTest extends MailPoetTest {
123,
456
),
'processed' => array(),
'failed' => array()
'processed' => array()
)
);
$queue->count_total = 2;
@ -251,54 +242,19 @@ class SendingQueueTest extends MailPoetTest {
$sending_queue_worker->process();
$updated_queue = SendingQueue::findOne($queue->id);
// queue subscriber processed/failed/to process count is updated
// queue subscriber processed/to process count is updated
$updated_queue->subscribers = $updated_queue->getSubscribers();
expect($updated_queue->subscribers)->equals(
array(
'to_process' => array(),
'failed' => array(),
'processed' => array()
)
);
expect($updated_queue->count_total)->equals(0);
expect($updated_queue->count_processed)->equals(0);
expect($updated_queue->count_failed)->equals(0);
expect($updated_queue->count_to_process)->equals(0);
}
function testItUpdatesFailedListWhenSendingFailed() {
$sending_queue_worker = new SendingQueueWorker(
$timer = false,
Stub::make(
new MailerTask(),
array('send' => Stub::exactly(1, function($newsletter, $subscriber) { return false; }))
)
);
$sending_queue_worker->process();
// queue subscriber processed/failed/to process count is updated
$updated_queue = SendingQueue::findOne($this->queue->id);
$updated_queue->subscribers = $updated_queue->getSubscribers();
expect($updated_queue->subscribers)->equals(
array(
'to_process' => array(),
'failed' => array($this->subscriber->id),
'processed' => array()
)
);
expect($updated_queue->count_total)->equals(1);
expect($updated_queue->count_processed)->equals(1);
expect($updated_queue->count_failed)->equals(1);
expect($updated_queue->count_to_process)->equals(0);
// statistics entry should not be created
$statistics = StatisticsNewsletters::where('newsletter_id', $this->newsletter->id)
->where('subscriber_id', $this->subscriber->id)
->where('queue_id', $this->queue->id)
->findOne();
expect($statistics)->false();
}
function testItDoesNotSendToTrashedSubscribers() {
$sending_queue_worker = $this->sending_queue_worker;
$sending_queue_worker->mailer_task = Stub::make(

View File

@ -51,8 +51,7 @@ class MailerTaskTest extends MailPoetTest {
function testItGetsMailerLog() {
$mailer_log = $this->mailer_task->getMailerLog();
expect(isset($mailer_log['sent']))->true();
expect(isset($mailer_log['started']))->true();
expect(is_array($mailer_log))->true();
}
function testItUpdatesMailerLogSentCount() {

View File

@ -51,7 +51,8 @@ class MailerLogTest extends MailPoetTest {
function testItIncrementsSentCount() {
$mailer_log = array(
'sent' => 1,
'started' => time()
'started' => time(),
'error' => null
);
Setting::setValue(MailerLog::SETTING_NAME, $mailer_log);
MailerLog::incrementSentCount();
@ -107,29 +108,126 @@ class MailerLogTest extends MailPoetTest {
expect($updated_mailer_log['sent'])->equals(0);
}
function testItCanEnforceSendingLimit() {
function testItResumesSending() {
// set status to "paused"
$mailer_log = array('status' => MailerLog::STATUS_PAUSED);
MailerLog::updateMailerLog($mailer_log);
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['status'])->equals(MailerLog::STATUS_PAUSED);
// status is reset when sending is resumed
MailerLog::resumeSending();
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['status'])->null();
}
function testItPausesSending() {
$mailer_log = array(
'status' => null,
'retry_attempt' => MailerLog::RETRY_ATTEMPTS_LIMIT,
'retry_at' => time() + 20
);
// status is set to PAUSED, retry attempt and retry at time are cleared
MailerLog::pauseSending($mailer_log);
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['status'])->equals(MailerLog::STATUS_PAUSED);
expect($mailer_log['retry_attempt'])->null();
expect($mailer_log['retry_at'])->null();
}
function itProcessesSendingError() {
// retry-related mailer values should be null
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['retry_attempt'])->null();
expect($mailer_log['retry_at'])->null();
expect($mailer_log['error'])->null();
// retry attempt should be incremented, error logged, retry attempt scheduled
MailerLog::processSendingError($operation = 'send', $error = 'email rejected');
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['retry_attempt'])->equals(1);
expect($mailer_log['retry_at'])->greaterThan(time());
expect($mailer_log['error'])->equals(
array(
'operation' => 'send',
'error_message' => 'email rejected'
)
);
}
function testItEnforcesSendingLimit() {
$mailer_config = array(
'frequency' => array(
'emails' => 2,
'interval' => 1
)
);
$mailer_log = array(
'sent' => 2,
'started' => time()
);
$mailer_log = MailerLog::createMailerLog();
$mailer_log['sent'] = 2;
$mailer_log['started'] = time();
Setting::setValue(MailerLog::SETTING_NAME, $mailer_log);
Setting::setValue(Mailer::MAILER_CONFIG_SETTING_NAME, $mailer_config);
// exception is thrown when sending limit is reached
try {
MailerLog::enforceSendingLimit();
MailerLog::enforceExecutionRequirements();
self::fail('Sending frequency exception was not thrown.');
} catch(\Exception $e) {
expect($e->getMessage())->equals('Sending frequency limit has been reached.');
}
}
function testItEnforcesRetryAtTime() {
$mailer_log = MailerLog::createMailerLog();
$mailer_log['retry_at'] = time() + 10;
// exception is thrown when current time is sooner than 120 seconds
try {
MailerLog::enforceExecutionRequirements($mailer_log);
self::fail('Sending waiting to be retried exception was not thrown.');
} catch(\Exception $e) {
expect($e->getMessage())->equals('Sending is waiting to be retried.');
}
}
function testItEnforcesRetryAttempts() {
$mailer_log = MailerLog::createMailerLog();
$mailer_log['retry_attempt'] = 2;
// allow less than 3 attempts
expect(MailerLog::enforceExecutionRequirements($mailer_log))->null();
// pase sending and throw exception when more than 3 attempts
$mailer_log['retry_attempt'] = MailerLog::RETRY_ATTEMPTS_LIMIT;
try {
MailerLog::enforceExecutionRequirements($mailer_log);
self::fail('Sending paused exception was not thrown.');
} catch(\Exception $e) {
expect($e->getMessage())->equals('Sending has been paused.');
}
$mailer_log = MailerLog::getMailerLog();
expect($mailer_log['status'])->equals(MailerLog::STATUS_PAUSED);
}
function testItClearsSendingErrorLog() {
$mailer_log = MailerLog::createMailerLog();
$mailer_log['retry_attempt'] = 1;
$mailer_log['retry_at'] = 1;
$mailer_log['error'] = 1;
$mailer_log['status'] = 'status';
$mailer_log = MailerLog::clearSendingErrorLog($mailer_log);
expect($mailer_log['retry_attempt'])->null();
expect($mailer_log['retry_at'])->null();
expect($mailer_log['error'])->null();
expect($mailer_log['status'])->equals('status');
}
function testItEnforcesPuasedStatus() {
$mailer_log = MailerLog::createMailerLog();
$mailer_log['status'] = MailerLog::STATUS_PAUSED;
try {
MailerLog::enforceExecutionRequirements($mailer_log);
self::fail('Sending paused exception was not thrown.');
} catch(\Exception $e) {
expect($e->getMessage())->equals('Sending has been paused.');
}
}
function _after() {
ORM::raw_execute('TRUNCATE ' . Setting::$_table);
}

View File

@ -11,15 +11,6 @@ class MailerTest extends MailPoetTest {
'access_key' => '1234567890',
'secret_key' => 'abcdefghijk',
),
array(
'method' => 'ElasticEmail',
'api_key' => 'abcdefghijk'
),
array(
'method' => 'MailGun',
'domain' => 'example.com',
'api_key' => 'abcdefghijk'
),
array(
'method' => 'MailPoet',
'mailpoet_api_key' => 'abcdefghijk'

View File

@ -1,78 +0,0 @@
<?php
use MailPoet\Mailer\Methods\ElasticEmail;
class ElasticEmailTest extends MailPoetTest {
function _before() {
$this->settings = array(
'method' => 'ElasticEmail',
'api_key' => getenv('WP_TEST_MAILER_ELASTICEMAIL_API') ?
getenv('WP_TEST_MAILER_ELASTICEMAIL_API') :
'1234567890'
);
$this->sender = array(
'from_name' => 'Sender',
'from_email' => 'staff@mailpoet.com',
'from_name_email' => 'Sender <staff@mailpoet.com>'
);
$this->reply_to = array(
'reply_to_name' => 'Reply To',
'reply_to_email' => 'reply-to@mailpoet.com',
'reply_to_name_email' => 'Reply To <reply-to@mailpoet.com>'
);
$this->mailer = new ElasticEmail(
$this->settings['api_key'],
$this->sender,
$this->reply_to
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(
'subject' => 'testing ElasticEmail',
'body' => array(
'html' => 'HTML body',
'text' => 'TEXT body'
)
);
}
function testItCanGenerateBody() {
$body = $this->mailer->getBody($this->newsletter, $this->subscriber);
expect($body['api_key'])->equals($this->settings['api_key']);
expect($body['from'])->equals($this->sender['from_email']);
expect($body['from_name'])->equals($this->sender['from_name']);
expect($body['reply_to'])->equals($this->reply_to['reply_to_email']);
expect($body['reply_to_name'])->equals($this->reply_to['reply_to_name']);
expect($body['to'])->contains($this->subscriber);
expect($body['subject'])->equals($this->newsletter['subject']);
expect($body['body_html'])->equals($this->newsletter['body']['html']);
expect($body['body_text'])->equals($this->newsletter['body']['text']);
}
function testItCanCreateRequest() {
$request = $this->mailer->request($this->newsletter, $this->subscriber);
$body = $this->mailer->getBody($this->newsletter, $this->subscriber);
expect($request['timeout'])->equals(10);
expect($request['httpversion'])->equals('1.0');
expect($request['method'])->equals('POST');
expect($request['body'])->equals(urldecode(http_build_query($body)));
}
function testItCannotSendWithoutProperApiKey() {
if(getenv('WP_TEST_MAILER_ENABLE_SENDING') !== 'true') return;
$this->mailer->api_key = 'someapi';
$result = $this->mailer->send(
$this->newsletter,
$this->subscriber
);
expect($result)->false();
}
function testItCanSend() {
if(getenv('WP_TEST_MAILER_ENABLE_SENDING') !== 'true') return;
$result = $this->mailer->send(
$this->newsletter,
$this->subscriber
);
expect($result)->true();
}
}

View File

@ -1,99 +0,0 @@
<?php
use MailPoet\Mailer\Methods\MailGun;
class MailGunTest extends MailPoetTest {
function _before() {
$this->settings = array(
'method' => 'MailGun',
'api_key' => getenv('WP_TEST_MAILER_MAILGUN_API') ?
getenv('WP_TEST_MAILER_MAILGUN_API') :
'1234567890',
'domain' => getenv('WP_TEST_MAILER_MAILGUN_DOMAIN') ?
getenv('WP_TEST_MAILER_MAILGUN_DOMAIN') :
'example.com'
);
$this->sender = array(
'from_name' => 'Sender',
'from_email' => 'staff@mailpoet.com',
'from_name_email' => 'Sender <staff@mailpoet.com>'
);
$this->reply_to = array(
'reply_to_name' => 'Reply To',
'reply_to_email' => 'reply-to@mailpoet.com',
'reply_to_name_email' => 'Reply To <reply-to@mailpoet.com>'
);
$this->mailer = new MailGun(
$this->settings['domain'],
$this->settings['api_key'],
$this->sender,
$this->reply_to
);
$this->subscriber = 'Recipient <mailpoet-phoenix-test@mailinator.com>';
$this->newsletter = array(
'subject' => 'testing MailGun',
'body' => array(
'html' => 'HTML body',
'text' => 'TEXT body'
)
);
}
function testItCanGenerateBody() {
$body = $this->mailer->getBody($this->newsletter, $this->subscriber);
expect($body['from'])->equals($this->sender['from_name_email']);
expect($body['h:Reply-To'])->equals($this->reply_to['reply_to_name_email']);
expect($body['to'])->equals($this->subscriber);
expect($body['subject'])->equals($this->newsletter['subject']);
expect($body['html'])->equals($this->newsletter['body']['html']);
expect($body['text'])->equals($this->newsletter['body']['text']);
}
function testItCanDoBasicAuth() {
expect($this->mailer->auth())
->equals('Basic ' . base64_encode('api:' . $this->settings['api_key']));
}
function testItCanCreateRequest() {
$request = $this->mailer->request($this->newsletter, $this->subscriber);
$body = $this->mailer->getBody($this->newsletter, $this->subscriber);
expect($request['timeout'])->equals(10);
expect($request['httpversion'])->equals('1.0');
expect($request['method'])->equals('POST');
expect($request['headers']['Content-Type'])
->equals('application/x-www-form-urlencoded');
expect($request['headers']['Authorization'])
->equals('Basic ' . base64_encode('api:' . $this->settings['api_key']));
expect($request['body'])->equals(urldecode(http_build_query($body)));
}
function testItCannotSendWithoutProperApiKey() {
if(getenv('WP_TEST_MAILER_ENABLE_SENDING') !== 'true') return;
$this->mailer->api_key = 'someapi';
$result = $this->mailer->send(
$this->newsletter,
$this->subscriber
);
expect($result)->false();
}
function testItCannotSendWithoutProperDomain() {
if(getenv('WP_TEST_MAILER_ENABLE_SENDING') !== 'true') return;
$this->mailer->url =
str_replace($this->settings['domain'], 'somedomain', $this->mailer->url);
$result = $this->mailer->send(
$this->newsletter,
$this->subscriber
);
expect($result)->false();
}
function testItCanSend() {
if(getenv('WP_TEST_MAILER_ENABLE_SENDING') !== 'true') return;
$result = $this->mailer->send(
$this->newsletter,
$this->subscriber
);
expect($result)->true();
}
}

View File

@ -85,6 +85,7 @@
'sentToXSubscribers': __('Sent to %$1d subscribers'),
'resume': __('Resume'),
'pause': __('Pause'),
'paused': __('Paused'),
'new': __('Add New'),
'templateFileMalformedError': __('This template file appears to be damaged. Please try another one.'),
@ -232,6 +233,12 @@
'backToPostNotifications': __('Back to Post notifications'),
'sentOn': __('Sent on'),
'noSubscribers': __('No subscribers!')
'noSubscribers': __('No subscribers!'),
'mailerSendErrorNotice': __("We've detected an issue with the %$1s sending method that prevents us from delivering the remaining emails: %$2s"),
'mailerConnectionErrorNotice': __("We've detected a connection issue that prevents us from delivering the remaining emails: %$1s"),
'mailerResumeSendingNotice': __('As a result, all sending has been paused. Please resolve the issue before continuing.'),
'mailerResumeSendingButton': __('Resume sending'),
'mailerSendingResumedNotice': __("Sending has been resumed.")
}) %>
<% endblock %>
<% endblock %>