feat: closes #13009, add dedicated test smtp button

which uses the dirty settings on the page
add clarification under send test email button
add missing lang keys
This commit is contained in:
Barış Soner Uşaklı
2026-02-02 13:36:38 -05:00
parent b61fa42625
commit c848801268
6 changed files with 121 additions and 46 deletions

View File

@@ -30,14 +30,20 @@
"smtp-transport.pool-help": "Pooling connections prevents NodeBB from creating a new connection for every email. This option only applies if SMTP Transport is enabled.",
"smtp-transport.allow-self-signed": "Allow self-signed certificates",
"smtp-transport.allow-self-signed-help": "Enabling this setting will allow you to use self-signed or invalid TLS certificates.",
"smtp-transport.test-success": "SMTP Test email sent successfully.",
"template": "Edit Email Template",
"template.select": "Select Email Template",
"template.revert": "Revert to Original",
"test-smtp-settings": "Test SMTP Settings",
"testing": "Email Testing",
"testing.success": "Test Email Sent.",
"testing.select": "Select Email Template",
"testing.send": "Send Test Email",
"testing.send-help": "The test email will be sent to the currently logged in user's email address.",
"testing.send-help-plugin": "<strong>\"%1\"</strong> will be used to send test emails.",
"testing.send-help-smtp": "SMTP transport is enabled and will be used to send emails.",
"testing.send-help-no-plugin": "No emailer plugin is installed to send emails, nodemailer will be used by default.",
"testing.send-help": "The test email will be sent to the currently logged in user's email address using the saved settings on this page. ",
"subscriptions": "Email Digests",
"subscriptions.disable": "Disable email digests",
"subscriptions.hour": "Digest Hour",

View File

@@ -6,6 +6,7 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
let emailEditor;
module.init = function () {
configureSmtpTester();
configureEmailTester();
configureEmailEditor();
handleDigestHourChange();
@@ -26,6 +27,29 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
socket.emit('admin.user.restartJobs');
}
function configureSmtpTester() {
$('[data-action="email.smtp.test"]').on('click', function () {
const smtpOptions = {};
$('[data-field^="email:smtp"]').each(function (index, el) {
const $el = $(el);
const key = $el.attr('data-field');
if ($el.is(':checkbox')) {
smtpOptions[key] = $el.is(':checked');
} else {
smtpOptions[key] = $el.val();
}
});
socket.emit('admin.email.testSmtp', { smtp: smtpOptions }, function (err) {
if (err) {
console.error(err.message);
return alerts.error(err);
}
alerts.success('[[admin/settings/email:smtp-transport.test-success]]');
});
});
}
function configureEmailTester() {
$('button[data-action="email.test"]').off('click').on('click', function () {
socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) {
@@ -33,7 +57,7 @@ define('admin/settings/email', ['ace/ace', 'alerts', 'admin/settings'], function
console.error(err.message);
return alerts.error(err);
}
alerts.success('Test Email Sent');
alerts.success('[[admin/settings/email:testing.success]]');
});
return false;
});

View File

