Built-in Zone MTA

Plugin for ZoneMTA for per-message DKIM keys.
This commit is contained in:
Tomas Bures 2018-12-16 22:35:21 +01:00
parent d103a2cc79
commit 77c64f487d
18 changed files with 231 additions and 110 deletions

View file

@ -215,6 +215,9 @@ testServer:
password: testpass
logger: false
builtinZoneMTA:
enabled: true
seleniumWebDriver:
browser: phantomjs

View file

@ -23,4 +23,6 @@ passwordresetlink="xxx"
[reports]
enabled=true
[redis]
enabled=true
enabled=true
[log]
level="verbose"

View file

@ -21,6 +21,7 @@ const privilegeHelpers = require('./lib/privilege-helpers');
const knex = require('./lib/knex');
const shares = require('./models/shares');
const { AppType } = require('../shared/app');
const builtinZoneMta = require('./lib/builtin-zone-mta');
const trustedPort = config.www.trustedPort;
const sandboxPort = config.www.sandboxPort;
@ -94,29 +95,31 @@ dbcheck(err => { // Check if database needs upgrading before starting the server
.then(() =>
executor.spawn(() =>
testServer(() =>
verpServer(() =>
startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () =>
startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () =>
startHTTPServer(AppType.PUBLIC, 'public', publicPort, () => {
privilegeHelpers.dropRootPrivileges();
verpServer(() =>
builtinZoneMta.spawn(() =>
startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () =>
startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () =>
startHTTPServer(AppType.PUBLIC, 'public', publicPort, () => {
privilegeHelpers.dropRootPrivileges();
tzupdate.start();
tzupdate.start();
importer.spawn(() =>
feedcheck.spawn(() =>
senders.spawn(() => {
triggers.start();
gdprCleanup.start();
importer.spawn(() =>
feedcheck.spawn(() =>
senders.spawn(() => {
triggers.start();
gdprCleanup.start();
postfixBounceServer(async () => {
await reportProcessor.init();
log.info('Service', 'All services started');
appBuilder.setReady();
});
})
)
);
})
postfixBounceServer(async () => {
await reportProcessor.init();
log.info('Service', 'All services started');
appBuilder.setReady();
});
})
)
);
})
)
)
)
)

View file

@ -0,0 +1,45 @@
'use strict';
const config = require('config');
const fork = require('child_process').fork;
const log = require('./log');
const path = require('path');
let zoneMtaProcess;
module.exports = {
spawn
};
function spawn(callback) {
if (config.builtinZoneMTA.enabled) {
log.info('ZoneMTA', 'Starting built-in Zone MTA process');
zoneMtaProcess = fork(
path.join(__dirname, '..', '..', 'zone-mta', 'index.js'),
['--config=' + path.join(__dirname, '..', '..', 'zone-mta', 'config', 'zonemta.js')],
{
cwd: path.join(__dirname, '..', '..', 'zone-mta'),
env: {NODE_ENV: process.env.NODE_ENV}
}
);
zoneMtaProcess.on('message', msg => {
if (msg) {
if (msg.type === 'zone-mta-started') {
log.info('ZoneMTA', 'ZoneMTA process started');
return callback();
} else if (msg.type === 'entries-added') {
senders.scheduleCheck();
}
}
});
zoneMtaProcess.on('close', (code, signal) => {
log.error('ZoneMTA', 'ZoneMTA process exited with code %s signal %s', code, signal);
});
} else {
callback();
}
}

View file

@ -328,9 +328,9 @@ class CampaignSender {
const getOverridable = key => {
if (sendConfiguration[key + '_overridable'] && this.campaign[key + '_override'] !== null) {
return campaign[key + '_override'];
return campaign[key + '_override'] || '';
} else {
return sendConfiguration[key];
return sendConfiguration[key] || '';
}
}

View file

@ -22,7 +22,7 @@ const transports = new Map();
async function getOrCreateMailer(sendConfigurationId) {
let sendConfiguration;
if (!sendConfiguration) {
if (!sendConfigurationId) {
sendConfiguration = await sendConfigurations.getSystemSendConfiguration();
} else {
sendConfiguration = await sendConfigurations.getById(contextHelpers.getAdminContext(), sendConfigurationId, false, true);
@ -38,8 +38,35 @@ function invalidateMailer(sendConfigurationId) {
function _addDkimKeys(transport, mail) {
const sendConfiguration = transport.mailer.sendConfiguration;
if (sendConfiguration.mailer_type === sendConfigurations.MailerType.ZONE_MTA) {
if (!mail.headers) {
mail.headers = {};
}
const dkimDomain = sendConfiguration.mailer_settings.dkimDomain;
const dkimSelector = (sendConfiguration.mailer_settings.dkimSelector || '').trim();
const dkimPrivateKey = (sendConfiguration.mailer_settings.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
});
}
}
}
async function _sendMail(transport, mail, template) {
_addDkimKeys(transport, mail);
let tryCount = 0;
const trySend = (callback) => {
tryCount++;
@ -209,6 +236,7 @@ async function _createTransport(sendConfiguration) {
}
transport.mailer = {
sendConfiguration,
throttleWait: bluebird.promisify(throttleWait),
sendTransactionalMail: async (mail, template) => await _sendTransactionalMail(transport, mail, template),
sendMassMail: async (mail, template) => await _sendMail(transport, mail)

View file

@ -666,46 +666,33 @@ async function getMessageByCid(messageCid) {
const [campaignCid, listCid, subscriptionCid] = messageCidElems;
await knex.transaction(async tx => {
return await knex.transaction(async tx => {
const list = await tx('lists').where('cid', listCid).select('id');
const subscrTblName = subscriptions.getSubscriptionTableName(list.id);
const message = await tx('campaign_messages')
.innerJoin('campaigns', 'campaign_messages.campaign', 'campaigns.id')
.innerJoin(subscrTblName, subscrTblName + '.id', 'campaign_messages.subscription')
.leftJoin('segments', 'segment.id', 'campaign_messages.segment') // This is just to make sure that the respective segment still exists or return null if it doesn't
.leftJoin('send_configurations', 'send_configurations.id', 'campaign_messages.send_configuration') // This is just to make sure that the respective send_configuration still exists or return null if it doesn't
.where(subscrTblName + '.cid', subscriptionCid)
.where('campaigns.cid', campaignCid)
.select([
'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'segments.id AS segment', 'campaign_messages.subscription',
'send_configurations.id AS send_configuration', 'campaign_messages.status', 'campaign_messages.response', 'campaign_messages.response_id',
'campaign_messages.updated', 'campaign_messages.created', 'send_configurations.verp_hostname AS verp_hostname'
]);
if (message) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', message.campaign, 'manageMessages');
}
'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.status'
])
.first();
return message;
});
}
async function getMessageByResponseId(responseId) {
await knex.transaction(async tx => {
return await knex.transaction(async tx => {
console.log(responseId);
const message = await tx('campaign_messages')
.leftJoin('segments', 'segment.id', 'campaign_messages.segment') // This is just to make sure that the respective segment still exists or return null if it doesn't
.leftJoin('send_configurations', 'send_configurations.id', 'campaign_messages.send_configuration') // This is just to make sure that the respective send_configuration still exists or return null if it doesn't
.where('campaign_messages.response_id', responseId)
.select([
'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'segments.id AS segment', 'campaign_messages.subscription',
'send_configurations.id AS send_configuration', 'campaign_messages.status', 'campaign_messages.response', 'campaign_messages.response_id',
'campaign_messages.updated', 'campaign_messages.created', 'send_configurations.verp_hostname AS verp_hostname'
]);
if (message) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', message.campaign, 'manageMessages');
}
'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.status'
])
.first();
return message;
});
@ -763,6 +750,7 @@ async function changeStatusByMessage(context, message, subscriptionStatus, updat
}
await _changeStatusByMessageTx(tx, context, message, subscriptionStatus);
});
}

View file

@ -180,22 +180,28 @@ router.postAsync('/mailgun', uploads.any(), async (req, res) => {
router.postAsync('/zone-mta', async (req, res) => {
if (typeof req.body === 'string') {
req.body = JSON.parse(req.body);
}
if (req.body.id) {
const message = await campaigns.getMessageByCid(req.body.id);
if (message) {
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
try {
if (typeof req.body === 'string') {
req.body = JSON.parse(req.body);
}
}
res.json({
success: true
});
if (req.body.id) {
const message = await campaigns.getMessageByResponseId(req.body.id);
console.log(message);
if (message) {
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
}
}
res.json({
success: true
});
} catch (err) {
console.log(err);
throw err;
}
});

View file

@ -164,7 +164,7 @@ async function processCampaign(campaignId) {
}
} catch (err) {
log.error('Senders', `Sending campaign ${campaignId} failed with error: ${err.message}`)
log.verbose(err);
log.verbose(err.stack);
}
}
@ -204,7 +204,7 @@ async function scheduleCampaigns() {
}
} catch (err) {
log.error('Senders', `Scheduling campaigns failed with error: ${err.message}`)
log.verbose(err);
log.verbose(err.stack);
}
@ -249,7 +249,7 @@ async function processQueued() {
}
} catch (err) {
log.error('Senders', `Processing queued messages failed with error: ${err.message}`)
log.verbose(err);
log.verbose(err.stack);
}
queuedSchedulerRunning = false;
@ -326,7 +326,7 @@ async function init() {
scheduleCampaigns();
} else if (type === 'reload-config') {
for (const worker of workerProcesses.keys()) {
for (const workerId of workerProcesses.keys()) {
sendToWorker(workerId, 'reload-config', msg.data);
}
}

View file

@ -35,7 +35,7 @@ async function processMessages(campaignId, subscribers) {
log.verbose('Senders', 'Message sent and status updated for %s:%s', subData.listId, subData.email || subData.subscriptionId);
} catch (err) {
log.error('Senders', `Sending message to ${subData.listId}:${subData.email} failed with error: ${err.message}`)
log.verbose(err);
log.verbose(err.stack);
}
}

View file

@ -4,7 +4,7 @@ const contextHelpers = require('../../../lib/context-helpers');
const mosaicoTemplates = require('../../../../shared/mosaico-templates');
const {getGlobalNamespaceId} = require('../../../../shared/namespaces');
const {getAdminId} = require('../../../../shared/users');
const { MailerType, getSystemSendConfigurationId, getSystemSendConfigurationCid } = require('../../../../shared/send-configurations');
const { MailerType, ZoneMTAType, getSystemSendConfigurationId, getSystemSendConfigurationCid } = require('../../../../shared/send-configurations');
const { enforce } = require('../../../lib/helpers');
const { EntityVals: TriggerEntityVals, EventVals: TriggerEventVals } = require('../../../../shared/triggers');
const { SubscriptionSource } = require('../../../../shared/lists');
@ -753,6 +753,7 @@ async function migrateSettings(knex) {
if (settings.dkimApiKey) {
mailer_type = MailerType.ZONE_MTA;
mailer_settings.dkimApiKey = settings.dkimApiKey;
mailer_settings.zoneMtaType = ZoneMTAType.WITH_HTTP_CONF;
mailer_settings.dkimDomain = settings.dkimDomain;
mailer_settings.dkimSelector = settings.dkimSelector;
mailer_settings.dkimPrivateKey = settings.dkimPrivateKey;