Added VERP support

This commit is contained in:
Andris Reinman 2016-04-10 20:26:20 -07:00
parent 06d5e0d9bf
commit e5e71e0407
13 changed files with 374 additions and 148 deletions

View file

@ -1,7 +1,7 @@
'use strict';
let log = require('npmlog');
let config = require('config');
let db = require('../lib/db');
let tools = require('../lib/tools');
let mailer = require('../lib/mailer');
@ -111,11 +111,13 @@ function formatMessage(message, callback) {
return callback(new Error('List not found'));
}
settings.get('serviceUrl', (err, serviceUrl) => {
settings.list(['serviceUrl', 'verpUse', 'verpHostname'], (err, configItems) => {
if (err) {
return callback(err);
}
let useVerp = config.verp.enabled && configItems.verpUse && configItems.verpHostname;
fields.list(list.id, (err, fieldList) => {
if (err) {
return callback(err);
@ -141,7 +143,7 @@ function formatMessage(message, callback) {
}
});
links.updateLinks(campaign, list, message.subscription, serviceUrl, campaign.html, (err, html) => {
links.updateLinks(campaign, list, message.subscription, configItems.serviceUrl, campaign.html, (err, html) => {
if (err) {
return callback(err);
}
@ -157,43 +159,52 @@ function formatMessage(message, callback) {
return prefix + 'cid:' + cid;
});
let campaignAddress = [campaign.cid, list.cid, message.subscription.cid].join('.');
return callback(null, {
from: {
name: campaign.from,
address: campaign.address
},
xMailer: 'Mailtrain Mailer (+http://mailtrain.org)',
xMailer: 'Mailtrain Mailer (+https://mailtrain.org)',
to: {
name: [].concat(message.subscription.firstName || []).concat(message.subscription.lastName || []).join(' '),
address: message.subscription.email
},
sender: useVerp ? campaignAddress + '@' + configItems.verpHostname : false,
envelope: useVerp ? {
from: campaignAddress + '@' + configItems.verpHostname,
to: message.subscription.email
} : false,
headers: {
'x-fbl': [campaign.cid, list.cid, message.subscription.cid].join('.'),
'x-fbl': campaignAddress,
// custom header for SparkPost
'x-msys-api': JSON.stringify({
campaign_id: [campaign.cid, list.cid, message.subscription.cid].join('.')
campaign_id: campaignAddress
}),
// custom header for SendGrid
'x-smtpapi': JSON.stringify({
unique_args: {
campaign_id: [campaign.cid, list.cid, message.subscription.cid].join('.')
campaign_id: campaignAddress
}
}),
// custom header for Mailgun
'x-mailgun-variables': JSON.stringify({
campaign_id: [campaign.cid, list.cid, message.subscription.cid].join('.')
campaign_id: campaignAddress
}),
'List-ID': {
prepared: true,
value: '"' + list.name.replace(/[^a-z0-9\s'.,\-]/g, '').trim() + '" <' + list.cid + '.' + (url.parse(serviceUrl).hostname || 'localhost') + '>'
value: '"' + list.name.replace(/[^a-z0-9\s'.,\-]/g, '').trim() + '" <' + list.cid + '.' + (url.parse(configItems.serviceUrl).hostname || 'localhost') + '>'
}
},
list: {
unsubscribe: url.resolve(serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + message.subscription.cid + '?auto=yes')
unsubscribe: url.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + message.subscription.cid + '?auto=yes')
},
subject: tools.formatMessage(serviceUrl, campaign, list, message.subscription, campaign.subject),
html: tools.formatMessage(serviceUrl, campaign, list, message.subscription, html),
text: tools.formatMessage(serviceUrl, campaign, list, message.subscription, campaign.text),
subject: tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, campaign.subject),
html: tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, html),
text: tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, campaign.text),
attachments
});

View file

@ -4,7 +4,6 @@ let log = require('npmlog');
let config = require('config');
let crypto = require('crypto');
// Replace '../lib/smtp-server' with 'smtp-server' when running this script outside this directory
let SMTPServer = require('smtp-server').SMTPServer;
// Setup server
@ -98,7 +97,7 @@ server.on('error', err => {
});
if (config.testserver.enabled) {
server.listen(config.testserver.port, () => {
server.listen(config.testserver.port, config.testserver.host, () => {
log.info('TESTSERV', 'Server listening on port %s', config.testserver.port);
});
}

103
services/verp-server.js Normal file
View file

@ -0,0 +1,103 @@
'use strict';
let log = require('npmlog');
let config = require('config');
let settings = require('../lib/models/settings');
let campaigns = require('../lib/models/campaigns');
let BounceHandler = require('bounce-handler').BounceHandler;
let SMTPServer = require('smtp-server').SMTPServer;
// Setup server
let server = new SMTPServer({
// log to console
logger: false,
banner: 'Mailtrain VERP bouncer',
disabledCommands: ['AUTH', 'STARTTLS'],
onRcptTo: (address, session, callback) => {
settings.list(['verpHostname'], (err, configItems) => {
if (err) {
err = new Error('Failed to load configuration');
err.responseCode = 421;
return callback(err);
}
let user = address.address.split('@').shift();
let host = address.address.split('@').pop();
if (host !== configItems.verpHostname || !/^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i.test(user)) {
err = new Error('Unknown user ' + address.address);
err.responseCode = 510;
return callback(err);
}
campaigns.findMailByCampaign(user, (err, message) => {
if (err) {
err = new Error('Failed to load user data');
err.responseCode = 421;
return callback(err);
}
if (!message) {
err = new Error('Unknown user ' + address.address);
err.responseCode = 510;
return callback(err);
}
session.campaignId = user;
session.message = message;
log.verbose('VERP', 'Incoming message for Campaign %s, List %s, Subscription %s', message.campaign, message.list, message.subscription);
callback();
});
});
},
// Handle message stream
onData: (stream, session, callback) => {
let chunks = [];
let chunklen = 0;
stream.on('data', chunk => {
if (!chunk || !chunk.length || chunklen > 60 * 1024) {
return;
}
chunks.push(chunk);
chunklen += chunk.length;
});
stream.on('end', () => {
let body = Buffer.concat(chunks, chunklen).toString();
let bh = new BounceHandler();
let bounceResult = [].concat(bh.parse_email(body) || []).shift();
if (!bounceResult || ['failed', 'transient'].indexOf(bounceResult.action) < 0) {
return callback(null, 'Message accepted');
} else {
campaigns.updateMessage(session.message, 'bounced', bounceResult.action === 'failed', (err, updated) => {
if (err) {
log.error('VERP', 'Failed updating message: %s', err.stack);
} else if (updated) {
log.verbose('VERP', 'Marked message %s as unsubscribed', session.campaignId);
}
callback(null, 'Message accepted');
});
}
});
}
});
server.on('error', err => {
log.error('VERP', err.stack);
});
if (config.verp.enabled) {
server.listen(config.verp.port, () => {
log.info('VERP', 'Server listening on port %s', config.verp.port);
});
}