mailtrain/server/lib/mailers.js

255 lines
8.7 KiB
JavaScript
Raw Normal View History

2018-04-29 16:13:40 +00:00
'use strict';
2018-09-27 19:32:35 +00:00
const log = require('./log');
const config = require('./config');
2018-04-29 16:13:40 +00:00
const nodemailer = require('nodemailer');
const aws = require('aws-sdk');
const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
const sendConfigurations = require('../models/send-configurations');
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 bluebird = require('bluebird');
const transports = new Map();
class SendConfigurationError extends Error {
constructor(sendConfigurationId, ...args) {
super(...args);
this.sendConfigurationId = sendConfigurationId;
Error.captureStackTrace(this, SendConfigurationError);
}
}
async function getOrCreateMailer(sendConfigurationId) {
let sendConfiguration;
if (!sendConfigurationId) {
sendConfiguration = await sendConfigurations.getSystemSendConfiguration();
} else {
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;
}
function invalidateMailer(sendConfigurationId) {
transports.delete(sendConfigurationId);
2018-04-29 16:13:40 +00:00
}
function _addDkimKeys(transport, mail) {
const sendConfiguration = transport.mailer.sendConfiguration;
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 = {};
}
const dkimDomain = mailerSettings.dkimDomain;
const dkimSelector = (mailerSettings.dkimSelector || '').trim();
const dkimPrivateKey = (mailerSettings.dkimPrivateKey || '').trim();
if (dkimSelector && dkimPrivateKey) {
const from = (mail.from.address || '').trim();
const domain = from.split('@').pop().toLowerCase().trim();
mail.headers['x-mailtrain-dkim'] = JSON.stringify({
domainName: dkimDomain || domain,
keySelector: dkimSelector,
privateKey: dkimPrivateKey
});
}
}
}
}
2018-04-29 16:13:40 +00:00
async function _sendMail(transport, mail, template) {
_addDkimKeys(transport, mail);
try {
return await transport.sendMailAsync(mail);
} catch (err) {
if ( (err.responseCode && err.responseCode >= 400 && err.responseCode < 500) ||
(err.code === 'ECONNECTION' && err.errno === 'ECONNREFUSED')
) {
throw new SendConfigurationError(transport.mailer.sendConfiguration.id, 'Cannot connect to service specified by send configuration ' + transport.mailer.sendConfiguration.id);
}
throw err;
}
}
async function _sendTransactionalMail(transport, mail) {
2018-04-29 16:13:40 +00:00
if (!mail.headers) {
mail.headers = {};
}
mail.headers['X-Sending-Zone'] = 'transactional';
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');
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;
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
}
};
if (mailerType === MailerType.ZONE_MTA && mailerSettings.zoneMtaType === ZoneMTAType.BUILTIN) {
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.sendMailAsync = bluebird.promisify(transport.sendMail.bind(transport));
2018-04-29 16:13:40 +00:00
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));
}
let throttleWait;
2018-04-29 16:13:40 +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();
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 {
throttleWait = next => next();
2018-04-29 16:13:40 +00:00
}
transport.mailer = {
sendConfiguration,
throttleWait: bluebird.promisify(throttleWait),
sendTransactionalMail: async (mail) => await _sendTransactionalMail(transport, mail),
sendMassMail: async (mail, template) => await _sendMail(transport, mail)
2018-04-29 16:13:40 +00:00
};
transports.set(sendConfiguration.id, transport);
return transport;
}
class MailerError extends Error {
constructor(msg, responseCode) {
super(msg);
this.responseCode = responseCode;
}
}
module.exports.getOrCreateMailer = getOrCreateMailer;
module.exports.invalidateMailer = invalidateMailer;
module.exports.MailerError = MailerError;
module.exports.SendConfigurationError = SendConfigurationError;