mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-11 16:35:47 +01:00
feat: displaying one-click unsubscribe link in email footer (#8024)
closes #8016
This commit is contained in:
@@ -44,6 +44,7 @@
|
|||||||
"notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.",
|
"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.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": "To the forum",
|
||||||
"notif.cta-new-reply": "View Post",
|
"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.",
|
"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",
|
"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.subject": "You have been banned from %1",
|
||||||
"banned.text1": "The user %1 has been banned from %2.",
|
"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) {
|
const jwtVerifyAsync = util.promisify(function (token, callback) {
|
||||||
jwt.verify(token, nconf.get('secret'), (err, payload) => callback(err, payload));
|
jwt.verify(token, nconf.get('secret'), (err, payload) => callback(err, payload));
|
||||||
});
|
});
|
||||||
|
const doUnsubscribe = async (payload) => {
|
||||||
settingsController.unsubscribe = async function (req, res) {
|
if (payload.template === 'digest') {
|
||||||
if (!req.params.token) {
|
await Promise.all([
|
||||||
return res.sendStatus(404);
|
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;
|
let payload;
|
||||||
try {
|
try {
|
||||||
payload = await jwtVerifyAsync(req.params.token);
|
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);
|
return res.sendStatus(404);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res.sendStatus(403);
|
return res.sendStatus(403);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (payload.template === 'digest') {
|
await doUnsubscribe(payload);
|
||||||
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'));
|
|
||||||
}
|
|
||||||
res.sendStatus(200);
|
res.sendStatus(200);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
winston.error('[settings/unsubscribe] One-click unsubscribe failed with error: ' + err.message);
|
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) {
|
Emailer.sendToEmail = async function (template, email, language, params) {
|
||||||
const lang = language || meta.config.defaultLang || 'en-GB';
|
const lang = language || meta.config.defaultLang || 'en-GB';
|
||||||
|
const unsubscribable = ['digest', 'notification'];
|
||||||
// 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 };
|
|
||||||
|
|
||||||
// Digests and notifications can be one-click unsubbed
|
// Digests and notifications can be one-click unsubbed
|
||||||
let payload = {
|
let payload = {
|
||||||
@@ -211,15 +207,22 @@ Emailer.sendToEmail = async function (template, email, language, params) {
|
|||||||
uid: params.uid,
|
uid: params.uid,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (template === 'digest' || template === 'notification') {
|
if (unsubscribable.includes(template)) {
|
||||||
if (template === 'notification') {
|
if (template === 'notification') {
|
||||||
payload.type = params.notification.type;
|
payload.type = params.notification.type;
|
||||||
}
|
}
|
||||||
payload = jwt.sign(payload, nconf.get('secret'), {
|
payload = jwt.sign(payload, nconf.get('secret'), {
|
||||||
expiresIn: '30d',
|
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', {
|
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, '/reset/:code?', middleware, [middleware.delayLoading], controllers.reset);
|
||||||
setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse);
|
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('/compose', middleware.applyCSRF, controllers.composer.post);
|
||||||
app.post('/email/unsubscribe/:token', controllers.accounts.settings.unsubscribe);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function modRoutes(app, middleware, controllers) {
|
function modRoutes(app, middleware, controllers) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
<br><br>
|
<br><br>
|
||||||
<!-- IF showUnsubscribe -->
|
<!-- IF showUnsubscribe -->
|
||||||
[[email:notif.post.unsub.info]] <a href="{url}/uid/{uid}/settings">[[email:unsub.cta]]</a>.
|
[[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 -->
|
<!-- ENDIF showUnsubscribe -->
|
||||||
<br><br>
|
<br><br>
|
||||||
</td>
|
</td>
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
<![endif]-->
|
<![endif]-->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</center>
|
</center>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user