diff --git a/assets/js/src/listing/listing.jsx b/assets/js/src/listing/listing.jsx index 7f8b1d85e1..d9ef512579 100644 --- a/assets/js/src/listing/listing.jsx +++ b/assets/js/src/listing/listing.jsx @@ -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() { diff --git a/assets/js/src/newsletters/listings/mixins.jsx b/assets/js/src/newsletters/listings/mixins.jsx index e3aa7857ef..613898153e 100644 --- a/assets/js/src/newsletters/listings/mixins.jsx +++ b/assets/js/src/newsletters/listings/mixins.jsx @@ -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 ( {MailPoet.I18n.t('notSentYet')} ); + } else if (mailer_log.status === 'paused') { + return ( + {MailPoet.I18n.t('paused')} + ) } else { if (newsletter.queue.status === 'scheduled') { return ( @@ -72,14 +77,8 @@ const _QueueMixin = { { 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) } ); @@ -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 ( +
+

{ mailer_error_notice }

+

{ MailPoet.I18n.t('mailerResumeSendingNotice') }

+

+ { MailPoet.I18n.t('mailerResumeSendingButton') } +

+
+ ); + }, + 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 }; \ No newline at end of file +export { _StatisticsMixin as StatisticsMixin }; +export { _MailerMixin as MailerMixin }; \ No newline at end of file diff --git a/assets/js/src/newsletters/listings/notification.jsx b/assets/js/src/newsletters/listings/notification.jsx index db0c50af50..85918c536c 100644 --- a/assets/js/src/newsletters/listings/notification.jsx +++ b/assets/js/src/newsletters/listings/notification.jsx @@ -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 } /> ); diff --git a/assets/js/src/newsletters/listings/notification_history.jsx b/assets/js/src/newsletters/listings/notification_history.jsx index 75284c1605..eaced5c474 100644 --- a/assets/js/src/newsletters/listings/notification_history.jsx +++ b/assets/js/src/newsletters/listings/notification_history.jsx @@ -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 (
@@ -73,7 +80,7 @@ const NewsletterListNotificationHistory = React.createClass({ { actions } - { this.renderQueueStatus(newsletter) } + { this.renderQueueStatus(newsletter, mailpoet_mailer_log) } { segments } @@ -116,6 +123,7 @@ const NewsletterListNotificationHistory = React.createClass({ auto_refresh={ true } sort_by="updated_at" sort_order="desc" + afterGetItems={ this.checkMailerStatus } />
); diff --git a/assets/js/src/newsletters/listings/standard.jsx b/assets/js/src/newsletters/listings/standard.jsx index 870ae70424..08e976df2f 100644 --- a/assets/js/src/newsletters/listings/standard.jsx +++ b/assets/js/src/newsletters/listings/standard.jsx @@ -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 } - { this.renderQueueStatus(newsletter) } + { this.renderQueueStatus(newsletter, meta.mta_log) } { segments } @@ -212,6 +216,7 @@ const NewsletterListStandard = React.createClass({ auto_refresh={ true } sort_by="updated_at" sort_order="desc" + afterGetItems={ this.checkMailerStatus } /> ); diff --git a/assets/js/src/newsletters/listings/welcome.jsx b/assets/js/src/newsletters/listings/welcome.jsx index 86b97f302b..e3f382da91 100644 --- a/assets/js/src/newsletters/listings/welcome.jsx +++ b/assets/js/src/newsletters/listings/welcome.jsx @@ -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 } /> ); diff --git a/assets/js/src/newsletters/newsletters.jsx b/assets/js/src/newsletters/newsletters.jsx index 0ed787054e..fd82973843 100644 --- a/assets/js/src/newsletters/newsletters.jsx +++ b/assets/js/src/newsletters/newsletters.jsx @@ -27,7 +27,7 @@ const App = React.createClass({ const container = document.getElementById('newsletters_container'); if(container) { - ReactDOM.render(( + const mailpoet_listing = ReactDOM.render(( @@ -49,4 +49,6 @@ if(container) { ), container); + + window.mailpoet_listing = mailpoet_listing; } diff --git a/assets/js/src/notice.js b/assets/js/src/notice.js index 43abfdd3c4..668cbfe496 100644 --- a/assets/js/src/notice.js +++ b/assets/js/src/notice.js @@ -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

- if(this.isHTML(message) === false) { - message = '

'+message+'

'; - } - // 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(''); - 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: '

'+this.formatMessage(message)+'

' - }, options)); - }, - success: function(message, options) { - this.show(jQuery.extend({}, { - type: 'success', - message: '

'+this.formatMessage(message)+'

' - }, options)); - }, - system: function(message, options) { - this.show(jQuery.extend({}, { - type: 'system', - static: true, - message: '

'+this.formatMessage(message)+'

' - }, options)); - }, - formatMessage: function(message) { - if(Array.isArray(message)) { - return message.join('
'); - } 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

+ if (this.isHTML(message) === false) { + message = '

'+message+'

'; + } + // set message + return this.element.html(message); + }, + formatMessage: function(message) { + if (Array.isArray(message)) { + return message.join('
'); + } 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(''); + 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)); + } }; }); diff --git a/lib/API/Endpoints/Mailer.php b/lib/API/Endpoints/Mailer.php index a920a4e4f2..709e783f18 100644 --- a/lib/API/Endpoints/Mailer.php +++ b/lib/API/Endpoints/Mailer.php @@ -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); + } } \ No newline at end of file diff --git a/lib/API/Endpoints/Newsletters.php b/lib/API/Endpoints/Newsletters.php index af40b5e24d..8557c66309 100644 --- a/lib/API/Endpoints/Newsletters.php +++ b/lib/API/Endpoints/Newsletters.php @@ -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') )); } diff --git a/lib/Config/Migrator.php b/lib/Config/Migrator.php index cc8f4223a8..9e9ad103e3 100644 --- a/lib/Config/Migrator.php +++ b/lib/Config/Migrator.php @@ -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,', diff --git a/lib/Config/Populator.php b/lib/Config/Populator.php index b0effe5a48..ae281a22a5 100644 --- a/lib/Config/Populator.php +++ b/lib/Config/Populator.php @@ -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() { diff --git a/lib/Cron/CronHelper.php b/lib/Cron/CronHelper.php index a45785aea1..9118d54dbd 100644 --- a/lib/Cron/CronHelper.php +++ b/lib/Cron/CronHelper.php @@ -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) { diff --git a/lib/Cron/Daemon.php b/lib/Cron/Daemon.php index fa1b11e1a0..d8e13389cb 100644 --- a/lib/Cron/Daemon.php +++ b/lib/Cron/Daemon.php @@ -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; diff --git a/lib/Cron/Workers/SendingQueue/SendingQueue.php b/lib/Cron/Workers/SendingQueue/SendingQueue.php index 83915b1307..b804291122 100644 --- a/lib/Cron/Workers/SendingQueue/SendingQueue.php +++ b/lib/Cron/Workers/SendingQueue/SendingQueue.php @@ -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; } diff --git a/lib/Mailer/Mailer.php b/lib/Mailer/Mailer.php index 80f758ce7f..0f99034edf 100644 --- a/lib/Mailer/Mailer.php +++ b/lib/Mailer/Mailer.php @@ -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 + ); + } } \ No newline at end of file diff --git a/lib/Mailer/MailerLog.php b/lib/Mailer/MailerLog.php index 4ca145979e..c2e3febe9b 100644 --- a/lib/Mailer/MailerLog.php +++ b/lib/Mailer/MailerLog.php @@ -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')); - } - } } \ No newline at end of file diff --git a/lib/Mailer/Methods/AmazonSES.php b/lib/Mailer/Methods/AmazonSES.php index 72c38dd927..dfbc4e6a5d 100644 --- a/lib/Mailer/Methods/AmazonSES.php +++ b/lib/Mailer/Methods/AmazonSES.php @@ -1,6 +1,8 @@ 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) { diff --git a/lib/Mailer/Methods/ElasticEmail.php b/lib/Mailer/Methods/ElasticEmail.php deleted file mode 100644 index 3b731d995c..0000000000 --- a/lib/Mailer/Methods/ElasticEmail.php +++ /dev/null @@ -1,56 +0,0 @@ -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)) - ); - } -} \ No newline at end of file diff --git a/lib/Mailer/Methods/MailGun.php b/lib/Mailer/Methods/MailGun.php deleted file mode 100644 index 8a7ff12c30..0000000000 --- a/lib/Mailer/Methods/MailGun.php +++ /dev/null @@ -1,63 +0,0 @@ -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)) - ); - } -} \ No newline at end of file diff --git a/lib/Mailer/Methods/MailPoet.php b/lib/Mailer/Methods/MailPoet.php index 43a4a2b345..228c82dcfe 100644 --- a/lib/Mailer/Methods/MailPoet.php +++ b/lib/Mailer/Methods/MailPoet.php @@ -1,6 +1,8 @@ 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'], diff --git a/lib/Mailer/Methods/PHPMail.php b/lib/Mailer/Methods/PHPMail.php index 19df340a52..87be807ee6 100644 --- a/lib/Mailer/Methods/PHPMail.php +++ b/lib/Mailer/Methods/PHPMail.php @@ -1,6 +1,8 @@ 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() { diff --git a/lib/Mailer/Methods/SMTP.php b/lib/Mailer/Methods/SMTP.php index 7ab318599d..8992f47175 100644 --- a/lib/Mailer/Methods/SMTP.php +++ b/lib/Mailer/Methods/SMTP.php @@ -1,6 +1,8 @@ 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() { diff --git a/lib/Mailer/Methods/SendGrid.php b/lib/Mailer/Methods/SendGrid.php index 9c990b2085..53eb6ccf30 100644 --- a/lib/Mailer/Methods/SendGrid.php +++ b/lib/Mailer/Methods/SendGrid.php @@ -1,6 +1,8 @@ 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) { diff --git a/lib/Models/SendingQueue.php b/lib/Models/SendingQueue.php index f520d4195a..39a9c87a47 100644 --- a/lib/Models/SendingQueue.php +++ b/lib/Models/SendingQueue.php @@ -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'); diff --git a/tests/unit/API/Endpoints/MailerTest.php b/tests/unit/API/Endpoints/MailerTest.php new file mode 100644 index 0000000000..554c9ebab4 --- /dev/null +++ b/tests/unit/API/Endpoints/MailerTest.php @@ -0,0 +1,21 @@ + 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(); + } +} \ No newline at end of file diff --git a/tests/unit/Cron/Workers/SendingQueue/SendingQueueTest.php b/tests/unit/Cron/Workers/SendingQueue/SendingQueueTest.php index fbc17ce9e9..07a4e6648e 100644 --- a/tests/unit/Cron/Workers/SendingQueue/SendingQueueTest.php +++ b/tests/unit/Cron/Workers/SendingQueue/SendingQueueTest.php @@ -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( diff --git a/tests/unit/Cron/Workers/SendingQueue/Tasks/MailerTest.php b/tests/unit/Cron/Workers/SendingQueue/Tasks/MailerTest.php index 4c9ab889ac..59cf56cc78 100644 --- a/tests/unit/Cron/Workers/SendingQueue/Tasks/MailerTest.php +++ b/tests/unit/Cron/Workers/SendingQueue/Tasks/MailerTest.php @@ -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() { diff --git a/tests/unit/Mailer/MailerLogTest.php b/tests/unit/Mailer/MailerLogTest.php index d4129e6786..ee7d9cd0e6 100644 --- a/tests/unit/Mailer/MailerLogTest.php +++ b/tests/unit/Mailer/MailerLogTest.php @@ -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); } diff --git a/tests/unit/Mailer/MailerTest.php b/tests/unit/Mailer/MailerTest.php index f6359b8d0f..61b5d51c00 100644 --- a/tests/unit/Mailer/MailerTest.php +++ b/tests/unit/Mailer/MailerTest.php @@ -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' diff --git a/tests/unit/Mailer/Methods/ElasticEmailTest.php b/tests/unit/Mailer/Methods/ElasticEmailTest.php deleted file mode 100644 index 1dd098628a..0000000000 --- a/tests/unit/Mailer/Methods/ElasticEmailTest.php +++ /dev/null @@ -1,78 +0,0 @@ -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 ' - ); - $this->reply_to = array( - 'reply_to_name' => 'Reply To', - 'reply_to_email' => 'reply-to@mailpoet.com', - 'reply_to_name_email' => 'Reply To ' - ); - $this->mailer = new ElasticEmail( - $this->settings['api_key'], - $this->sender, - $this->reply_to - ); - $this->subscriber = 'Recipient '; - $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(); - } -} \ No newline at end of file diff --git a/tests/unit/Mailer/Methods/MailGunTest.php b/tests/unit/Mailer/Methods/MailGunTest.php deleted file mode 100644 index f0ee563971..0000000000 --- a/tests/unit/Mailer/Methods/MailGunTest.php +++ /dev/null @@ -1,99 +0,0 @@ -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 ' - ); - $this->reply_to = array( - 'reply_to_name' => 'Reply To', - 'reply_to_email' => 'reply-to@mailpoet.com', - 'reply_to_name_email' => 'Reply To ' - ); - $this->mailer = new MailGun( - $this->settings['domain'], - $this->settings['api_key'], - $this->sender, - $this->reply_to - ); - $this->subscriber = 'Recipient '; - $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(); - } -} \ No newline at end of file diff --git a/views/newsletters.html b/views/newsletters.html index a46a72f832..3814005bf4 100644 --- a/views/newsletters.html +++ b/views/newsletters.html @@ -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 %> \ No newline at end of file