Ghost/ghost/email-service/lib/sending-service.js
Simon Backx 0e8d13bdde 🐛 Fixed email replacements without fallback
fixes https://github.com/TryGhost/Team/issues/2557

When a member doen't have a name, and the first_name replacement doesn't have a fallback, we did show %recipient.first_name% instead of an empty string.
2023-02-13 15:58:40 +01:00

161 lines
4.8 KiB
JavaScript

const validator = require('@tryghost/validator');
const logging = require('@tryghost/logging');
/**
* @typedef {object} EmailData
* @prop {string} html
* @prop {string} plaintext
* @prop {string} subject
* @prop {string} from
* @prop {string} emailId
* @prop {string} [replyTo]
* @prop {Recipient[]} recipients
* @prop {import("./email-renderer").ReplacementDefinition[]} replacementDefinitions
*
* @typedef {object} IEmailProviderService
* @prop {(emailData: EmailData, options: EmailSendingOptions) => Promise<EmailProviderSuccessResponse>} send
* @prop {() => number} getMaximumRecipients
*
* @typedef {object} Post
* @typedef {object} Newsletter
*/
/**
* @typedef {import("./email-renderer")} EmailRenderer
* @typedef {import("./email-renderer").EmailBody} EmailBody
*/
/**
* @typedef {object} EmailSendingOptions
* @prop {boolean} clickTrackingEnabled
* @prop {boolean} openTrackingEnabled
* @prop {{get(id: string): EmailBody | null, set(id: string, body: EmailBody): void}} [emailBodyCache]
*/
/**
* @typedef {import("./email-renderer").MemberLike} MemberLike
*/
/**
* @typedef {object} Recipient
* @prop {string} email
* @prop {Replacement[]} replacements
*/
/**
* @typedef {object} Replacement
* @prop {string} id
* @prop {RegExp} token
* @prop {string} value
*/
/**
* @typedef {object} EmailProviderSuccessResponse
* @prop {string} id
*/
class SendingService {
#emailProvider;
#emailRenderer;
/**
* @param {object} dependencies
* @param {IEmailProviderService} dependencies.emailProvider
* @param {EmailRenderer} dependencies.emailRenderer
*/
constructor({
emailProvider,
emailRenderer
}) {
this.#emailProvider = emailProvider;
this.#emailRenderer = emailRenderer;
}
getMaximumRecipients() {
return this.#emailProvider.getMaximumRecipients();
}
/**
* Send a given post, rendered for a given newsletter and segment to the members provided in the list
* @param {object} data
* @param {Post} data.post
* @param {Newsletter} data.newsletter
* @param {string|null} data.segment
* @param {string|null} data.emailId
* @param {MemberLike[]} data.members
* @param {EmailSendingOptions} options
* @returns {Promise<EmailProviderSuccessResponse>}
*/
async send({post, newsletter, segment, members, emailId}, options) {
const cacheId = emailId + '-' + (segment ?? 'null');
/**
* @type {EmailBody | null}
*/
let emailBody = null;
if (options.emailBodyCache) {
emailBody = options.emailBodyCache.get(cacheId);
}
if (!emailBody) {
emailBody = await this.#emailRenderer.renderBody(
post,
newsletter,
segment,
{
clickTrackingEnabled: !!options.clickTrackingEnabled
}
);
if (options.emailBodyCache) {
options.emailBodyCache.set(cacheId, emailBody);
}
}
const recipients = this.buildRecipients(members, emailBody.replacements);
return await this.#emailProvider.send({
subject: this.#emailRenderer.getSubject(post),
from: this.#emailRenderer.getFromAddress(post, newsletter),
replyTo: this.#emailRenderer.getReplyToAddress(post, newsletter) ?? undefined,
html: emailBody.html,
plaintext: emailBody.plaintext,
recipients,
emailId: emailId,
replacementDefinitions: emailBody.replacements
}, {
clickTrackingEnabled: !!options.clickTrackingEnabled,
openTrackingEnabled: !!options.openTrackingEnabled
});
}
/**
* @private
* @param {MemberLike[]} members
* @param {import("./email-renderer").ReplacementDefinition[]} replacementDefinitions
* @returns {Recipient[]}
*/
buildRecipients(members, replacementDefinitions) {
return members.map((member) => {
return {
email: member.email?.trim(),
replacements: replacementDefinitions.map((def) => {
return {
id: def.id,
token: def.token,
value: def.getValue(member) || ''
};
})
};
}).filter((recipient) => {
// Remove invalid recipient email addresses
const isValidRecipient = validator.isEmail(recipient.email, {legacy: false});
if (!isValidRecipient) {
logging.warn(`Removed recipient ${recipient.email} from list because it is not a valid email address`);
}
return isValidRecipient;
});
}
}
module.exports = SendingService;