From 76732140f3316798e9711f663add6028d7f7f03e Mon Sep 17 00:00:00 2001
From: Misty Release Bot
Date: Fri, 3 Feb 2023 16:39:40 +0000
Subject: [PATCH 01/13] chore: incrementing version number - v2.8.6
---
install/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/install/package.json b/install/package.json
index 34c5e0bf05..a25c262ff1 100644
--- a/install/package.json
+++ b/install/package.json
@@ -2,7 +2,7 @@
"name": "nodebb",
"license": "GPL-3.0",
"description": "NodeBB Forum",
- "version": "2.8.5",
+ "version": "2.8.6",
"homepage": "http://www.nodebb.org",
"repository": {
"type": "git",
From f3306d038a36fbbe525125ac9986c11be5d86368 Mon Sep 17 00:00:00 2001
From: Misty Release Bot
Date: Fri, 3 Feb 2023 16:39:40 +0000
Subject: [PATCH 02/13] chore: update changelog for v2.8.6
---
CHANGELOG.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 51 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8b793bc928..b053f14578 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,54 @@
+#### v2.8.6 (2023-02-03)
+
+##### Chores
+
+* **i18n:** fallback strings for new resources: nodebb.error (8335f90a)
+* incrementing version number - v2.8.5 (bff5ce2d)
+* update changelog for v2.8.5 (24e58c28)
+* incrementing version number - v2.8.4 (a46b2bbc)
+* incrementing version number - v2.8.3 (c20b20a7)
+* incrementing version number - v2.8.2 (050e43f8)
+* incrementing version number - v2.8.1 (727f879e)
+* incrementing version number - v2.8.0 (8e77673d)
+* incrementing version number - v2.7.0 (96cc0617)
+* incrementing version number - v2.6.1 (7e52a7a5)
+* incrementing version number - v2.6.0 (e7fcf482)
+* incrementing version number - v2.5.8 (dec0e7de)
+* incrementing version number - v2.5.7 (5836bf4a)
+* incrementing version number - v2.5.6 (c7bd7dbf)
+* incrementing version number - v2.5.5 (3509ed94)
+* incrementing version number - v2.5.4 (e83260ca)
+* incrementing version number - v2.5.3 (7e922936)
+* incrementing version number - v2.5.2 (babcd17e)
+* incrementing version number - v2.5.1 (ce3aa950)
+* incrementing version number - v2.5.0 (01d276cb)
+* incrementing version number - v2.4.5 (dd3e1a28)
+* incrementing version number - v2.4.4 (d5525c87)
+* incrementing version number - v2.4.3 (9c647c6c)
+* incrementing version number - v2.4.2 (3aa7b855)
+* incrementing version number - v2.4.1 (60cbd148)
+* incrementing version number - v2.4.0 (4834cde3)
+* incrementing version number - v2.3.1 (d2425942)
+* incrementing version number - v2.3.0 (046ea120)
+
+##### New Features
+
+* add sitemap filter hooks for categories/topic pages (bf92ee0e)
+* closes #11241, add missing error lang keys (c241baf6)
+* #11240, only show relevant users in flags assignee list (0713482b)
+
+##### Bug Fixes
+
+* #11254, return check for reroll property (202378b9)
+* closes #11249, notification uses displayname (705cd13a)
+* wrong link to topics in acp dashboard (b5598a6e)
+* https://github.com/NodeBB/NodeBB/issues/11239 (1d3c0e5a)
+* notif filter selecte field (6d819b05)
+
+##### Other Changes
+
+* remove unused (d68352cc)
+
#### v2.8.5 (2023-01-27)
##### Chores
From 7a5bcc217146621fa088e3572ccb8d950b3097c5 Mon Sep 17 00:00:00 2001
From: Julian Lam
Date: Fri, 3 Feb 2023 16:01:31 -0500
Subject: [PATCH 03/13] fix: #11257, onSuccessfulLogin called with improper uid
---
src/routes/authentication.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/routes/authentication.js b/src/routes/authentication.js
index 9febe062a8..934fdec80e 100644
--- a/src/routes/authentication.js
+++ b/src/routes/authentication.js
@@ -154,7 +154,7 @@ Auth.reloadRoutes = async function (params) {
}, Auth.middleware.validateAuth, (req, res, next) => {
async.waterfall([
async.apply(req.login.bind(req), res.locals.user, { keepSessionInfo: true }),
- async.apply(controllers.authentication.onSuccessfulLogin, req, req.uid),
+ async.apply(controllers.authentication.onSuccessfulLogin, req, res.locals.user.uid),
], (err) => {
if (err) {
return next(err);
From 845c8013b6442ef31fa0476f1c8e2447c643f58f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?=
Date: Mon, 6 Feb 2023 10:45:01 -0500
Subject: [PATCH 04/13] fix: #11259, clean old emails when updating via admin
(#11260)
when admin is changing users emails check if its avaiable and remove old email of user first
upgrade script to cleanup email:uid, email:sorted, will remove entries if user doesn't exist or doesn't have email or if entry in user hash doesn't match entry in email:uid
fix missing ! in email interstitial
fix missing await in canSendValidation,
fix broken tests
dont pass sessionId to email.remove if admin is changing/removing email
---
src/upgrades/2.8.7/fix-email-sorted-sets.js | 46 +++++++++++++++++++++
src/user/email.js | 18 +++++++-
src/user/interstitials.js | 11 +++--
test/user/emails.js | 4 +-
4 files changed, 72 insertions(+), 7 deletions(-)
create mode 100644 src/upgrades/2.8.7/fix-email-sorted-sets.js
diff --git a/src/upgrades/2.8.7/fix-email-sorted-sets.js b/src/upgrades/2.8.7/fix-email-sorted-sets.js
new file mode 100644
index 0000000000..fcab69a8f4
--- /dev/null
+++ b/src/upgrades/2.8.7/fix-email-sorted-sets.js
@@ -0,0 +1,46 @@
+'use strict';
+
+
+const db = require('../../database');
+const batch = require('../../batch');
+
+
+module.exports = {
+ name: 'Fix user email sorted sets',
+ timestamp: Date.UTC(2023, 1, 4),
+ method: async function () {
+ const { progress } = this;
+ const bulkRemove = [];
+ await batch.processSortedSet('email:uid', async (data) => {
+ progress.incr(data.length);
+ const usersData = await db.getObjects(data.map(d => `user:${d.score}`));
+ data.forEach((emailData, index) => {
+ const { score: uid, value: email } = emailData;
+ const userData = usersData[index];
+ // user no longer exists or doesn't have email set in user hash
+ // remove the email/uid pair from email:uid, email:sorted
+ if (!userData || !userData.email) {
+ bulkRemove.push(['email:uid', email]);
+ bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]);
+ return;
+ }
+
+ // user has email but doesn't match whats stored in user hash, gh#11259
+ if (userData.email && userData.email.toLowerCase() !== email.toLowerCase()) {
+ bulkRemove.push(['email:uid', email]);
+ bulkRemove.push(['email:sorted', `${email.toLowerCase()}:${uid}`]);
+ }
+ });
+ }, {
+ batch: 500,
+ withScores: true,
+ progress: progress,
+ });
+
+ await batch.processArray(bulkRemove, async (bulk) => {
+ await db.sortedSetRemoveBulk(bulk);
+ }, {
+ batch: 500,
+ });
+ },
+};
diff --git a/src/user/email.js b/src/user/email.js
index 1ea8bd551e..c6fc3274d4 100644
--- a/src/user/email.js
+++ b/src/user/email.js
@@ -39,7 +39,7 @@ UserEmail.remove = async function (uid, sessionId) {
db.sortedSetRemove('email:uid', email.toLowerCase()),
db.sortedSetRemove('email:sorted', `${email.toLowerCase()}:${uid}`),
user.email.expireValidation(uid),
- user.auth.revokeAllSessions(uid, sessionId),
+ sessionId ? user.auth.revokeAllSessions(uid, sessionId) : Promise.resolve(),
events.log({ type: 'email-change', email, newEmail: '' }),
]);
};
@@ -69,7 +69,7 @@ UserEmail.expireValidation = async (uid) => {
};
UserEmail.canSendValidation = async (uid, email) => {
- const pending = UserEmail.isValidationPending(uid, email);
+ const pending = await UserEmail.isValidationPending(uid, email);
if (!pending) {
return true;
}
@@ -196,6 +196,20 @@ UserEmail.confirmByUid = async function (uid) {
throw new Error('[[error:invalid-email]]');
}
+ // If another uid has the same email throw error
+ const oldUid = await db.sortedSetScore('email:uid', currentEmail.toLowerCase());
+ if (oldUid && oldUid !== parseInt(uid, 10)) {
+ throw new Error('[[error:email-taken]]');
+ }
+
+ const confirmedEmails = await db.getSortedSetRangeByScore(`email:uid`, 0, -1, uid, uid);
+ if (confirmedEmails.length) {
+ // remove old email of user by uid
+ await db.sortedSetsRemoveRangeByScore([`email:uid`], uid, uid);
+ await db.sortedSetRemoveBulk(
+ confirmedEmails.map(email => [`email:sorted`, `${email.toLowerCase()}:${uid}`])
+ );
+ }
await Promise.all([
db.sortedSetAddBulk([
['email:uid', uid, currentEmail.toLowerCase()],
diff --git a/src/user/interstitials.js b/src/user/interstitials.js
index 2a662785f9..aa70e8098f 100644
--- a/src/user/interstitials.js
+++ b/src/user/interstitials.js
@@ -42,6 +42,7 @@ Interstitials.email = async (data) => {
callback: async (userData, formData) => {
// Validate and send email confirmation
if (userData.uid) {
+ const isSelf = parseInt(userData.uid, 10) === parseInt(data.req.uid, 10);
const [isPasswordCorrect, canEdit, { email: current, 'email:confirmed': confirmed }, { allowed, error }] = await Promise.all([
user.isPasswordCorrect(userData.uid, formData.password, data.req.ip),
privileges.users.canEdit(data.req.uid, userData.uid),
@@ -68,13 +69,17 @@ Interstitials.email = async (data) => {
if (formData.email === current) {
if (confirmed) {
throw new Error('[[error:email-nochange]]');
- } else if (await user.email.canSendValidation(userData.uid, current)) {
+ } else if (!await user.email.canSendValidation(userData.uid, current)) {
throw new Error(`[[error:confirm-email-already-sent, ${meta.config.emailConfirmInterval}]]`);
}
}
// Admins editing will auto-confirm, unless editing their own email
if (isAdminOrGlobalMod && userData.uid !== data.req.uid) {
+ if (!await user.email.available(formData.email)) {
+ throw new Error('[[error:email-taken]]');
+ }
+ await user.email.remove(userData.uid);
await user.setUserField(userData.uid, 'email', formData.email);
await user.email.confirmByUid(userData.uid);
} else if (canEdit) {
@@ -99,8 +104,8 @@ Interstitials.email = async (data) => {
}
if (current.length && (!hasPassword || (hasPassword && isPasswordCorrect) || isAdminOrGlobalMod)) {
- // User explicitly clearing their email
- await user.email.remove(userData.uid, data.req.session.id);
+ // User or admin explicitly clearing their email
+ await user.email.remove(userData.uid, isSelf ? data.req.session.id : null);
}
}
} else {
diff --git a/test/user/emails.js b/test/user/emails.js
index f413a51283..47d3fcb6d0 100644
--- a/test/user/emails.js
+++ b/test/user/emails.js
@@ -120,7 +120,7 @@ describe('email confirmation (library methods)', () => {
await user.email.sendValidationEmail(uid, {
email,
});
- const ok = await user.email.canSendValidation(uid, 'test@example.com');
+ const ok = await user.email.canSendValidation(uid, email);
assert.strictEqual(ok, false);
});
@@ -131,7 +131,7 @@ describe('email confirmation (library methods)', () => {
email,
});
await db.pexpire(`confirm:byUid:${uid}`, 1000);
- const ok = await user.email.canSendValidation(uid, 'test@example.com');
+ const ok = await user.email.canSendValidation(uid, email);
assert(ok);
});
From e335d0f6016c76575ed768d6c33130f8ff847b35 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?=
Date: Wed, 8 Feb 2023 13:22:16 -0500
Subject: [PATCH 05/13] fix: email expiry timestamps
emailConfirmExpiry is hours and default is 24
---
src/user/email.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/user/email.js b/src/user/email.js
index c6fc3274d4..9b51b43ddd 100644
--- a/src/user/email.js
+++ b/src/user/email.js
@@ -134,13 +134,13 @@ UserEmail.sendValidationEmail = async function (uid, options) {
await UserEmail.expireValidation(uid);
await db.set(`confirm:byUid:${uid}`, confirm_code);
- await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 24 * 60 * 60 * 1000);
+ await db.pexpire(`confirm:byUid:${uid}`, emailConfirmExpiry * 60 * 60 * 1000);
await db.setObject(`confirm:${confirm_code}`, {
email: options.email.toLowerCase(),
uid: uid,
});
- await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 24 * 60 * 60 * 1000);
+ await db.pexpire(`confirm:${confirm_code}`, emailConfirmExpiry * 60 * 60 * 1000);
winston.verbose(`[user/email] Validation email for uid ${uid} sent to ${options.email}`);
events.log({
From 326b92687fa5d2b68cc5f55275c565a43bf6a16e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?=
Date: Wed, 8 Feb 2023 17:35:38 -0500
Subject: [PATCH 06/13] fix: show admins/globalmods if content is purged
---
src/controllers/mods.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/controllers/mods.js b/src/controllers/mods.js
index 3656146652..760c119fbe 100644
--- a/src/controllers/mods.js
+++ b/src/controllers/mods.js
@@ -132,11 +132,11 @@ modsController.flags.detail = async function (req, res, next) {
uids = _.uniq(admins.concat(uids));
} else if (flagData.type === 'post') {
const cid = await posts.getCidByPid(flagData.targetId);
- if (!cid) {
- return [];
+ uids = _.uniq(admins.concat(globalMods));
+ if (cid) {
+ const modUids = (await privileges.categories.getUidsWithPrivilege([cid], 'moderate'))[0];
+ uids = _.uniq(uids.concat(modUids));
}
- uids = (await privileges.categories.getUidsWithPrivilege([cid], 'moderate'))[0];
- uids = _.uniq(admins.concat(globalMods).concat(uids));
}
const userData = await user.getUsersData(uids);
return userData.filter(u => u && u.userslug);
From 40e7b86da9b3eab82914ce557333eb257c4c43d3 Mon Sep 17 00:00:00 2001
From: Julian Lam
Date: Mon, 13 Feb 2023 11:44:40 -0500
Subject: [PATCH 07/13] docs: update openapi spec to include info about passing
in timestamps for topic creation, removing timestamp as valid request param
for topic replying
---
public/openapi/write/topics.yaml | 9 +++++++++
public/openapi/write/topics/tid.yaml | 2 --
2 files changed, 9 insertions(+), 2 deletions(-)
diff --git a/public/openapi/write/topics.yaml b/public/openapi/write/topics.yaml
index ba00cf0024..49cae076f9 100644
--- a/public/openapi/write/topics.yaml
+++ b/public/openapi/write/topics.yaml
@@ -19,6 +19,15 @@ post:
content:
type: string
example: This is the test topic's content
+ timestamp:
+ type: number
+ description: |
+ A UNIX timestamp of the topic's creation date (i.e. when it will be posted).
+ Specifically, this value can only be set to a value in the future if the calling user has the `topics:schedule` privilege for the passed-in category.
+ Otherwise, the current date and time are always assumed.
+ In some scenarios (e.g. forum migrations), you may want to backdate topics and posts.
+ Please see [this Developer FAQ topic](https://community.nodebb.org/topic/16983/how-can-i-backdate-topics-and-posts-for-migration-purposes) for more information.
+ example: 556084800000
tags:
type: array
items:
diff --git a/public/openapi/write/topics/tid.yaml b/public/openapi/write/topics/tid.yaml
index 8e68efe25a..4c1acab3d6 100644
--- a/public/openapi/write/topics/tid.yaml
+++ b/public/openapi/write/topics/tid.yaml
@@ -46,8 +46,6 @@ post:
content:
type: string
example: This is a test reply
- timestamp:
- type: number
toPid:
type: number
required:
From 1b29dbb69d123c35d4fa954b8ee9aa63ded28136 Mon Sep 17 00:00:00 2001
From: Julian Lam
Date: Mon, 13 Feb 2023 12:15:45 -0500
Subject: [PATCH 08/13] test: add dummy emailer hook in authentication test
---
test/authentication.js | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/test/authentication.js b/test/authentication.js
index b8889d95eb..3718271809 100644
--- a/test/authentication.js
+++ b/test/authentication.js
@@ -12,13 +12,22 @@ const db = require('./mocks/databasemock');
const user = require('../src/user');
const utils = require('../src/utils');
const meta = require('../src/meta');
+const plugins = require('../src/plugins');
const privileges = require('../src/privileges');
const helpers = require('./helpers');
describe('authentication', () => {
const jar = request.jar();
let regularUid;
+ const dummyEmailerHook = async (data) => {};
+
before((done) => {
+ // Attach an emailer hook so related requests do not error
+ plugins.hooks.register('authentication-test', {
+ hook: 'filter:email.send',
+ method: dummyEmailerHook,
+ });
+
user.create({ username: 'regular', password: 'regularpwd', email: 'regular@nodebb.org' }, (err, uid) => {
assert.ifError(err);
regularUid = uid;
@@ -27,6 +36,10 @@ describe('authentication', () => {
});
});
+ after(() => {
+ plugins.hooks.unregister('authentication-test', 'filter:email.send');
+ });
+
it('should allow login with email for uid 1', async () => {
const oldValue = meta.config.allowLoginWith;
meta.config.allowLoginWith = 'username-email';
From edd2fc38fc2d6f0ff7f344d11236190a44404f5d Mon Sep 17 00:00:00 2001
From: gasoved
Date: Thu, 16 Feb 2023 04:38:51 +0300
Subject: [PATCH 09/13] fix: update main post timestamp when rescheduling
---
src/topics/scheduled.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/topics/scheduled.js b/src/topics/scheduled.js
index 3544e54945..a386de8869 100644
--- a/src/topics/scheduled.js
+++ b/src/topics/scheduled.js
@@ -60,6 +60,7 @@ Scheduled.pin = async function (tid, topicData) {
};
Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
+ const mainPid = await topics.getTopicField(tid, 'mainPid');
await Promise.all([
db.sortedSetsAdd([
'topics:scheduled',
@@ -67,6 +68,7 @@ Scheduled.reschedule = async function ({ cid, tid, timestamp, uid }) {
'topics:tid',
`cid:${cid}:uid:${uid}:tids`,
], timestamp, tid),
+ posts.setPostField(mainPid, 'timestamp', timestamp),
shiftPostTimes(tid, timestamp),
]);
return topics.updateLastPostTimeFromLastPid(tid);
From 3bd9a8715409d9bda478202e8b62e3d396b6dfcc Mon Sep 17 00:00:00 2001
From: Eldor
Date: Tue, 21 Feb 2023 15:56:00 +0200
Subject: [PATCH 10/13] fix: show error alert if password change fails
---
public/src/client/account/edit/password.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/public/src/client/account/edit/password.js b/public/src/client/account/edit/password.js
index abbe443e85..4ad1b47dbb 100644
--- a/public/src/client/account/edit/password.js
+++ b/public/src/client/account/edit/password.js
@@ -77,6 +77,7 @@ define('forum/account/edit/password', [
ajaxify.go('user/' + ajaxify.data.userslug + '/edit');
}
})
+ .catch(alerts.error)
.finally(() => {
btn.removeClass('disabled').find('i').addClass('hide');
currentPassword.val('');
From 8cf4a6f62eab74961f351f0cbf83e53722378e04 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?=
Date: Tue, 15 Nov 2022 09:29:14 -0500
Subject: [PATCH 11/13] fix: alert on page load
---
public/src/admin/settings/email.js | 52 +++++++++++++++++++-----------
src/views/admin/settings/email.tpl | 2 +-
2 files changed, 34 insertions(+), 20 deletions(-)
diff --git a/public/src/admin/settings/email.js b/public/src/admin/settings/email.js
index e1598c2c0b..c1b3ee33b5 100644
--- a/public/src/admin/settings/email.js
+++ b/public/src/admin/settings/email.js
@@ -9,15 +9,23 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
configureEmailTester();
configureEmailEditor();
handleDigestHourChange();
- handleSmtpServiceChange();
- $(window).on('action:admin.settingsLoaded action:admin.settingsSaved', handleDigestHourChange);
- $(window).on('action:admin.settingsSaved', function () {
- socket.emit('admin.user.restartJobs');
- });
- $('[id="email:smtpTransport:service"]').change(handleSmtpServiceChange);
+ $(window).off('action:admin.settingsLoaded', onSettingsLoaded)
+ .on('action:admin.settingsLoaded', onSettingsLoaded);
+ $(window).off('action:admin.settingsSaved', onSettingsSaved)
+ .on('action:admin.settingsSaved', onSettingsSaved);
};
+ function onSettingsLoaded() {
+ handleDigestHourChange();
+ handleSmtpServiceChange();
+ }
+
+ function onSettingsSaved() {
+ handleDigestHourChange();
+ socket.emit('admin.user.restartJobs');
+ }
+
function configureEmailTester() {
$('button[data-action="email.test"]').off('click').on('click', function () {
socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) {
@@ -106,20 +114,26 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
}
function handleSmtpServiceChange() {
- const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp';
- $('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom);
-
- const enabledEl = document.getElementById('email:smtpTransport:enabled');
- if (enabledEl) {
- if (!enabledEl.checked) {
- enabledEl.closest('label').classList.toggle('is-checked', true);
- enabledEl.checked = true;
- alerts.alert({
- message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]',
- timeout: 5000,
- });
- }
+ function toggleCustomService() {
+ const isCustom = $('[id="email:smtpTransport:service"]').val() === 'nodebb-custom-smtp';
+ $('[id="email:smtpTransport:custom-service"]')[isCustom ? 'slideDown' : 'slideUp'](isCustom);
}
+ toggleCustomService();
+ $('[id="email:smtpTransport:service"]').change(function () {
+ toggleCustomService();
+
+ const enabledEl = document.getElementById('email:smtpTransport:enabled');
+ if (enabledEl) {
+ if (!enabledEl.checked) {
+ $('label[for="email:smtpTransport:enabled"]').toggleClass('is-checked', true);
+ enabledEl.checked = true;
+ alerts.alert({
+ message: '[[admin/settings/email:smtp-transport.auto-enable-toast]]',
+ timeout: 5000,
+ });
+ }
+ }
+ });
}
return module;
diff --git a/src/views/admin/settings/email.tpl b/src/views/admin/settings/email.tpl
index 4d8dcf27b1..86145d0c69 100644
--- a/src/views/admin/settings/email.tpl
+++ b/src/views/admin/settings/email.tpl
@@ -150,7 +150,7 @@
[[admin/settings/email:smtp-transport.gmail-warning2]]
-