More or less all the functionality for selectable unsubscription process. Not tested yet!

Sending emails moved completely to controller. It felt strange to have some emails sent from the controller and some of them from the model.
Confirmations refactored to an independent model that can be potentially used also for other actions that need an email confirmation.
This commit is contained in:
Tomas Bures 2017-05-03 15:46:49 -04:00
parent 32e2e61789
commit bd4961366f
13 changed files with 672 additions and 488 deletions

View file

@ -266,6 +266,11 @@ router.get('/:list/edit/:form', passport.csrfProtection, (req, res) => {
label: _('Mail - Unsubscription Confirmed (Text)'),
type: 'text',
help: helpEmailText
}, {
name: 'web_manual_unsubscribe_notice',
label: _('Web - Manual Unsubscribe Notice'),
type: 'mjml',
help: helpMjmlGeneral
}]
}
];

View file

@ -4,10 +4,8 @@ let log = require('npmlog');
let config = require('config');
let tools = require('../lib/tools');
let helpers = require('../lib/helpers');
let mailer = require('../lib/mailer');
let passport = require('../lib/passport');
let express = require('express');
let urllib = require('url');
let router = new express.Router();
let lists = require('../lib/models/lists');
let fields = require('../lib/models/fields');
@ -18,6 +16,9 @@ let _ = require('../lib/translate')._;
let util = require('util');
let cors = require('cors');
let cache = require('memory-cache');
let geoip = require('geoip-ultralight');
let confirmations = require('../lib/models/confirmations');
let mailHelpers = require('../lib/subscription-mail-helpers');
let originWhitelist = config.cors && config.cors.origins || [];
@ -45,7 +46,7 @@ let corsOrCsrfProtection = (req, res, next) => {
};
router.get('/confirm/:cid', (req, res, next) => {
subscriptions.processConfirmation(req.params.cid, req.ip, (err, subscription, action) => {
confirmations.takeConfirmation(req.params.cid, (err, confirmation) => {
if (!err && !subscription) {
err = new Error(_('Selected subscription not found'));
err.status = 404;
@ -55,7 +56,9 @@ router.get('/confirm/:cid', (req, res, next) => {
return next(err);
}
lists.get(subscription.list, (err, list) => {
const data = confirmation.data;
lists.get(confirmation.listId, (err, list) => {
if (!err && !list) {
err = new Error(_('Selected list not found'));
err.status = 404;
@ -65,23 +68,89 @@ router.get('/confirm/:cid', (req, res, next) => {
return next(err);
}
// FIXME - differentiate email based on action
const relativeUrls = {
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
};
subscriptions.sendMail(list, subscription.email, 'subscription-confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, subscription, (err) => {
if (err) {
req.flash('danger', err.message || err);
log.error('Subscription', err);
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/unsubscribe/' + encodeURIComponent(req.body.cid) + '?' + tools.queryParams(req.body));
if (confirmation.action === 'change-address') {
if (!data.subscriptionId) { // Something went terribly wrong and we don't have data that we have originally provided
return next(new Error(_('Subscriber info corrupted or missing')));
}
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
});
subscriptions.updateAddress(list.id, data.subscriptionId, data.emailNew, err => {
if (err) {
return next(err);
}
subscriptions.getById(list.id, subscriptionId, (err, subscription) => {
if (err) {
return next(err);
}
mailHelpers.sendSubscriptionConfirmed(list, data.emailNew, subscription, err => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + list.cid + '/manage-address/' + subscription.cid);
});
});
});
} else if (confirmation.action === 'subscribe') {
let optInCountry = geoip.lookupCountry(confirmation.ip) || null;
const meta = {
email: data.email,
optInIp: confirmation.ip,
optInCountry,
status: 1
};
subscriptions.insert(list.id, meta, data.subscriptionData, (err, result) => {
if (err) {
return next(err);
}
if (!result.entryId) {
return next(new Error(_('Could not save subscription')));
}
subscriptions.getById(list.id, result.entryId, (err, subscription) => {
if (err) {
return next(err);
}
mailHelpers.sendSubscriptionConfirmed(list, data.email, subscription, err => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + list.cid + '/subscribed-notice');
});
});
});
} else if (confirmation.action === 'unsubscribe') {
subscriptions.changeStatus(list.id, confirmation.data.subscriptionId, confirmation.data.campaignId, 2, (err, found) => {
if (err) {
return next(err);
}
// TODO: Shall we do anything with "found"?
subscriptions.getById(list.id, confirmation.data.subscriptionId, (err, subscription) => {
if (err) {
return next(err);
}
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + req.params.lcid + '/unsubscribed-notice');
});
});
});
}
});
});
});
@ -262,7 +331,7 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
}
// Check if the subscriber seems legit. This is a really simple check, the only requirement is that
// the subsciber has JavaScript turned on and thats it. If Mailtrain gets more targeted then this
// the subscriber has JavaScript turned on and thats it. If Mailtrain gets more targeted then this
// simple check should be replaced with an actual captcha
let subTime = Number(req.body.sub) || 0;
// allow clock skew 24h in the past and 24h to the future
@ -285,39 +354,67 @@ router.post('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, (req, r
return req.xhr ? sendJsonError(err) : next(err);
}
let data = {};
let subscriptionData = {};
Object.keys(req.body).forEach(key => {
if (key !== 'email' && key.charAt(0) !== '_') {
data[key] = (req.body[key] || '').toString().trim();
subscriptionData[key] = (req.body[key] || '').toString().trim();
}
});
subscriptionData = tools.convertKeys(data);
data = tools.convertKeys(data);
data._address = req.body.address;
data._sub = req.body.sub;
data._skip = !testsPass;
data._action = 'subscribe';
subscriptions.addConfirmation(list, email, req.ip, data, (err, confirmCid) => {
if (!err && !confirmCid) {
err = new Error(_('Could not store confirmation data'));
}
subscriptions.getByEmail(list.id, email, (err, subscription) => {
if (err) {
if (req.xhr) {
return sendJsonError(err);
}
req.flash('danger', err.message || err);
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
return req.xhr ? sendJsonError(err) : next(err);
}
if (req.xhr) {
return res.status(200).json({
msg: _('Please Confirm Subscription')
if (subscription) {
mailHelpers.sendAlreadySubscribed(list, email, subscription, (err) => {
if (err) {
return req.xhr ? sendJsonError(err) : next(err);
}
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
});
} else {
const data = {
email,
subscriptionData
};
confirmations.addConfirmation(list.id, 'subscribe', req.ip, data, (err, confirmCid) => {
if (!err && !confirmCid) {
err = new Error(_('Could not store confirmation data'));
}
if (err) {
if (req.xhr) {
return sendJsonError(err);
}
req.flash('danger', err.message || err);
return res.redirect('/subscription/' + encodeURIComponent(req.params.cid) + '?' + tools.queryParams(req.body));
}
function sendWebResponse() {
if (req.xhr) {
return res.status(200).json({
msg: _('Please Confirm Subscription')
});
}
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
}
if (!testsPass) {
log.info('Subscription', 'Confirmation message for %s marked to be skipped (%s)', email, JSON.stringify(data));
sendWebResponse();
} else {
sendConfirmSubscription(list, email, confirmCid, data, (err) => {
if (err) {
return req.xhr ? sendJsonError(err) : sendWebResponse(err);
}
sendWebResponse();
})
}
});
}
res.redirect('/subscription/' + req.params.cid + '/confirm-subscription-notice');
});
});
});
@ -492,15 +589,41 @@ router.post('/:lcid/manage-address', passport.parseForm, passport.csrfProtection
return next(err);
}
subscriptions.updateAddress(list, req.body.cid, req.body, req.ip, err => {
const emailNew = (req.body.emailNew || '').toString().trim();
subscriptions.updateAddressCheck(list, req.body.cid, emailNew, req.ip, (err, subscription, newEmailAvailable) => {
if (err) {
req.flash('danger', err.message || err);
log.error('Subscription', err);
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/manage-address/' + encodeURIComponent(req.body.cid) + '?' + tools.queryParams(req.body));
}
req.flash('info', _('An email with further instructions has been sent to the provided address'));
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
function sendWebResponse(err) {
if (err) {
return next(err);
}
req.flash('info', _('An email with further instructions has been sent to the provided address'));
res.redirect('/subscription/' + req.params.lcid + '/manage/' + req.body.cid);
}
if (newEmailAvailable) {
const data = {
subscriptionId: subscription.id,
emailNew
};
confirmations.addConfirmation(list.id, 'change-address', req.ip, data, err => {
if (err) {
return next(err);
}
mailHelpers.sendConfirmAddressChange(list, emailNew, subscription, sendWebResponse);
});
} else {
mailHelpers.sendAlreadySubscribed(list, emailNew, subscription, sendWebResponse);
}
});
});
});
@ -525,7 +648,7 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
subscriptions.get(list.id, req.params.ucid, (err, subscription) => {
if (!err && !subscription) {
err = new Error(_('Subscription not found from this list'));
err = new Error(_('Subscription not found in this list'));
err.status = 404;
}
@ -533,40 +656,47 @@ router.get('/:lcid/unsubscribe/:ucid', passport.csrfProtection, (req, res, next)
return next(err);
}
subscription.lcid = req.params.lcid;
subscription.ucid = req.params.ucid;
subscription.title = list.name;
subscription.csrfToken = req.csrfToken();
subscription.campaign = req.query.c;
subscription.defaultAddress = configItems.defaultAddress;
subscription.defaultPostaddress = configItems.defaultPostaddress;
subscription.template = {
template: 'subscription/web-unsubscribe.mjml.hbs',
layout: 'subscription/layout.mjml.hbs'
};
if (list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM ||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe', subscription, (err, data) => {
if (err) {
return next(err);
}
subscription.lcid = req.params.lcid;
subscription.ucid = req.params.ucid;
subscription.title = list.name;
subscription.csrfToken = req.csrfToken();
subscription.campaign = req.query.c;
subscription.defaultAddress = configItems.defaultAddress;
subscription.defaultPostaddress = configItems.defaultPostaddress;
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
subscription.template = {
template: 'subscription/web-unsubscribe.mjml.hbs',
layout: 'subscription/layout.mjml.hbs'
};
helpers.injectCustomFormData(req.query.fid || list.defaultForm, 'subscription/web-unsubscribe', subscription, (err, data) => {
if (err) {
return next(err);
}
helpers.captureFlashMessages(req, res, (err, flash) => {
helpers.getMjmlTemplate(data.template, (err, htmlRenderer) => {
if (err) {
return next(err);
}
data.isWeb = true;
data.flashMessages = flash;
res.send(htmlRenderer(data));
helpers.captureFlashMessages(req, res, (err, flash) => {
if (err) {
return next(err);
}
data.isWeb = true;
data.flashMessages = flash;
res.send(htmlRenderer(data));
});
});
});
});
} else { // UnsubscriptionMode.ONE_STEP || UnsubscriptionMode.TWO_STEP || UnsubscriptionMode.MANUAL
handleUnsubscribe(list, subscription, res);
}
});
});
});
@ -583,47 +713,95 @@ router.post('/:lcid/unsubscribe', passport.parseForm, passport.csrfProtection, (
return next(err);
}
subscriptions.unsubscribe(list.id, req.body.ucid, req.body.campaign, (err, subscription) => {
if (err) {
req.flash('danger', err.message || err);
log.error('Subscription', err);
return res.redirect('/subscription/' + encodeURIComponent(req.params.lcid) + '/unsubscribe/' + encodeURIComponent(req.body.ucid) + '?' + tools.queryParams(req.body));
const campaignId = (req.body.campaign || '').toString().trim() || false;
subscriptions.get(list.id, req.body.ucid, (err, subscription) => {
if (!err && !subscription) {
err = new Error(_('Subscription not found in this list'));
err.status = 404;
}
const relativeUrls = {
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
};
subscriptions.sendMail(list, subscription.email, 'unsubscription-confirmed', _('%s: Unsubscribe Confirmed'), relativeUrls, {}, subscription, (err) => {
if (err) {
req.flash('danger', err.message || err);
log.error('Subscription', err);
return res.redirect('/subscription/' + encodeURIComponent(list.cid) + '/unsubscribe/' + encodeURIComponent(subscription.cid) + '?' + tools.queryParams(req.body));
}
if (err) {
return next(err);
}
res.redirect('/subscription/' + req.params.lcid + '/unsubscribed-notice');
});
handleUnsubscribe(list, subscription, res);
});
});
});
function handleUnsubscribe(list, subscription, res) {
if (list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP ||
list.unsubscriptionMode === lists.UnsubscriptionMode.TWO_STEP_WITH_FORM) {
const data = {
subscriptionId: subscription.id,
campaignId
};
confirmations.addConfirmation(list.id, 'unsubscribe', req.ip, data, (err, confirmCid) => {
if (!err && !confirmCid) {
err = new Error(_('Could not store confirmation data'));
}
if (err) {
return next(err);
}
mailHelpers.sendConfirmUnsubscription(list, subscription.email, subscription, err => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
});
});
} else if (list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP ||
list.unsubscriptionMode === lists.UnsubscriptionMode.ONE_STEP_WITH_FORM) {
subscriptions.changeStatus(subscription.id, list.id, campaignId, 2, (err, found) => {
if (err) {
return next(err);
}
// TODO: Shall we do anything with "found"?
mailHelpers.sendUnsubscriptionConfirmed(list, subscription.email, subscription, err => {
if (err) {
return next(err);
}
res.redirect('/subscription/' + list.cid + '/unsubscribed-notice');
});
});
} else { // UnsubscriptionMode.MANUAL
res.redirect('/subscription/' + list.cid + '/manual-unsubscribe-notice');
}
}
router.get('/:cid/confirm-subscription-notice', (req, res, next) => {
notice('confirm-subscription', req, res, next);
webNotice('confirm-subscription', req, res, next);
});
router.get('/:cid/confirm-unsubscription-notice', (req, res, next) => {
notice('confirm-unsubscription', req, res, next);
webNotice('confirm-unsubscription', req, res, next);
});
router.get('/:cid/subscribed-notice', (req, res, next) => {
notice('subscribed', req, res, next);
webNotice('subscribed', req, res, next);
});
router.get('/:cid/updated-notice', (req, res, next) => {
notice('updated', req, res, next);
webNotice('updated', req, res, next);
});
router.get('/:cid/unsubscribed-notice', (req, res, next) => {
notice('unsubscribed', req, res, next);
webNotice('unsubscribed', req, res, next);
});
router.get('/:cid/manual-unsubscribe-notice', (req, res, next) => {
webNotice('manual-unsubscribe', req, res, next);
});
router.post('/publickey', passport.parseForm, (req, res, next) => {
@ -665,7 +843,7 @@ router.post('/publickey', passport.parseForm, (req, res, next) => {
});
function notice(type, req, res, next) {
function webNotice(type, req, res, next) {
lists.getByCid(req.params.cid, (err, list) => {
if (!err && !list) {
err = new Error(_('Selected list not found'));
@ -676,7 +854,7 @@ function notice(type, req, res, next) {
return next(err);
}
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress'], (err, configItems) => {
settings.list(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail'], (err, configItems) => {
if (err) {
return next(err);
}
@ -686,6 +864,7 @@ function notice(type, req, res, next) {
homepage: configItems.defaultHomepage || configItems.serviceUrl,
defaultAddress: configItems.defaultAddress,
defaultPostaddress: configItems.defaultPostaddress,
contactAddress: configItems.defaultAddress,
template: {
template: 'subscription/web-' + type + '-notice.mjml.hbs',
layout: 'subscription/layout.mjml.hbs'
@ -718,5 +897,4 @@ function notice(type, req, res, next) {
});
}
module.exports = router;