feat: displaying one-click unsubscribe link in email footer (#8024)

closes #8016
v1.18.x
Julian Lam 5 years ago committed by GitHub
parent c01699f706
commit df139928b6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -44,6 +44,7 @@
"notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.",
"notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.",
"notif.post.unsub.one-click": "Alternatively, unsubscribe from future emails like this, by clicking",
"notif.cta": "To the forum",
"notif.cta-new-reply": "View Post",
@ -54,6 +55,8 @@
"test.text1": "This is a test email to verify that the emailer is set up correctly for your NodeBB.",
"unsub.cta": "Click here to alter those settings",
"unsubscribe": "unsubscribe",
"unsub.success": "You will no longer receive emails from the <strong>%1</strong> mailing list",
"banned.subject": "You have been banned from %1",
"banned.text1": "The user %1 has been banned from %2.",

@ -166,33 +166,56 @@ function addSoundSettings(userData, soundsMapping) {
});
}
const unsubscribable = ['digest', 'notification'];
const jwtVerifyAsync = util.promisify(function (token, callback) {
jwt.verify(token, nconf.get('secret'), (err, payload) => callback(err, payload));
});
const doUnsubscribe = async (payload) => {
if (payload.template === 'digest') {
await Promise.all([
user.setSetting(payload.uid, 'dailyDigestFreq', 'off'),
user.updateDigestSetting(payload.uid, 'off'),
]);
} else if (payload.template === 'notification') {
const current = await db.getObjectField('user:' + payload.uid + ':settings', 'notificationType_' + payload.type);
await user.setSetting(payload.uid, 'notificationType_' + payload.type, (current === 'notificationemail' ? 'notification' : 'none'));
}
return true;
};
settingsController.unsubscribe = async function (req, res) {
if (!req.params.token) {
return res.sendStatus(404);
settingsController.unsubscribe = async (req, res) => {
let payload;
try {
payload = await jwtVerifyAsync(req.params.token);
if (!payload || !unsubscribable.includes(payload.template)) {
return;
}
} catch (err) {
throw new Error(err);
}
try {
await doUnsubscribe(payload);
res.render('unsubscribe', {
payload: payload,
});
} catch (err) {
throw new Error(err);
}
};
settingsController.unsubscribePost = async function (req, res) {
let payload;
try {
payload = await jwtVerifyAsync(req.params.token);
if (!payload || (payload.template !== 'notification' && payload.template !== 'digest')) {
if (!payload || !unsubscribable.includes(payload.template)) {
return res.sendStatus(404);
}
} catch (err) {
return res.sendStatus(403);
}
try {
if (payload.template === 'digest') {
await Promise.all([
user.setSetting(payload.uid, 'dailyDigestFreq', 'off'),
user.updateDigestSetting(payload.uid, 'off'),
]);
} else if (payload.template === 'notification') {
const current = await db.getObjectField('user:' + payload.uid + ':settings', 'notificationType_' + payload.type);
await user.setSetting(payload.uid, 'notificationType_' + payload.type, (current === 'notificationemail' ? 'notification' : 'none'));
}
await doUnsubscribe(payload);
res.sendStatus(200);
} catch (err) {
winston.error('[settings/unsubscribe] One-click unsubscribe failed with error: ' + err.message);

@ -199,11 +199,7 @@ Emailer.send = async function (template, uid, params) {
Emailer.sendToEmail = async function (template, email, language, params) {
const lang = language || meta.config.defaultLang || 'en-GB';
// Add some default email headers based on local configuration
params.headers = { 'List-Id': '<' + [template, params.uid, getHostname()].join('.') + '>',
'List-Unsubscribe': '<' + [nconf.get('url'), 'uid', params.uid, 'settings'].join('/') + '>',
...params.headers };
const unsubscribable = ['digest', 'notification'];
// Digests and notifications can be one-click unsubbed
let payload = {
@ -211,15 +207,22 @@ Emailer.sendToEmail = async function (template, email, language, params) {
uid: params.uid,
};
if (template === 'digest' || template === 'notification') {
if (unsubscribable.includes(template)) {
if (template === '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';
const unsubUrl = [nconf.get('url'), 'email', 'unsubscribe', payload].join('/');
params.headers = {
'List-Id': '<' + [template, params.uid, getHostname()].join('.') + '>',
'List-Unsubscribe': '<' + unsubUrl + '>',
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
...params.headers,
};
params.unsubUrl = unsubUrl;
}
const result = await Plugins.fireHook('filter:email.params', {

@ -32,8 +32,10 @@ function mainRoutes(app, middleware, controllers) {
setupPageRoute(app, '/reset/:code?', middleware, [middleware.delayLoading], controllers.reset);
setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse);
setupPageRoute(app, '/email/unsubscribe/:token', middleware, [], controllers.accounts.settings.unsubscribe);
app.post('/email/unsubscribe/:token', controllers.accounts.settings.unsubscribePost);
app.post('/compose', middleware.applyCSRF, controllers.composer.post);
app.post('/email/unsubscribe/:token', controllers.accounts.settings.unsubscribe);
}
function modRoutes(app, middleware, controllers) {

@ -5,6 +5,7 @@
<br><br>
<!-- IF showUnsubscribe -->
[[email:notif.post.unsub.info]] <a href="{url}/uid/{uid}/settings">[[email:unsub.cta]]</a>.
<br />[[email:notif.post.unsub.one-click]] <a href="{unsubUrl}">[[email:unsubscribe]]</a>.
<!-- ENDIF showUnsubscribe -->
<br><br>
</td>
@ -19,7 +20,7 @@
<![endif]-->
</div>
</center>
</center>
</body>
</html>
Loading…
Cancel
Save