diff --git a/public/language/en-GB/email.json b/public/language/en-GB/email.json
index 5a51ef2230..cb2fd70dfa 100644
--- a/public/language/en-GB/email.json
+++ b/public/language/en-GB/email.json
@@ -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 %1 mailing list",
"banned.subject": "You have been banned from %1",
"banned.text1": "The user %1 has been banned from %2.",
diff --git a/src/controllers/accounts/settings.js b/src/controllers/accounts/settings.js
index 55cc3736c4..e0d7d2fc2c 100644
--- a/src/controllers/accounts/settings.js
+++ b/src/controllers/accounts/settings.js
@@ -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));
});
-
-settingsController.unsubscribe = async function (req, res) {
- if (!req.params.token) {
- return res.sendStatus(404);
+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 (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;
+ }
+ } 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 || !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);
diff --git a/src/emailer.js b/src/emailer.js
index 9c9b729841..2f7bb1e147 100644
--- a/src/emailer.js
+++ b/src/emailer.js
@@ -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', {
diff --git a/src/routes/index.js b/src/routes/index.js
index 561ae8ee1f..fcab003624 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -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) {
diff --git a/src/views/emails/partials/footer.tpl b/src/views/emails/partials/footer.tpl
index 13b350ef23..98fb2824e4 100644
--- a/src/views/emails/partials/footer.tpl
+++ b/src/views/emails/partials/footer.tpl
@@ -5,6 +5,7 @@
[[email:notif.post.unsub.info]] [[email:unsub.cta]].
+
[[email:notif.post.unsub.one-click]] [[email:unsubscribe]].
@@ -19,7 +20,7 @@
-
+