0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Refactored GhostMailer into a class

no-issue

This breaks down the send method into distinct components that are
easier to reason about
This commit is contained in:
Fabien O'Carroll 2019-10-06 18:28:36 +07:00
parent f349c5385c
commit a6086995a6
2 changed files with 175 additions and 77 deletions

View file

@ -8,15 +8,9 @@ const common = require('../../lib/common');
const settingsCache = require('../settings/cache');
const urlUtils = require('../../lib/url-utils');
function GhostMailer() {
var nodemailer = require('nodemailer'),
transport = config.get('mail') && config.get('mail').transport || 'direct',
options = config.get('mail') && _.clone(config.get('mail').options) || {};
const helpMessage = common.i18n.t('errors.api.authentication.checkEmailConfigInstructions', {url: 'https://ghost.org/docs/concepts/config/#mail'});
const defaultErrorMessage = common.i18n.t('errors.mail.failedSendingEmail.error');
this.state = {};
this.transport = nodemailer.createTransport(transport, options);
this.state.usingDirect = transport === 'direct';
}
function getDomain() {
const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i'));
return domain && domain[1];
@ -41,75 +35,93 @@ function getFromAddress() {
return address;
}
// Sends an email message enforcing `to` (blog owner) and `from` fields
// This assumes that api.settings.read('email') was already done on the API level
GhostMailer.prototype.send = function (message) {
var self = this,
to,
help = common.i18n.t('errors.api.authentication.checkEmailConfigInstructions', {url: 'https://ghost.org/docs/concepts/config/#mail'}),
errorMessage = common.i18n.t('errors.mail.failedSendingEmail.error');
// important to clone message as we modify it
message = _.clone(message) || {};
to = message.to || false;
if (!(message && message.subject && message.html && message.to)) {
return Promise.reject(new common.errors.EmailError({
message: common.i18n.t('errors.mail.incompleteMessageData.error'),
help: help
}));
}
message = _.extend(message, {
from: self.from(),
to: to,
function createMessage(message) {
return Object.assign({}, message, {
from: getFromAddress(),
generateTextFromHTML: true,
encoding: 'base64'
});
}
return new Promise(function (resolve, reject) {
self.transport.sendMail(message, function (err, response) {
if (err) {
errorMessage += common.i18n.t('errors.mail.reason', {reason: err.message || err});
function createMailError({message, err, ignoreDefaultMessage} = {message: ''}) {
const fullErrorMessage = defaultErrorMessage + message;
return new common.errors.EmailError({
message: ignoreDefaultMessage ? message : fullErrorMessage,
err: err,
help: helpMessage
});
}
return reject(new common.errors.EmailError({
message: errorMessage,
err: err,
help: help
}));
module.exports = class GhostMailer {
constructor() {
const nodemailer = require('nodemailer');
const transport = config.get('mail') && config.get('mail').transport || 'direct';
// nodemailer mutates the options passed to createTransport
const options = config.get('mail') && _.clone(config.get('mail').options) || {};
this.state = {
usingDirect: transport === 'direct'
};
this.transport = nodemailer.createTransport(transport, options);
}
send(message) {
if (!(message && message.subject && message.html && message.to)) {
return Promise.reject(createMailError({
message: common.i18n.t('errors.mail.incompleteMessageData.error'),
ignoreDefaultMessage: true
}));
}
const messageToSend = createMessage(message);
return this.sendMail(messageToSend).then((response) => {
if (this.transport.transportType === 'DIRECT') {
return this.handleDirectTransportResponse(response);
}
return response;
});
}
if (self.transport.transportType !== 'DIRECT') {
return resolve(response);
}
sendMail(message) {
return new Promise((resolve, reject) => {
this.transport.sendMail(message, (err, response) => {
if (err) {
reject(createMailError({
message: common.i18n.t('errors.mail.reason', {reason: err.message || err}),
err
}));
}
resolve(response);
});
});
}
handleDirectTransportResponse(response) {
return new Promise((resolve, reject) => {
response.statusHandler.once('failed', function (data) {
if (data.error && data.error.errno === 'ENOTFOUND') {
errorMessage += common.i18n.t('errors.mail.noMailServerAtAddress.error', {domain: data.domain});
reject(createMailError({
message: common.i18n.t('errors.mail.noMailServerAtAddress.error', {domain: data.domain})
}));
}
return reject(new common.errors.EmailError({
message: errorMessage,
help: help
}));
reject(createMailError());
});
response.statusHandler.once('requeue', function (data) {
if (data.error && data.error.message) {
errorMessage += common.i18n.t('errors.mail.reason', {reason: data.error.message});
reject(createMailError({
message: common.i18n.t('errors.mail.reason', {reason: data.error.message})
}));
}
return reject(new common.errors.EmailError({
message: errorMessage,
help: help
}));
reject(createMailError());
});
response.statusHandler.once('sent', function () {
return resolve(common.i18n.t('notices.mail.messageSent'));
resolve(common.i18n.t('notices.mail.messageSent'));
});
});
});
}
};
module.exports = GhostMailer;

View file

@ -167,7 +167,7 @@ describe('Mail: Ghostmailer', function () {
});
describe('From address', function () {
it('should use the config', function () {
it('should use the config', async function () {
configUtils.set({
mail: {
from: '"Blog Title" <static@example.com>'
@ -176,89 +176,175 @@ describe('Mail: Ghostmailer', function () {
mailer = new mail.GhostMailer();
mailer.from().should.equal('"Blog Title" <static@example.com>');
sandbox.stub(mailer, 'sendMail').resolves();
mailer.transport.transportType = 'NOT DIRECT';
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"Blog Title" <static@example.com>');
});
describe('should fall back to [blog.title] <noreply@[blog.url]>', function () {
let mailer;
beforeEach(function () {
beforeEach(async function () {
mailer = new mail.GhostMailer();
sandbox.stub(mailer, 'sendMail').resolves();
mailer.transport.transportType = 'NOT DIRECT';
sandbox.stub(settingsCache, 'get').returns('Test');
});
it('standard domain', function () {
it('standard domain', async function () {
sandbox.stub(urlUtils, 'urlFor').returns('http://default.com');
configUtils.set({mail: {from: null}});
mailer.from().should.equal('"Test" <noreply@default.com>');
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"Test" <noreply@default.com>');
});
it('trailing slash', function () {
it('trailing slash', async function () {
sandbox.stub(urlUtils, 'urlFor').returns('http://default.com/');
configUtils.set({mail: {from: null}});
mailer.from().should.equal('"Test" <noreply@default.com>');
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"Test" <noreply@default.com>');
});
it('strip port', function () {
it('strip port', async function () {
sandbox.stub(urlUtils, 'urlFor').returns('http://default.com:2368/');
configUtils.set({mail: {from: null}});
mailer.from().should.equal('"Test" <noreply@default.com>');
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"Test" <noreply@default.com>');
});
it('Escape title', function () {
it('Escape title', async function () {
settingsCache.get.restore();
sandbox.stub(settingsCache, 'get').returns('Test"');
sandbox.stub(urlUtils, 'urlFor').returns('http://default.com:2368/');
configUtils.set({mail: {from: null}});
mailer.from().should.equal('"Test\\"" <noreply@default.com>');
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"Test\\"" <noreply@default.com>');
});
});
it('should use mail.from', function () {
it('should use mail.from', async function () {
// Standard domain
configUtils.set({mail: {from: '"bar" <from@default.com>'}});
mailer = new mail.GhostMailer();
mailer.from().should.equal('"bar" <from@default.com>');
sandbox.stub(mailer, 'sendMail').resolves();
mailer.transport.transportType = 'NOT DIRECT';
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"bar" <from@default.com>');
});
it('should attach blog title', function () {
it('should attach blog title', async function () {
sandbox.stub(settingsCache, 'get').returns('Test');
configUtils.set({mail: {from: 'from@default.com'}});
mailer = new mail.GhostMailer();
mailer.from().should.equal('"Test" <from@default.com>');
sandbox.stub(mailer, 'sendMail').resolves();
mailer.transport.transportType = 'NOT DIRECT';
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"Test" <from@default.com>');
// only from set
configUtils.set({mail: {from: 'from@default.com'}});
mailer.from().should.equal('"Test" <from@default.com>');
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"Test" <from@default.com>');
});
it('should ignore theme title if from address is Title <email@address.com> format', function () {
it('should ignore theme title if from address is Title <email@address.com> format', async function () {
configUtils.set({mail: {from: '"R2D2" <from@default.com>'}});
mailer = new mail.GhostMailer();
mailer.from().should.equal('"R2D2" <from@default.com>');
sandbox.stub(mailer, 'sendMail').resolves();
mailer.transport.transportType = 'NOT DIRECT';
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"R2D2" <from@default.com>');
// only from set
configUtils.set({mail: {from: '"R2D2" <from@default.com>'}});
mailer.from().should.equal('"R2D2" <from@default.com>');
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"R2D2" <from@default.com>');
});
it('should use default title if not theme title is provided', function () {
it('should use default title if not theme title is provided', async function () {
configUtils.set({mail: {from: null}});
sandbox.stub(urlUtils, 'urlFor').returns('http://default.com:2368/');
mailer = new mail.GhostMailer();
mailer.from().should.equal('"Ghost at default.com" <noreply@default.com>');
sandbox.stub(mailer, 'sendMail').resolves();
mailer.transport.transportType = 'NOT DIRECT';
await mailer.send({
to: 'user@example.com',
subject: 'subject',
html: 'content'
});
mailer.sendMail.firstCall.args[0].from.should.equal('"Ghost at default.com" <noreply@default.com>');
});
});
});