@@ -14,6 +14,7 @@ const api = require('../../api');
const pagination = require('../../pagination');
const helpers = require('../helpers');
const translator = require('../../translator');
const plugins = require('../../plugins');
const settingsController = module.exports;
@@ -114,9 +115,14 @@ settingsController.uploads = async (req, res) => {
settingsController.email = async (req, res) => {
const emails = await emailer.getTemplates(meta.config);
const hooks = plugins.loadedHooks['static:email.send'];
const emailerPlugin = hooks && hooks.length ? hooks[0].id : null;
const smtpEnabled = parseInt(meta.config['email:smtpTransport:enabled'], 10) === 1;
res.render('admin/settings/email', {
title: '[[admin/menu:settings/email]]',
emailerPlugin,
smtpEnabled,
emails: emails,
sendable: emails.filter(e => !e.path.includes('_plaintext') && !e.path.includes('partials')).map(tpl => tpl.path),
services: emailer.listServices(),

View File

@@ -56,8 +56,7 @@ const smtpSettingsChanged = (config) => {
const getHostname = () => {
const configUrl = nconf.get('url');
const parsed = url.parse(configUrl);
return parsed.hostname;
return new URL(configUrl).hostname;
};
const buildCustomTemplates = async (config) => {
@@ -120,51 +119,55 @@ Emailer.setupFallbackTransport = (config) => {
winston.verbose('[emailer] Setting up fallback transport');
// Enable SMTP transport if enabled in ACP
if (parseInt(config['email:smtpTransport:enabled'], 10) === 1) {
const smtpOptions = {
name: getHostname(),
pool: config['email:smtpTransport:pool'],
};
if (config['email:smtpTransport:user'] || config['email:smtpTransport:pass']) {
smtpOptions.auth = {
user: config['email:smtpTransport:user'],
pass: config['email:smtpTransport:pass'],
};
}
if (config['email:smtpTransport:service'] === 'nodebb-custom-smtp') {
smtpOptions.port = config['email:smtpTransport:port'];
smtpOptions.host = config['email:smtpTransport:host'];
if (config['email:smtpTransport:security'] === 'NONE') {
smtpOptions.secure = false;
smtpOptions.requireTLS = false;
smtpOptions.ignoreTLS = true;
} else if (config['email:smtpTransport:security'] === 'STARTTLS') {
smtpOptions.secure = false;
smtpOptions.requireTLS = true;
smtpOptions.ignoreTLS = false;
} else {
// meta.config['email:smtpTransport:security'] === 'ENCRYPTED' or undefined
smtpOptions.secure = true;
smtpOptions.requireTLS = true;
smtpOptions.ignoreTLS = false;
}
} else {
smtpOptions.service = String(config['email:smtpTransport:service']);
}
if (config['email:smtpTransport:allow-self-signed']) {
smtpOptions.tls = {
rejectUnauthorized: false,
};
}
Emailer.transports.smtp = nodemailer.createTransport(smtpOptions);
Emailer.transports.smtp = Emailer.createSmtpTransport(config);
Emailer.fallbackTransport = Emailer.transports.smtp;
} else {
Emailer.fallbackTransport = Emailer.transports.sendmail;
}
};
Emailer.createSmtpTransport = (config) => {
const smtpOptions = {
name: getHostname(),
pool: config['email:smtpTransport:pool'],
};
if (config['email:smtpTransport:user'] || config['email:smtpTransport:pass']) {
smtpOptions.auth = {
user: config['email:smtpTransport:user'],
pass: config['email:smtpTransport:pass'],
};
}
if (config['email:smtpTransport:service'] === 'nodebb-custom-smtp') {
smtpOptions.port = config['email:smtpTransport:port'];
smtpOptions.host = config['email:smtpTransport:host'];
if (config['email:smtpTransport:security'] === 'NONE') {
smtpOptions.secure = false;
smtpOptions.requireTLS = false;
smtpOptions.ignoreTLS = true;
} else if (config['email:smtpTransport:security'] === 'STARTTLS') {
smtpOptions.secure = false;
smtpOptions.requireTLS = true;
smtpOptions.ignoreTLS = false;
} else {
// meta.config['email:smtpTransport:security'] === 'ENCRYPTED' or undefined
smtpOptions.secure = true;
smtpOptions.requireTLS = true;
smtpOptions.ignoreTLS = false;
}
} else {
smtpOptions.service = String(config['email:smtpTransport:service']);
}
if (config['email:smtpTransport:allow-self-signed']) {
smtpOptions.tls = {
rejectUnauthorized: false,
};
}
return nodemailer.createTransport(smtpOptions);
};
Emailer.registerApp = (expressApp) => {
app = expressApp;

View File

@@ -1,5 +1,6 @@
'use strict';
const nconf = require('nconf');
const winston = require('winston');
const meta = require('../../meta');
@@ -8,6 +9,7 @@ const userEmail = require('../../user/email');
const notifications = require('../../notifications');
const emailer = require('../../emailer');
const utils = require('../../utils');
const user = require('../../user');
const Email = module.exports;
@@ -72,3 +74,25 @@ Email.test = async function (socket, data) {
throw err;
}
};
Email.testSmtp = async (socket, data) => {
try {
const smtp = emailer.createSmtpTransport(data.smtp);
const content = 'This is a test email sent from NodeBB to verify your SMTP settings are correct.';
const { hostname } = new URL(nconf.get('url'));
const toEmail = await user.getUserField(socket.uid, 'email');
await smtp.sendMail({
to: toEmail,
subject: `[${meta.config.title}] SMTP Settings Test Email`,
html: content,
text: content,
from: {
name: meta.config['email:from_name'] || 'NodeBB',
address: meta.config['email:from'] || `no-reply@${hostname}`,
},
});
} catch (err) {
winston.error(err.stack);
throw err;
}
};

View File

@@ -167,10 +167,13 @@
[[admin/settings/email:smtp-transport.username-help]]
</p>
</div>
<div>
<div class="mb-3">
<label class="form-label" for="email:smtpTransport:pass">[[admin/settings/email:smtp-transport.password]]</label>
<input id="email:smtpTransport:pass" type="password" class="form-control input-lg" data-field="email:smtpTransport:pass" autocomplete="off" />
</div>
<div>
<button class="btn btn-primary text-nowrap" type="button" data-action="email.smtp.test">[[admin/settings/email:test-smtp-settings]]</button>
</div>
</div>
<div id="email-testing" class="mb-4">
@@ -181,14 +184,23 @@
<div class="d-flex justify-content-between gap-1">
<select id="test-email" class="form-select">
{{{ each sendable }}}
<option value="{@value}">{@value}</option>
<option value="{@value}" {{{ if (@value == "test")}}} selected{{{ end }}}>{@value}</option>
{{{ end }}}
</select>
<button class="btn btn-primary text-nowrap" type="button" data-action="email.test">[[admin/settings/email:testing.send]]</button>
</div>
</div>
<p class="form-text">
[[admin/settings/email:testing.send-help]]
[[admin/settings/email:testing.send-help]]<br/>
{{{ if emailerPlugin }}}
[[admin/settings/email:testing.send-help-plugin, {emailerPlugin}]]
{{{ else }}}
{{{ if smtpEnabled }}}
[[admin/settings/email:testing.send-help-smtp]]
{{{ else }}}
[[admin/settings/email:testing.send-help-no-plugin]]
{{{ end }}}
{{{ end }}}
</p>
</div>