Added VERP support
This commit is contained in:
parent
06d5e0d9bf
commit
e5e71e0407
13 changed files with 374 additions and 148 deletions
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
103
services/verp-server.js
Normal 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);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue