mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-30 21:40:39 +03:00
169 lines
5.1 KiB
JavaScript
169 lines
5.1 KiB
JavaScript
|
const logging = require('@tryghost/logging');
|
||
|
const errors = require('@tryghost/errors');
|
||
|
const debug = require('@tryghost/debug')('email-service:mailgun-provider-service');
|
||
|
|
||
|
/**
|
||
|
* @typedef {object} Recipient
|
||
|
* @prop {string} email
|
||
|
* @prop {Replacement[]} replacements
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {object} Replacement
|
||
|
* @prop {string} token
|
||
|
* @prop {string} value
|
||
|
* @prop {string} id
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {object} EmailSendingOptions
|
||
|
* @prop {boolean} clickTrackingEnabled
|
||
|
* @prop {boolean} openTrackingEnabled
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @typedef {object} EmailProviderSuccessResponse
|
||
|
* @prop {string} id
|
||
|
*/
|
||
|
|
||
|
class MailgunEmailProvider {
|
||
|
#mailgunClient;
|
||
|
#errorHandler;
|
||
|
|
||
|
static BATCH_SIZE = 1000;
|
||
|
|
||
|
/**
|
||
|
* @param {object} dependencies
|
||
|
* @param {import('@tryghost/mailgun-client/lib/mailgun-client')} dependencies.mailgunClient - mailgun client to send emails
|
||
|
* @param {Function} [dependencies.errorHandler] - custom error handler for logging exceptions
|
||
|
*/
|
||
|
constructor({
|
||
|
mailgunClient,
|
||
|
errorHandler
|
||
|
}) {
|
||
|
this.#mailgunClient = mailgunClient;
|
||
|
this.#errorHandler = errorHandler;
|
||
|
}
|
||
|
|
||
|
#createRecipientData(replacements) {
|
||
|
let recipientData = {};
|
||
|
|
||
|
recipientData = replacements.reduce((acc, replacement) => {
|
||
|
const {id, value} = replacement;
|
||
|
if (!acc[id]) {
|
||
|
acc[id] = {};
|
||
|
}
|
||
|
acc[id] = value;
|
||
|
return acc;
|
||
|
}, {});
|
||
|
|
||
|
return recipientData;
|
||
|
}
|
||
|
|
||
|
#updateRecipientVariables(data, replacementDefinitions) {
|
||
|
for (const def of replacementDefinitions) {
|
||
|
data = data.replace(
|
||
|
def.token,
|
||
|
`%recipient.${def.id}%`
|
||
|
);
|
||
|
}
|
||
|
return data;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create mailgun error message for storing in the database
|
||
|
* @param {Object} error
|
||
|
* @param {string} error.message
|
||
|
* @param {string} error.details
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
#createMailgunErrorMessage(error) {
|
||
|
const message = (error?.message || '') + ':' + (error?.details || '');
|
||
|
return message.slice(0, 2000);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send an email using the Mailgun API
|
||
|
* @param {import('./sending-service').EmailData} data
|
||
|
* @param {EmailSendingOptions} options
|
||
|
* @returns {Promise<EmailProviderSuccessResponse>}
|
||
|
*/
|
||
|
async send(data, options) {
|
||
|
const {
|
||
|
subject,
|
||
|
html,
|
||
|
plaintext,
|
||
|
from,
|
||
|
replyTo,
|
||
|
emailId,
|
||
|
recipients,
|
||
|
replacementDefinitions
|
||
|
} = data;
|
||
|
|
||
|
logging.info(`Sending email to ${recipients.length} recipients`);
|
||
|
const startTime = Date.now();
|
||
|
debug(`sending message to ${recipients.length} recipients`);
|
||
|
|
||
|
try {
|
||
|
const messageData = {
|
||
|
subject,
|
||
|
html,
|
||
|
plaintext,
|
||
|
from,
|
||
|
replyTo,
|
||
|
id: emailId,
|
||
|
track_opens: !!options.openTrackingEnabled,
|
||
|
track_clicks: !!options.clickTrackingEnabled
|
||
|
};
|
||
|
|
||
|
// create recipient data for Mailgun using replacement definitions
|
||
|
const recipientData = recipients.reduce((acc, recipient) => {
|
||
|
acc[recipient.email] = this.#createRecipientData(recipient.replacements);
|
||
|
return acc;
|
||
|
}, {});
|
||
|
|
||
|
// update content to use Mailgun variable syntax for all replacements
|
||
|
['html', 'plaintext'].forEach((key) => {
|
||
|
if (messageData[key]) {
|
||
|
messageData[key] = this.#updateRecipientVariables(messageData[key], replacementDefinitions);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// send the email using Mailgun
|
||
|
// uses empty replacements array as we've already replaced all tokens with Mailgun variables
|
||
|
const response = await this.#mailgunClient.send(
|
||
|
messageData,
|
||
|
recipientData,
|
||
|
[]
|
||
|
);
|
||
|
|
||
|
debug(`sent message (${Date.now() - startTime}ms)`);
|
||
|
logging.info(`Sent message (${Date.now() - startTime}ms)`);
|
||
|
|
||
|
// Return mailgun provider id, trim <> from response
|
||
|
return {
|
||
|
id: response.id.trim().replace(/^<|>$/g, '')
|
||
|
};
|
||
|
} catch ({error, messageData}) {
|
||
|
// REF: possible mailgun errors https://documentation.mailgun.com/en/latest/api-intro.html#status-codes
|
||
|
let ghostError = new errors.EmailError({
|
||
|
statusCode: error.status,
|
||
|
message: this.#createMailgunErrorMessage(error),
|
||
|
errorDetails: JSON.stringify({error, messageData}),
|
||
|
context: `Mailgun Error ${error.status}: ${error.details}`,
|
||
|
help: `https://ghost.org/docs/newsletters/#bulk-email-configuration`,
|
||
|
code: 'BULK_EMAIL_SEND_FAILED'
|
||
|
});
|
||
|
|
||
|
logging.warn(ghostError);
|
||
|
debug(`failed to send message (${Date.now() - startTime}ms)`);
|
||
|
|
||
|
// log error to custom error handler. ex sentry
|
||
|
this.#errorHandler(ghostError);
|
||
|
throw ghostError;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = MailgunEmailProvider;
|