// # Mail // Handles sending email for Ghost const _ = require('lodash'); const Promise = require('bluebird'); const validator = require('validator'); const config = require('../../config'); const common = require('../../lib/common'); const settingsCache = require('../settings/cache'); const urlUtils = require('../../lib/url-utils'); 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'); function getDomain() { const domain = urlUtils.urlFor('home', true).match(new RegExp('^https?://([^/:?#]+)(?:[/:?#]|$)', 'i')); return domain && domain[1]; } function getFromAddress() { const configAddress = config.get('mail') && config.get('mail').from; const address = configAddress; // If we don't have a from address at all if (!address) { // Default to noreply@[blog.url] return getFromAddress(`noreply@${getDomain()}`); } // If we do have a from address, and it's just an email if (validator.isEmail(address)) { const defaultBlogTitle = settingsCache.get('title') ? settingsCache.get('title').replace(/"/g, '\\"') : common.i18n.t('common.mail.title', {domain: getDomain()}); return `"${defaultBlogTitle}" <${address}>`; } return address; } function createMessage(message) { return Object.assign({}, message, { from: getFromAddress(), generateTextFromHTML: true, encoding: 'base64' }); } function createMailError({message, err, ignoreDefaultMessage} = {message: ''}) { const fullErrorMessage = defaultErrorMessage + message; return new common.errors.EmailError({ message: ignoreDefaultMessage ? message : fullErrorMessage, err: err, help: helpMessage }); } 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; }); } 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') { reject(createMailError({ message: common.i18n.t('errors.mail.noMailServerAtAddress.error', {domain: data.domain}) })); } reject(createMailError()); }); response.statusHandler.once('requeue', function (data) { if (data.error && data.error.message) { reject(createMailError({ message: common.i18n.t('errors.mail.reason', {reason: data.error.message}) })); } reject(createMailError()); }); response.statusHandler.once('sent', function () { resolve(common.i18n.t('notices.mail.messageSent')); }); }); } };