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

View File

@@ -1,4 +1,5 @@
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom'
import MailPoet from 'mailpoet' import MailPoet from 'mailpoet'
import classNames from 'classnames' import classNames from 'classnames'
import jQuery from 'jquery' import jQuery from 'jquery'
@@ -42,11 +43,15 @@ const _QueueMixin = {
} }
}); });
}, },
renderQueueStatus: function(newsletter) { renderQueueStatus: function(newsletter, mailer_log) {
if (!newsletter.queue) { if (!newsletter.queue) {
return ( return (
<span>{MailPoet.I18n.t('notSentYet')}</span> <span>{MailPoet.I18n.t('notSentYet')}</span>
); );
} else if (mailer_log.status === 'paused') {
return (
<span>{MailPoet.I18n.t('paused')}</span>
)
} else { } else {
if (newsletter.queue.status === 'scheduled') { if (newsletter.queue.status === 'scheduled') {
return ( return (
@@ -72,14 +77,8 @@ const _QueueMixin = {
<span> <span>
{ {
MailPoet.I18n.t('newsletterQueueCompleted') MailPoet.I18n.t('newsletterQueueCompleted')
.replace( .replace("%$1d",newsletter.queue.count_processed)
"%$1d", .replace("%$2d", newsletter.queue.count_total)
newsletter.queue.count_processed - newsletter.queue.count_failed
)
.replace(
"%$2d",
newsletter.queue.count_total
)
} }
</span> </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 { _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 Listing from 'listing/listing.jsx'
import ListingTabs from 'newsletters/listings/tabs.jsx' import ListingTabs from 'newsletters/listings/tabs.jsx'
import { MailerMixin } from 'newsletters/listings/mixins.jsx'
import classNames from 'classnames' import classNames from 'classnames'
import jQuery from 'jquery' import jQuery from 'jquery'
import MailPoet from 'mailpoet' import MailPoet from 'mailpoet'
@@ -16,6 +18,8 @@ import {
nthWeekDayValues nthWeekDayValues
} from 'newsletters/scheduling/common.jsx' } from 'newsletters/scheduling/common.jsx'
const mailpoet_settings = window.mailpoet_settings || {};
const messages = { const messages = {
onTrash: (response) => { onTrash: (response) => {
const count = ~~response.meta.count; const count = ~~response.meta.count;
@@ -153,6 +157,7 @@ const newsletter_actions = [
]; ];
const NewsletterListNotification = React.createClass({ const NewsletterListNotification = React.createClass({
mixins: [ MailerMixin ],
updateStatus: function(e) { updateStatus: function(e) {
// make the event persist so that we can still override the selected value // make the event persist so that we can still override the selected value
// in the ajax callback // in the ajax callback
@@ -328,6 +333,7 @@ const NewsletterListNotification = React.createClass({
auto_refresh={ true } auto_refresh={ true }
sort_by="updated_at" sort_by="updated_at"
sort_order="desc" sort_order="desc"
afterGetItems={ this.checkMailerStatus }
/> />
</div> </div>
); );

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,14 +5,14 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
MailPoet Notice: MailPoet Notice:
description: Handles notices description: Handles notices
version: 0.2 version: 1.0
author: Jonathan Labreuille author: Jonathan Labreuille
company: Wysija company: Wysija
dependencies: jQuery dependencies: jQuery
Usage: Usage:
// success message (static: false) // success message (static: false)
MailPoet.Notice.success('Yatta!'); MailPoet.Notice.success('Yatta!');
// error message (static: false) // error message (static: false)
@@ -21,199 +21,206 @@ define('notice', ['mailpoet', 'jquery'], function(MailPoet, jQuery) {
// system message (static: true) // system message (static: true)
MailPoet.Notice.system('You need to updated ASAP!'); 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 = { MailPoet.Notice = {
version: 0.2, version: 1.0,
// default options // default options
defaults: { defaults: {
type: 'success', type: 'success',
message: '', message: '',
static: false, static: false,
hideClose: false, hideClose: false,
id: null, id: null,
positionAfter: false, positionAfter: false,
scroll: false, scroll: false,
timeout: 5000, timeout: 5000,
onOpen: null, onOpen: null,
onClose: null onClose: null
}, },
options: {}, options: {},
init: function(options) { init: function(options) {
// set options // set options
this.options = jQuery.extend({}, this.defaults, options); this.options = jQuery.extend({}, this.defaults, options);
// clone element return this;
this.element = jQuery('#mailpoet_notice_'+this.options.type).clone(); },
createNotice: function() {
// clone element
this.element = jQuery('#mailpoet_notice_'+this.options.type).clone();
// add data-id to the element // add data-id to the element
if (this.options.id) this.element.attr('data-id', 'notice_' + this.options.id); if (this.options.id) {
this.element.attr(
// remove id from clone 'data-id',
this.element.removeAttr('id'); this.options.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;
}
} }
// 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; namespace MailPoet\API\Endpoints;
use MailPoet\API\Endpoint as APIEndpoint; use MailPoet\API\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError; use MailPoet\API\Error as APIError;
use MailPoet\Mailer\MailerLog;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
@@ -20,12 +21,19 @@ class Mailer extends APIEndpoint {
)); ));
} }
if($result === false) { if($result['response'] === false) {
return $this->errorResponse(array( $error = sprintf(
APIError::BAD_REQUEST => __("The email could not be sent. Please check your settings.", 'mailpoet') __('The email could not be sent: %s', 'mailpoet'),
)); $result['error']
);
return $this->errorResponse(array(APIError::BAD_REQUEST => $error));
} else { } else {
return $this->successResponse(null); 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\Endpoint as APIEndpoint;
use MailPoet\API\Error as APIError; use MailPoet\API\Error as APIError;
use MailPoet\Listing; use MailPoet\Listing;
use MailPoet\Models\Setting;
use MailPoet\Models\Newsletter; use MailPoet\Models\Newsletter;
use MailPoet\Models\NewsletterTemplate; use MailPoet\Models\NewsletterTemplate;
use MailPoet\Models\NewsletterSegment; use MailPoet\Models\NewsletterSegment;
@@ -275,10 +276,19 @@ class Newsletters extends APIEndpoint {
$sender = false, $sender = false,
$reply_to = false $reply_to = false
); );
$mailer->send($newsletter, $data['subscriber']); $result = $mailer->send($newsletter, $data['subscriber']);
return $this->successResponse(
Newsletter::findOne($id)->asArray() 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) { } catch(\Exception $e) {
return $this->errorResponse(array( return $this->errorResponse(array(
$e->getCode() => $e->getMessage() $e->getCode() => $e->getMessage()
@@ -337,7 +347,9 @@ class Newsletters extends APIEndpoint {
return $this->successResponse($data, array( return $this->successResponse($data, array(
'count' => $listing_data['count'], 'count' => $listing_data['count'],
'filters' => $listing_data['filters'], '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_total mediumint(9) NOT NULL DEFAULT 0,',
'count_processed mediumint(9) NOT NULL DEFAULT 0,', 'count_processed mediumint(9) NOT NULL DEFAULT 0,',
'count_to_process 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,', 'scheduled_at TIMESTAMP NULL,',
'processed_at TIMESTAMP NULL,', 'processed_at TIMESTAMP NULL,',
'created_at TIMESTAMP NULL,', 'created_at TIMESTAMP NULL,',

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,8 +14,6 @@ class Mailer {
const MAILER_CONFIG_SETTING_NAME = 'mta'; const MAILER_CONFIG_SETTING_NAME = 'mta';
const SENDING_LIMIT_INTERVAL_MULTIPLIER = 60; const SENDING_LIMIT_INTERVAL_MULTIPLIER = 60;
const METHOD_MAILPOET = 'MailPoet'; const METHOD_MAILPOET = 'MailPoet';
const METHOD_MAILGUN = 'MailGun';
const METHOD_ELASTICEMAIL = 'ElasticEmail';
const METHOD_AMAZONSES = 'AmazonSES'; const METHOD_AMAZONSES = 'AmazonSES';
const METHOD_SENDGRID = 'SendGrid'; const METHOD_SENDGRID = 'SendGrid';
const METHOD_PHPMAIL = 'PHPMail'; const METHOD_PHPMAIL = 'PHPMail';
@@ -44,21 +42,6 @@ class Mailer {
$this->reply_to $this->reply_to
); );
break; 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: case self::METHOD_MAILPOET:
$mailer_instance = new $this->mailer_config['class']( $mailer_instance = new $this->mailer_config['class'](
$this->mailer_config['mailpoet_api_key'], $this->mailer_config['mailpoet_api_key'],
@@ -168,7 +151,29 @@ class Mailer {
function encodeAddressNamePart($name) { function encodeAddressNamePart($name) {
if(mb_detect_encoding($name) === 'ASCII') return $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)); 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 { class MailerLog {
const SETTING_NAME = 'mta_log'; 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); $mailer_log = Setting::getValue(self::SETTING_NAME);
if(!$mailer_log) { if(!$mailer_log) {
$mailer_log = self::createMailerLog(); $mailer_log = self::createMailerLog();
@@ -18,8 +22,12 @@ class MailerLog {
static function createMailerLog() { static function createMailerLog() {
$mailer_log = array( $mailer_log = array(
'sent' => 0, 'sent' => null,
'started' => time() 'started' => time(),
'status' => null,
'retry_attempt' => null,
'retry_at' => null,
'error' => null
); );
Setting::setValue(self::SETTING_NAME, $mailer_log); Setting::setValue(self::SETTING_NAME, $mailer_log);
return $mailer_log; return $mailer_log;
@@ -34,15 +42,73 @@ class MailerLog {
return $mailer_log; 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() { static function incrementSentCount() {
$mailer_log = self::getMailerLog(); $mailer_log = self::getMailerLog();
// clear previous retry count, errors, etc.
if($mailer_log['error']) {
$mailer_log = self::clearSendingErrorLog($mailer_log);
}
(int)$mailer_log['sent']++; (int)$mailer_log['sent']++;
return self::updateMailerLog($mailer_log); 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_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']; $elapsed_time = time() - (int)$mailer_log['started'];
if($mailer_log['sent'] === $mailer_config['frequency_limit']) { if($mailer_log['sent'] === $mailer_config['frequency_limit']) {
if($elapsed_time <= $mailer_config['frequency_interval']) return true; if($elapsed_time <= $mailer_config['frequency_interval']) return true;
@@ -51,10 +117,4 @@ class MailerLog {
} }
return false; 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 <?php
namespace MailPoet\Mailer\Methods; namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class AmazonSES { class AmazonSES {
@@ -48,10 +50,17 @@ class AmazonSES {
$this->url, $this->url,
$this->request($newsletter, $subscriber) $this->request($newsletter, $subscriber)
); );
return ( if(is_wp_error($result)) {
!is_wp_error($result) === true && return Mailer::formatMailerConnectionErrorResult($result->get_error_message());
wp_remote_retrieve_response_code($result) === 200 }
); 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) { 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 <?php
namespace MailPoet\Mailer\Methods; namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class MailPoet { class MailPoet {
@@ -21,10 +23,16 @@ class MailPoet {
$this->url, $this->url,
$this->request($message_body) $this->request($message_body)
); );
return ( if(is_wp_error($result)) {
!is_wp_error($result) === true && return Mailer::formatMailerConnectionErrorResult($result->get_error_message());
wp_remote_retrieve_response_code($result) === 201 }
); 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) { function processSubscriber($subscriber) {
@@ -41,7 +49,7 @@ class MailPoet {
} }
function getBody($newsletter, $subscriber) { function getBody($newsletter, $subscriber) {
$composeBody = function ($newsletter, $subscriber) { $composeBody = function($newsletter, $subscriber) {
$body = array( $body = array(
'to' => (array( 'to' => (array(
'address' => $subscriber['email'], 'address' => $subscriber['email'],

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace MailPoet\Mailer\Methods; namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class PHPMail { class PHPMail {
@@ -19,9 +21,13 @@ class PHPMail {
$message = $this->createMessage($newsletter, $subscriber); $message = $this->createMessage($newsletter, $subscriber);
$result = $this->mailer->send($message); $result = $this->mailer->send($message);
} catch(\Exception $e) { } 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() { function buildMailer() {

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace MailPoet\Mailer\Methods; namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class SMTP { class SMTP {
@@ -33,9 +35,13 @@ class SMTP {
$message = $this->createMessage($newsletter, $subscriber); $message = $this->createMessage($newsletter, $subscriber);
$result = $this->mailer->send($message); $result = $this->mailer->send($message);
} catch(\Exception $e) { } 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() { function buildMailer() {

View File

@@ -1,6 +1,8 @@
<?php <?php
namespace MailPoet\Mailer\Methods; namespace MailPoet\Mailer\Methods;
use MailPoet\Mailer\Mailer;
if(!defined('ABSPATH')) exit; if(!defined('ABSPATH')) exit;
class SendGrid { class SendGrid {
@@ -20,13 +22,17 @@ class SendGrid {
$this->url, $this->url,
$this->request($newsletter, $subscriber) $this->request($newsletter, $subscriber)
); );
$result_body = json_decode($result['body'], true); if(is_wp_error($result)) {
return ( return Mailer::formatMailerConnectionErrorResult($result->get_error_message());
!is_wp_error($result) === true && }
!preg_match('!invalid!', $result['body']) === true && if(wp_remote_retrieve_response_code($result) !== 200) {
!isset($result_body['errors']) === true && $response = json_decode($result['body'], true);
wp_remote_retrieve_response_code($result) === 200 $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) { function getBody($newsletter, $subscriber) {

View File

@@ -67,9 +67,6 @@ class SendingQueue extends Model {
if(empty($subscribers['processed'])) { if(empty($subscribers['processed'])) {
$subscribers['processed'] = array(); $subscribers['processed'] = array();
} }
if(empty($subscribers['failed'])) {
$subscribers['failed'] = array();
}
return $subscribers; return $subscribers;
} }
@@ -104,22 +101,6 @@ class SendingQueue extends Model {
$this->updateCount(); $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) { function updateProcessedSubscribers($processed_subscribers) {
$subscribers = $this->getSubscribers(); $subscribers = $this->getSubscribers();
$subscribers['processed'] = array_merge( $subscribers['processed'] = array_merge(
@@ -138,10 +119,8 @@ class SendingQueue extends Model {
function updateCount() { function updateCount() {
$this->subscribers = $this->getSubscribers(); $this->subscribers = $this->getSubscribers();
$this->count_processed = $this->count_processed = count($this->subscribers['processed']);
count($this->subscribers['processed']) + count($this->subscribers['failed']);
$this->count_to_process = count($this->subscribers['to_process']); $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; $this->count_total = $this->count_processed + $this->count_to_process;
if(!$this->count_to_process) { if(!$this->count_to_process) {
$this->processed_at = current_time('mysql'); $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( $this->queue->subscribers = serialize(
array( array(
'to_process' => array($this->subscriber->id), 'to_process' => array($this->subscriber->id),
'processed' => array(), 'processed' => array()
'failed' => array()
) )
); );
$this->queue->count_total = 1; $this->queue->count_total = 1;
@@ -118,18 +117,16 @@ class SendingQueueTest extends MailPoetTest {
$updated_queue = SendingQueue::findOne($this->queue->id); $updated_queue = SendingQueue::findOne($this->queue->id);
expect($updated_queue->status)->equals(SendingQueue::STATUS_COMPLETED); 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(); $updated_queue->subscribers = $updated_queue->getSubscribers();
expect($updated_queue->subscribers)->equals( expect($updated_queue->subscribers)->equals(
array( array(
'to_process' => array(), 'to_process' => array(),
'failed' => array(),
'processed' => array($this->subscriber->id) 'processed' => array($this->subscriber->id)
) )
); );
expect($updated_queue->count_total)->equals(1); expect($updated_queue->count_total)->equals(1);
expect($updated_queue->count_processed)->equals(1); expect($updated_queue->count_processed)->equals(1);
expect($updated_queue->count_failed)->equals(0);
expect($updated_queue->count_to_process)->equals(0); expect($updated_queue->count_to_process)->equals(0);
// statistics entry should be created // statistics entry should be created
@@ -166,18 +163,16 @@ class SendingQueueTest extends MailPoetTest {
$updated_queue = SendingQueue::findOne($this->queue->id); $updated_queue = SendingQueue::findOne($this->queue->id);
expect($updated_queue->status)->equals(SendingQueue::STATUS_COMPLETED); 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(); $updated_queue->subscribers = $updated_queue->getSubscribers();
expect($updated_queue->subscribers)->equals( expect($updated_queue->subscribers)->equals(
array( array(
'to_process' => array(), 'to_process' => array(),
'failed' => array(),
'processed' => array($this->subscriber->id) 'processed' => array($this->subscriber->id)
) )
); );
expect($updated_queue->count_total)->equals(1); expect($updated_queue->count_total)->equals(1);
expect($updated_queue->count_processed)->equals(1); expect($updated_queue->count_processed)->equals(1);
expect($updated_queue->count_failed)->equals(0);
expect($updated_queue->count_to_process)->equals(0); expect($updated_queue->count_to_process)->equals(0);
// statistics entry should be created // statistics entry should be created
@@ -196,8 +191,7 @@ class SendingQueueTest extends MailPoetTest {
$this->subscriber->id(), $this->subscriber->id(),
123 123
), ),
'processed' => array(), 'processed' => array()
'failed' => array()
) )
); );
$queue->count_total = 2; $queue->count_total = 2;
@@ -210,18 +204,16 @@ class SendingQueueTest extends MailPoetTest {
$sending_queue_worker->process(); $sending_queue_worker->process();
$updated_queue = SendingQueue::findOne($queue->id); $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(); $updated_queue->subscribers = $updated_queue->getSubscribers();
expect($updated_queue->subscribers)->equals( expect($updated_queue->subscribers)->equals(
array( array(
'to_process' => array(), 'to_process' => array(),
'failed' => array(),
'processed' => array($this->subscriber->id) 'processed' => array($this->subscriber->id)
) )
); );
expect($updated_queue->count_total)->equals(1); expect($updated_queue->count_total)->equals(1);
expect($updated_queue->count_processed)->equals(1); expect($updated_queue->count_processed)->equals(1);
expect($updated_queue->count_failed)->equals(0);
expect($updated_queue->count_to_process)->equals(0); expect($updated_queue->count_to_process)->equals(0);
// statistics entry should be created only for 1 subscriber // statistics entry should be created only for 1 subscriber
@@ -237,8 +229,7 @@ class SendingQueueTest extends MailPoetTest {
123, 123,
456 456
), ),
'processed' => array(), 'processed' => array()
'failed' => array()
) )
); );
$queue->count_total = 2; $queue->count_total = 2;
@@ -251,54 +242,19 @@ class SendingQueueTest extends MailPoetTest {
$sending_queue_worker->process(); $sending_queue_worker->process();
$updated_queue = SendingQueue::findOne($queue->id); $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(); $updated_queue->subscribers = $updated_queue->getSubscribers();
expect($updated_queue->subscribers)->equals( expect($updated_queue->subscribers)->equals(
array( array(
'to_process' => array(), 'to_process' => array(),
'failed' => array(),
'processed' => array() 'processed' => array()
) )
); );
expect($updated_queue->count_total)->equals(0); expect($updated_queue->count_total)->equals(0);
expect($updated_queue->count_processed)->equals(0); expect($updated_queue->count_processed)->equals(0);
expect($updated_queue->count_failed)->equals(0);
expect($updated_queue->count_to_process)->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() { function testItDoesNotSendToTrashedSubscribers() {
$sending_queue_worker = $this->sending_queue_worker; $sending_queue_worker = $this->sending_queue_worker;
$sending_queue_worker->mailer_task = Stub::make( $sending_queue_worker->mailer_task = Stub::make(

View File

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

View File

@@ -51,7 +51,8 @@ class MailerLogTest extends MailPoetTest {
function testItIncrementsSentCount() { function testItIncrementsSentCount() {
$mailer_log = array( $mailer_log = array(
'sent' => 1, 'sent' => 1,
'started' => time() 'started' => time(),
'error' => null
); );
Setting::setValue(MailerLog::SETTING_NAME, $mailer_log); Setting::setValue(MailerLog::SETTING_NAME, $mailer_log);
MailerLog::incrementSentCount(); MailerLog::incrementSentCount();
@@ -107,29 +108,126 @@ class MailerLogTest extends MailPoetTest {
expect($updated_mailer_log['sent'])->equals(0); 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( $mailer_config = array(
'frequency' => array( 'frequency' => array(
'emails' => 2, 'emails' => 2,
'interval' => 1 'interval' => 1
) )
); );
$mailer_log = array( $mailer_log = MailerLog::createMailerLog();
'sent' => 2, $mailer_log['sent'] = 2;
'started' => time() $mailer_log['started'] = time();
);
Setting::setValue(MailerLog::SETTING_NAME, $mailer_log); Setting::setValue(MailerLog::SETTING_NAME, $mailer_log);
Setting::setValue(Mailer::MAILER_CONFIG_SETTING_NAME, $mailer_config); Setting::setValue(Mailer::MAILER_CONFIG_SETTING_NAME, $mailer_config);
// exception is thrown when sending limit is reached // exception is thrown when sending limit is reached
try { try {
MailerLog::enforceSendingLimit(); MailerLog::enforceExecutionRequirements();
self::fail('Sending frequency exception was not thrown.'); self::fail('Sending frequency exception was not thrown.');
} catch(\Exception $e) { } catch(\Exception $e) {
expect($e->getMessage())->equals('Sending frequency limit has been reached.'); 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() { function _after() {
ORM::raw_execute('TRUNCATE ' . Setting::$_table); ORM::raw_execute('TRUNCATE ' . Setting::$_table);
} }

View File

@@ -11,15 +11,6 @@ class MailerTest extends MailPoetTest {
'access_key' => '1234567890', 'access_key' => '1234567890',
'secret_key' => 'abcdefghijk', 'secret_key' => 'abcdefghijk',
), ),
array(
'method' => 'ElasticEmail',
'api_key' => 'abcdefghijk'
),
array(
'method' => 'MailGun',
'domain' => 'example.com',
'api_key' => 'abcdefghijk'
),
array( array(
'method' => 'MailPoet', 'method' => 'MailPoet',
'mailpoet_api_key' => 'abcdefghijk' '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'), 'sentToXSubscribers': __('Sent to %$1d subscribers'),
'resume': __('Resume'), 'resume': __('Resume'),
'pause': __('Pause'), 'pause': __('Pause'),
'paused': __('Paused'),
'new': __('Add New'), 'new': __('Add New'),
'templateFileMalformedError': __('This template file appears to be damaged. Please try another one.'), 'templateFileMalformedError': __('This template file appears to be damaged. Please try another one.'),
@@ -232,6 +233,12 @@
'backToPostNotifications': __('Back to Post notifications'), 'backToPostNotifications': __('Back to Post notifications'),
'sentOn': __('Sent on'), '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 %>