2018-04-29 16:13:40 +00:00
|
|
|
'use strict';
|
|
|
|
|
2018-09-27 19:32:35 +00:00
|
|
|
const log = require('./log');
|
2018-04-29 16:13:40 +00:00
|
|
|
const config = require('config');
|
|
|
|
|
|
|
|
const Handlebars = require('handlebars');
|
|
|
|
const util = require('util');
|
|
|
|
const nodemailer = require('nodemailer');
|
|
|
|
const aws = require('aws-sdk');
|
|
|
|
const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
|
|
|
|
const sendConfigurations = require('../models/send-configurations');
|
2018-12-21 18:09:18 +00:00
|
|
|
const { ZoneMTAType, MailerType } = require('../../shared/send-configurations');
|
|
|
|
const builtinZoneMta = require('./builtin-zone-mta');
|
2018-04-29 16:13:40 +00:00
|
|
|
|
|
|
|
const contextHelpers = require('./context-helpers');
|
|
|
|
const settings = require('../models/settings');
|
|
|
|
const tools = require('./tools');
|
|
|
|
const htmlToText = require('html-to-text');
|
|
|
|
|
|
|
|
const bluebird = require('bluebird');
|
|
|
|
|
|
|
|
const transports = new Map();
|
|
|
|
|
2018-05-20 18:27:35 +00:00
|
|
|
async function getOrCreateMailer(sendConfigurationId) {
|
|
|
|
let sendConfiguration;
|
|
|
|
|
2018-12-16 21:35:21 +00:00
|
|
|
if (!sendConfigurationId) {
|
2018-05-20 18:27:35 +00:00
|
|
|
sendConfiguration = await sendConfigurations.getSystemSendConfiguration();
|
|
|
|
} else {
|
2018-07-31 04:34:28 +00:00
|
|
|
sendConfiguration = await sendConfigurations.getById(contextHelpers.getAdminContext(), sendConfigurationId, false, true);
|
2018-04-29 16:13:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const transport = transports.get(sendConfiguration.id) || await _createTransport(sendConfiguration);
|
|
|
|
return transport.mailer;
|
|
|
|
}
|
|
|
|
|
2018-09-02 12:59:02 +00:00
|
|
|
function invalidateMailer(sendConfigurationId) {
|
|
|
|
transports.delete(sendConfigurationId);
|
2018-04-29 16:13:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2018-12-16 21:35:21 +00:00
|
|
|
function _addDkimKeys(transport, mail) {
|
|
|
|
const sendConfiguration = transport.mailer.sendConfiguration;
|
|
|
|
|
2018-12-21 18:09:18 +00:00
|
|
|
if (sendConfiguration.mailer_type === MailerType.ZONE_MTA) {
|
|
|
|
const mailerSettings = sendConfiguration.mailer_settings;
|
|
|
|
|
|
|
|
if (mailerSettings.zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || mailerSettings.zoneMtaType === ZoneMTAType.BUILTIN) {
|
|
|
|
if (!mail.headers) {
|
|
|
|
mail.headers = {};
|
|
|
|
}
|
2018-12-16 21:35:21 +00:00
|
|
|
|
2018-12-21 18:09:18 +00:00
|
|
|
const dkimDomain = mailerSettings.dkimDomain;
|
|
|
|
const dkimSelector = (mailerSettings.dkimSelector || '').trim();
|
|
|
|
const dkimPrivateKey = (mailerSettings.dkimPrivateKey || '').trim();
|
2018-12-16 21:35:21 +00:00
|
|
|
|
2018-12-21 18:09:18 +00:00
|
|
|
if (dkimSelector && dkimPrivateKey) {
|
|
|
|
const from = (mail.from.address || '').trim();
|
|
|
|
const domain = from.split('@').pop().toLowerCase().trim();
|
2018-12-16 21:35:21 +00:00
|
|
|
|
2018-12-21 18:09:18 +00:00
|
|
|
mail.headers['x-mailtrain-dkim'] = JSON.stringify({
|
|
|
|
domainName: dkimDomain || domain,
|
|
|
|
keySelector: dkimSelector,
|
|
|
|
privateKey: dkimPrivateKey
|
|
|
|
});
|
|
|
|
}
|
2018-12-16 21:35:21 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
|
|
|
|
async function _sendMail(transport, mail, template) {
|
2018-12-16 21:35:21 +00:00
|
|
|
_addDkimKeys(transport, mail);
|
|
|
|
|
2018-09-18 08:30:13 +00:00
|
|
|
let tryCount = 0;
|
|
|
|
const trySend = (callback) => {
|
|
|
|
tryCount++;
|
|
|
|
transport.sendMail(mail, (err, info) => {
|
|
|
|
if (err) {
|
|
|
|
log.error('Mail', err);
|
|
|
|
if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
|
|
|
|
// temporary error, try again
|
|
|
|
log.verbose('Mail', 'Retrying after %s sec. ...', tryCount);
|
|
|
|
return setTimeout(trySend, tryCount * 1000);
|
|
|
|
}
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
return callback(null, info);
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const trySendAsync = bluebird.promisify(trySend);
|
|
|
|
return await trySendAsync();
|
|
|
|
}
|
|
|
|
|
|
|
|
async function _sendTransactionalMail(transport, mail, template) {
|
2018-12-23 19:27:29 +00:00
|
|
|
const sendConfiguration = transport.mailer.sendConfiguration;
|
|
|
|
|
2018-04-29 16:13:40 +00:00
|
|
|
if (!mail.headers) {
|
|
|
|
mail.headers = {};
|
|
|
|
}
|
|
|
|
mail.headers['X-Sending-Zone'] = 'transactional';
|
|
|
|
|
2018-12-23 19:27:29 +00:00
|
|
|
mail.from = {
|
|
|
|
name: sendConfiguration.from_name,
|
|
|
|
address: sendConfiguration.from_email
|
|
|
|
};
|
|
|
|
|
2018-12-15 14:15:48 +00:00
|
|
|
const htmlRenderer = await tools.getTemplate(template.html, template.locale);
|
2018-04-29 16:13:40 +00:00
|
|
|
|
|
|
|
if (htmlRenderer) {
|
|
|
|
mail.html = htmlRenderer(template.data || {});
|
|
|
|
}
|
|
|
|
|
|
|
|
const preparedHtml = await tools.prepareHtml(mail.html);
|
|
|
|
|
2018-05-20 18:27:35 +00:00
|
|
|
if (preparedHtml) {
|
|
|
|
mail.html = preparedHtml;
|
2018-04-29 16:13:40 +00:00
|
|
|
}
|
|
|
|
|
2018-12-15 14:15:48 +00:00
|
|
|
const textRenderer = await tools.getTemplate(template.text, template.locale);
|
2018-04-29 16:13:40 +00:00
|
|
|
|
|
|
|
if (textRenderer) {
|
|
|
|
mail.text = textRenderer(template.data || {});
|
|
|
|
} else if (mail.html) {
|
|
|
|
mail.text = htmlToText.fromString(mail.html, {
|
|
|
|
wordwrap: 130
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-09-18 08:30:13 +00:00
|
|
|
return await _sendMail(transport, mail);
|
2018-04-29 16:13:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
async function _createTransport(sendConfiguration) {
|
|
|
|
const mailerSettings = sendConfiguration.mailer_settings;
|
|
|
|
const mailerType = sendConfiguration.mailer_type;
|
|
|
|
const configItems = await settings.get(contextHelpers.getAdminContext(), ['pgpPrivateKey', 'pgpPassphrase']);
|
|
|
|
|
|
|
|
const existingTransport = transports.get(sendConfiguration.id);
|
|
|
|
|
|
|
|
let existingListeners = [];
|
|
|
|
if (existingTransport) {
|
|
|
|
existingListeners = existingTransport.listeners('idle');
|
|
|
|
existingTransport.removeAllListeners('idle');
|
|
|
|
existingTransport.removeAllListeners('stream');
|
2018-09-18 08:30:13 +00:00
|
|
|
existingTransport.throttleWait = null;
|
2018-04-29 16:13:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const logFunc = (...args) => {
|
|
|
|
const level = args.shift();
|
|
|
|
args.shift();
|
|
|
|
args.unshift('Mail');
|
|
|
|
log[level](...args);
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let transportOptions;
|
|
|
|
|
2018-12-21 18:09:18 +00:00
|
|
|
if (mailerType === MailerType.GENERIC_SMTP || mailerType === MailerType.ZONE_MTA) {
|
2018-04-29 16:13:40 +00:00
|
|
|
transportOptions = {
|
|
|
|
pool: true,
|
|
|
|
debug: mailerSettings.logTransactions,
|
|
|
|
logger: mailerSettings.logTransactions ? {
|
|
|
|
debug: logFunc.bind(null, 'verbose'),
|
|
|
|
info: logFunc.bind(null, 'info'),
|
|
|
|
error: logFunc.bind(null, 'error')
|
|
|
|
} : false,
|
|
|
|
maxConnections: mailerSettings.maxConnections,
|
|
|
|
maxMessages: mailerSettings.maxMessages,
|
|
|
|
tls: {
|
|
|
|
rejectUnauthorized: !mailerSettings.allowSelfSigned
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2019-02-17 17:47:27 +00:00
|
|
|
if (mailerType === MailerType.ZONE_MTA && mailerSettings.zoneMtaType === ZoneMTAType.BUILTIN) {
|
2018-12-21 18:09:18 +00:00
|
|
|
transportOptions.host = config.builtinZoneMTA.host;
|
|
|
|
transportOptions.port = config.builtinZoneMTA.port;
|
|
|
|
transportOptions.secure = false;
|
|
|
|
transportOptions.ignoreTLS = true;
|
|
|
|
transportOptions.auth = {
|
|
|
|
user: builtinZoneMta.getUsername(),
|
|
|
|
pass: builtinZoneMta.getPassword()
|
|
|
|
};
|
|
|
|
} else {
|
|
|
|
transportOptions.host = mailerSettings.hostname;
|
|
|
|
transportOptions.port = mailerSettings.port || false;
|
|
|
|
transportOptions.secure = mailerSettings.encryption === 'TLS';
|
|
|
|
transportOptions.ignoreTLS = mailerSettings.encryption === 'NONE';
|
|
|
|
transportOptions.auth = mailerSettings.useAuth ? {
|
|
|
|
user: mailerSettings.user,
|
|
|
|
pass: mailerSettings.password
|
|
|
|
} : false;
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if (mailerType === MailerType.AWS_SES) {
|
2018-04-29 16:13:40 +00:00
|
|
|
const sendingRate = mailerSettings.throttling / 3600; // convert to messages/second
|
|
|
|
|
|
|
|
transportOptions = {
|
|
|
|
SES: new aws.SES({
|
|
|
|
apiVersion: '2010-12-01',
|
|
|
|
accessKeyId: mailerSettings.key,
|
|
|
|
secretAccessKey: mailerSettings.secret,
|
|
|
|
region: mailerSettings.region
|
|
|
|
}),
|
|
|
|
debug: mailerSettings.logTransactions,
|
|
|
|
logger: mailerSettings.logTransactions ? {
|
|
|
|
debug: logFunc.bind(null, 'verbose'),
|
|
|
|
info: logFunc.bind(null, 'info'),
|
|
|
|
error: logFunc.bind(null, 'error')
|
|
|
|
} : false,
|
|
|
|
maxConnections: mailerSettings.maxConnections,
|
|
|
|
sendingRate
|
|
|
|
};
|
|
|
|
|
|
|
|
} else {
|
|
|
|
throw new Error('Invalid mail transport');
|
|
|
|
}
|
|
|
|
|
|
|
|
const transport = nodemailer.createTransport(transportOptions, config.nodemailer);
|
|
|
|
|
|
|
|
transport.use('stream', openpgpEncrypt({
|
|
|
|
signingKey: configItems.pgpPrivateKey,
|
|
|
|
passphrase: configItems.pgpPassphrase
|
|
|
|
}));
|
|
|
|
|
|
|
|
if (existingListeners.length) {
|
|
|
|
log.info('Mail', 'Reattaching %s idle listeners', existingListeners.length);
|
|
|
|
existingListeners.forEach(listener => transport.on('idle', listener));
|
|
|
|
}
|
|
|
|
|
2018-09-18 08:30:13 +00:00
|
|
|
let throttleWait;
|
2018-04-29 16:13:40 +00:00
|
|
|
|
2018-12-21 18:09:18 +00:00
|
|
|
if (mailerType === MailerType.GENERIC_SMTP || mailerType === MailerType.ZONE_MTA) {
|
2018-04-29 16:13:40 +00:00
|
|
|
let throttling = mailerSettings.throttling;
|
|
|
|
if (throttling) {
|
|
|
|
throttling = 1 / (throttling / (3600 * 1000));
|
|
|
|
}
|
|
|
|
|
|
|
|
let lastCheck = Date.now();
|
|
|
|
|
2018-09-18 08:30:13 +00:00
|
|
|
throttleWait = function (next) {
|
2018-04-29 16:13:40 +00:00
|
|
|
if (!throttling) {
|
|
|
|
return next();
|
|
|
|
}
|
|
|
|
let nextCheck = Date.now();
|
|
|
|
let checkDiff = (nextCheck - lastCheck);
|
|
|
|
if (checkDiff < throttling) {
|
|
|
|
log.verbose('Mail', 'Throttling next message in %s sec.', (throttling - checkDiff) / 1000);
|
|
|
|
setTimeout(() => {
|
|
|
|
lastCheck = Date.now();
|
|
|
|
next();
|
|
|
|
}, throttling - checkDiff);
|
|
|
|
} else {
|
|
|
|
lastCheck = nextCheck;
|
|
|
|
next();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
} else {
|
2018-09-18 08:30:13 +00:00
|
|
|
throttleWait = next => next();
|
2018-04-29 16:13:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
transport.mailer = {
|
2018-12-16 21:35:21 +00:00
|
|
|
sendConfiguration,
|
2018-09-18 08:30:13 +00:00
|
|
|
throttleWait: bluebird.promisify(throttleWait),
|
2018-09-27 21:37:50 +00:00
|
|
|
sendTransactionalMail: async (mail, template) => await _sendTransactionalMail(transport, mail, template),
|
2018-09-18 08:30:13 +00:00
|
|
|
sendMassMail: async (mail, template) => await _sendMail(transport, mail)
|
2018-04-29 16:13:40 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
transports.set(sendConfiguration.id, transport);
|
|
|
|
return transport;
|
|
|
|
}
|
|
|
|
|
2018-09-09 22:55:44 +00:00
|
|
|
class MailerError extends Error {
|
|
|
|
constructor(msg, responseCode) {
|
|
|
|
super(msg);
|
|
|
|
this.responseCode = responseCode;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-09-02 12:59:02 +00:00
|
|
|
module.exports.getOrCreateMailer = getOrCreateMailer;
|
|
|
|
module.exports.invalidateMailer = invalidateMailer;
|
2018-09-09 22:55:44 +00:00
|
|
|
module.exports.MailerError = MailerError;
|