2019-11-13 13:52:23 +03:00
|
|
|
const _ = require('lodash');
|
2020-04-30 22:26:12 +03:00
|
|
|
const errors = require('@tryghost/errors');
|
2020-05-28 21:30:23 +03:00
|
|
|
const {i18n} = require('../../lib/common');
|
|
|
|
const logging = require('../../../shared/logging');
|
2019-11-13 06:56:31 +03:00
|
|
|
const mailgunProvider = require('./mailgun');
|
2020-05-27 20:47:53 +03:00
|
|
|
const configService = require('../../../shared/config');
|
2019-11-14 18:09:51 +03:00
|
|
|
const settingsCache = require('../settings/cache');
|
2020-04-25 22:53:58 +03:00
|
|
|
const sentry = require('../../../shared/sentry');
|
2020-08-06 16:19:39 +03:00
|
|
|
const debug = require('ghost-ignition').debug('mega');
|
2019-11-04 08:36:10 +03:00
|
|
|
|
2019-11-15 14:25:33 +03:00
|
|
|
/**
|
|
|
|
* An object representing batch request result
|
|
|
|
* @typedef { Object } BatchResultBase
|
|
|
|
* @property { string } data - data that is returned from Mailgun or one which Mailgun was called with
|
|
|
|
*/
|
|
|
|
class BatchResultBase {
|
|
|
|
}
|
|
|
|
|
|
|
|
class SuccessfulBatch extends BatchResultBase {
|
|
|
|
constructor(data) {
|
|
|
|
super();
|
|
|
|
this.data = data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class FailedBatch extends BatchResultBase {
|
|
|
|
constructor(error, data) {
|
|
|
|
super();
|
2020-07-10 20:32:15 +03:00
|
|
|
error.originalMessage = error.message;
|
2019-11-20 20:31:12 +03:00
|
|
|
|
2020-07-10 20:32:15 +03:00
|
|
|
if (error.statusCode >= 500) {
|
|
|
|
error.message = 'Email service is currently unavailable - please try again';
|
|
|
|
} else if (error.statusCode === 401) {
|
|
|
|
error.message = 'Email failed to send - please verify your credentials';
|
|
|
|
} else if (error.message && error.message.toLowerCase().includes('dmarc')) {
|
|
|
|
error.message = 'Unable to send email from domains implementing strict DMARC policies';
|
|
|
|
} else if (error.message.includes(`'to' parameter is not a valid address`)) {
|
|
|
|
error.message = 'Recipient is not a valid address';
|
|
|
|
} else {
|
|
|
|
error.message = 'Email failed to send - please verify your email settings';
|
2019-11-20 20:31:12 +03:00
|
|
|
}
|
|
|
|
|
2019-11-15 14:25:33 +03:00
|
|
|
this.error = error;
|
|
|
|
this.data = data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-04 13:24:02 +03:00
|
|
|
/**
|
2019-11-04 08:36:10 +03:00
|
|
|
* An email address
|
|
|
|
* @typedef { string } EmailAddress
|
|
|
|
*/
|
|
|
|
|
2019-11-04 13:24:02 +03:00
|
|
|
/**
|
2019-11-04 08:36:10 +03:00
|
|
|
* An object representing an email to send
|
|
|
|
* @typedef { Object } Email
|
|
|
|
* @property { string } html - The html content of the email
|
2019-11-04 08:36:10 +03:00
|
|
|
* @property { string } subject - The subject of the email
|
2019-11-04 08:36:10 +03:00
|
|
|
*/
|
|
|
|
|
|
|
|
module.exports = {
|
2019-11-15 14:25:33 +03:00
|
|
|
SuccessfulBatch,
|
|
|
|
FailedBatch,
|
2019-11-04 08:36:10 +03:00
|
|
|
/**
|
|
|
|
* @param {Email} message - The message to send
|
|
|
|
* @param {[EmailAddress]} recipients - the recipients to send the email to
|
2019-11-06 13:50:41 +03:00
|
|
|
* @param {[object]} recipientData - list of data keyed by email to inject into the email
|
2019-11-15 14:25:33 +03:00
|
|
|
* @returns {Promise<Array<BatchResultBase>>} An array of promises representing the success of the batch email sending
|
2019-11-04 08:36:10 +03:00
|
|
|
*/
|
2019-11-13 14:31:37 +03:00
|
|
|
async send(message, recipients, recipientData = {}) {
|
2019-11-13 13:52:23 +03:00
|
|
|
let BATCH_SIZE = 1000;
|
2019-11-14 08:15:26 +03:00
|
|
|
const mailgunInstance = mailgunProvider.getInstance();
|
2019-11-08 07:13:43 +03:00
|
|
|
if (!mailgunInstance) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
let fromAddress = message.from;
|
2019-11-13 13:52:23 +03:00
|
|
|
if (/@localhost$/.test(message.from) || /@ghost.local$/.test(message.from)) {
|
2019-11-08 07:13:43 +03:00
|
|
|
fromAddress = 'localhost@example.com';
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn(`Rewriting bulk email from address ${message.from} to ${fromAddress}`);
|
2019-11-13 13:52:23 +03:00
|
|
|
|
|
|
|
BATCH_SIZE = 2;
|
2019-11-08 07:13:43 +03:00
|
|
|
}
|
2019-11-13 13:36:19 +03:00
|
|
|
|
2020-06-08 19:26:11 +03:00
|
|
|
const blogTitle = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : '';
|
2020-09-03 08:00:09 +03:00
|
|
|
let supportAddress = message.supportAddress;
|
|
|
|
delete message.supportAddress;
|
|
|
|
const replyAddressOption = settingsCache.get('members_reply_address');
|
|
|
|
const replyToAddress = (replyAddressOption === 'support') ? supportAddress : fromAddress;
|
2020-06-08 19:26:11 +03:00
|
|
|
fromAddress = blogTitle ? `"${blogTitle}"<${fromAddress}>` : fromAddress;
|
2019-11-15 14:25:33 +03:00
|
|
|
|
|
|
|
const chunkedRecipients = _.chunk(recipients, BATCH_SIZE);
|
|
|
|
|
2020-08-06 16:19:39 +03:00
|
|
|
return Promise.map(chunkedRecipients, (toAddresses, chunkIndex) => {
|
2019-11-15 14:25:33 +03:00
|
|
|
const recipientVariables = {};
|
|
|
|
toAddresses.forEach((email) => {
|
|
|
|
recipientVariables[email] = recipientData[email];
|
|
|
|
});
|
|
|
|
|
|
|
|
const batchData = {
|
|
|
|
to: toAddresses,
|
|
|
|
from: fromAddress,
|
2020-08-26 11:02:07 +03:00
|
|
|
'h:Reply-To': replyToAddress || fromAddress,
|
2019-11-15 14:25:33 +03:00
|
|
|
'recipient-variables': recipientVariables
|
|
|
|
};
|
|
|
|
|
|
|
|
const bulkEmailConfig = configService.get('bulkEmail');
|
|
|
|
|
|
|
|
if (bulkEmailConfig && bulkEmailConfig.mailgun && bulkEmailConfig.mailgun.tag) {
|
|
|
|
Object.assign(batchData, {
|
|
|
|
'o:tag': [bulkEmailConfig.mailgun.tag, 'bulk-email']
|
2019-11-13 13:52:23 +03:00
|
|
|
});
|
2019-11-15 14:25:33 +03:00
|
|
|
}
|
|
|
|
|
2020-07-24 13:55:34 +03:00
|
|
|
if (bulkEmailConfig && bulkEmailConfig.mailgun && bulkEmailConfig.mailgun.testmode) {
|
|
|
|
Object.assign(batchData, {
|
|
|
|
'o:testmode': true
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-11-15 14:25:33 +03:00
|
|
|
const messageData = Object.assign({}, message, batchData);
|
2019-11-13 13:52:23 +03:00
|
|
|
|
2020-06-15 17:31:09 +03:00
|
|
|
// Rename plaintext field to text for Mailgun
|
|
|
|
messageData.text = messageData.plaintext;
|
|
|
|
delete messageData.plaintext;
|
|
|
|
|
2019-11-15 14:25:33 +03:00
|
|
|
return new Promise((resolve) => {
|
2020-08-06 16:19:39 +03:00
|
|
|
const batchStartTime = Date.now();
|
|
|
|
debug(`sending message batch ${chunkIndex + 1} to ${toAddresses.length}`);
|
2019-11-15 14:25:33 +03:00
|
|
|
mailgunInstance.messages().send(messageData, (error, body) => {
|
|
|
|
if (error) {
|
|
|
|
// NOTE: logging an error here only but actual handling should happen in more sophisticated batch retry handler
|
|
|
|
// REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#errors
|
2020-04-30 22:26:12 +03:00
|
|
|
let ghostError = new errors.EmailError({
|
2019-11-15 14:25:33 +03:00
|
|
|
err: error,
|
2020-04-30 22:26:12 +03:00
|
|
|
context: i18n.t('errors.services.mega.requestFailed.error')
|
2020-03-04 16:44:23 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
sentry.captureException(ghostError);
|
2020-04-30 22:26:12 +03:00
|
|
|
logging.warn(ghostError);
|
2019-11-13 13:52:23 +03:00
|
|
|
|
2019-11-15 14:25:33 +03:00
|
|
|
// NOTE: these are generated variables, so can be regenerated when retry is done
|
|
|
|
const data = _.omit(batchData, ['recipient-variables']);
|
2020-08-06 16:19:39 +03:00
|
|
|
debug(`failed message batch ${chunkIndex + 1} (${Date.now() - batchStartTime}ms)`);
|
2019-11-15 14:25:33 +03:00
|
|
|
resolve(new FailedBatch(error, data));
|
|
|
|
} else {
|
2020-08-06 16:19:39 +03:00
|
|
|
debug(`sent message batch ${chunkIndex + 1} (${Date.now() - batchStartTime}ms)`);
|
2019-11-15 14:25:33 +03:00
|
|
|
resolve(new SuccessfulBatch(body));
|
|
|
|
}
|
|
|
|
});
|
2019-11-13 13:52:23 +03:00
|
|
|
});
|
2020-08-06 16:19:39 +03:00
|
|
|
}, {concurrency: 10});
|
2019-11-04 08:36:10 +03:00
|
|
|
}
|
|
|
|
};
|