feat: support for one-click unsubscribe from email clients (#7203)

* feat: sending notifs via ACP creates real notification

re: #7202

* feat: basic integration for one-click unsubscription #7202

* feat: tests for #7202 + bugfix

* feat: added and organized digest unsub tests

closes #7202
This commit is contained in:
Julian Lam
2019-01-08 13:56:11 -05:00
committed by GitHub
parent 29acad4247
commit 70a87d4399
6 changed files with 210 additions and 0 deletions

View File

@@ -1,7 +1,10 @@
'use strict';
var async = require('async');
var nconf = require('nconf');
var winston = require('winston');
var _ = require('lodash');
var jwt = require('jsonwebtoken');
var user = require('../../user');
var languages = require('../../languages');
@@ -183,6 +186,52 @@ settingsController.get = function (req, res, callback) {
], callback);
};
settingsController.unsubscribe = function (req, res) {
if (!req.params.token) {
return res.sendStatus(404);
}
jwt.verify(req.params.token, nconf.get('secret'), function (err, payload) {
if (err) {
return res.sendStatus(403);
}
switch (payload.template) {
case 'digest':
async.parallel([
async.apply(user.setSetting, payload.uid, 'dailyDigestFreq', 'off'),
async.apply(user.updateDigestSetting, payload.uid, 'off'),
], function (err) {
if (err) {
winston.error('[settings/unsubscribe] One-click unsubscribe failed with error: ' + err.message);
return res.sendStatus(500);
}
return res.sendStatus(200);
});
break;
case 'notification':
async.waterfall([
async.apply(db.getObjectField, 'user:' + payload.uid + ':settings', 'notificationType_' + payload.type),
(current, next) => {
user.setSetting(payload.uid, 'notificationType_' + payload.type, (current === 'notificationemail' ? 'notification' : 'none'), next);
},
], function (err) {
if (err) {
winston.error('[settings/unsubscribe] One-click unsubscribe failed with error: ' + err.message);
return res.sendStatus(500);
}
return res.sendStatus(200);
});
break;
default:
res.sendStatus(404);
break;
}
});
};
function getNotificationSettings(userData, callback) {
var privilegedTypes = [];

View File

@@ -11,6 +11,7 @@ var url = require('url');
var path = require('path');
var fs = require('fs');
var _ = require('lodash');
var jwt = require('jsonwebtoken');
var User = require('./user');
var Plugins = require('./plugins');
@@ -216,6 +217,31 @@ Emailer.sendToEmail = function (template, email, language, params, callback) {
'List-Unsubscribe': '<' + [nconf.get('url'), 'uid', params.uid, 'settings'].join('/') + '>',
}, params.headers);
// Digests and notifications can be one-click unsubbed
let payload = {
template: template,
uid: params.uid,
};
switch (template) {
case 'digest':
payload = jwt.sign(payload, nconf.get('secret'), {
expiresIn: '30d',
});
params.headers['List-Unsubscribe'] = '<' + [nconf.get('url'), 'email', 'unsubscribe', payload].join('/') + '>';
params.headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
break;
case 'notification':
payload.type = params.notification.type;
payload = jwt.sign(payload, nconf.get('secret'), {
expiresIn: '30d',
});
params.headers['List-Unsubscribe'] = '<' + [nconf.get('url'), 'email', 'unsubscribe', payload].join('/') + '>';
params.headers['List-Unsubscribe-Post'] = 'List-Unsubscribe=One-Click';
break;
}
async.waterfall([
function (next) {
Plugins.fireHook('filter:email.params', {

View File

@@ -35,6 +35,7 @@ function mainRoutes(app, middleware, controllers) {
setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse);
app.post('/compose', middleware.applyCSRF, controllers.composer.post);
app.post('/email/unsubscribe/:token', controllers.accounts.settings.unsubscribe);
}
function modRoutes(app, middleware, controllers) {

View File

@@ -14,6 +14,7 @@ var userDigest = require('../user/digest');
var userEmail = require('../user/email');
var logger = require('../logger');
var events = require('../events');
var notifications = require('../notifications');
var emailer = require('../emailer');
var db = require('../database');
var analytics = require('../analytics');
@@ -273,6 +274,30 @@ SocketAdmin.email.test = function (socket, data, callback) {
}, callback);
break;
case 'notification':
async.waterfall([
function (next) {
notifications.create({
type: 'test',
bodyShort: '[[admin-settings-email:testing]]',
bodyLong: '[[admin-settings-email:testing.send-help]]',
nid: 'uid:' + socket.uid + ':test',
path: '/',
from: socket.uid,
}, next);
},
function (notifObj, next) {
emailer.send('notification', socket.uid, {
path: notifObj.path,
subject: utils.stripHTMLTags(notifObj.subject || '[[notifications:new_notification]]'),
intro: utils.stripHTMLTags(notifObj.bodyShort),
body: notifObj.bodyLong || '',
notification: notifObj,
showUnsubscribe: true,
}, next);
},
]);
break;
default:
emailer.send(data.template, socket.uid, payload, callback);
break;