fix: email testing and settings change from ACP

- changing email SMTP settings wouldn't apply the first time
- "Send Test Email" now will report emailer errors in most cases
v1.18.x
Peter Jaszkowiak 4 years ago committed by Julian Lam
parent 713f029dc8
commit 2be396ff6e

@ -22,6 +22,7 @@ define('admin/settings/email', ['ace/ace', 'admin/settings'], function (ace) {
$('button[data-action="email.test"]').off('click').on('click', function () { $('button[data-action="email.test"]').off('click').on('click', function () {
socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) { socket.emit('admin.email.test', { template: $('#test-email').val() }, function (err) {
if (err) { if (err) {
console.error(err.message);
return app.alertError(err.message); return app.alertError(err.message);
} }
app.alertSuccess('Test Email Sent'); app.alertSuccess('Test Email Sent');

@ -37,12 +37,9 @@ Emailer.listServices = () => Object.keys(wellKnownServices);
Emailer._defaultPayload = {}; Emailer._defaultPayload = {};
const smtpSettingsChanged = (config) => { const smtpSettingsChanged = (config) => {
if (!prevConfig) {
prevConfig = meta.config;
}
const settings = [ const settings = [
'email:smtpTransport:enabled', 'email:smtpTransport:enabled',
'email:smtpTransport:pool',
'email:smtpTransport:user', 'email:smtpTransport:user',
'email:smtpTransport:pass', 'email:smtpTransport:pass',
'email:smtpTransport:service', 'email:smtpTransport:service',
@ -113,8 +110,8 @@ Emailer.getTemplates = async (config) => {
}; };
Emailer.setupFallbackTransport = (config) => { Emailer.setupFallbackTransport = (config) => {
winston.verbose('[emailer] Setting up SMTP fallback transport'); winston.verbose('[emailer] Setting up fallback transport');
// Enable Gmail transport if enabled in ACP // Enable SMTP transport if enabled in ACP
if (parseInt(config['email:smtpTransport:enabled'], 10) === 1) { if (parseInt(config['email:smtpTransport:enabled'], 10) === 1) {
const smtpOptions = { const smtpOptions = {
pool: config['email:smtpTransport:pool'], pool: config['email:smtpTransport:pool'],
@ -177,6 +174,11 @@ Emailer.registerApp = (expressApp) => {
Emailer.setupFallbackTransport(meta.config); Emailer.setupFallbackTransport(meta.config);
buildCustomTemplates(meta.config); buildCustomTemplates(meta.config);
// need to shallow clone the config object
// otherwise prevConfig holds reference to meta.config object,
// which is updated before the pubsub handler is called
prevConfig = { ...meta.config };
// Update default payload if new logo is uploaded // Update default payload if new logo is uploaded
pubsub.on('config:update', (config) => { pubsub.on('config:update', (config) => {
if (config) { if (config) {
@ -204,8 +206,7 @@ Emailer.registerApp = (expressApp) => {
Emailer.send = async (template, uid, params) => { Emailer.send = async (template, uid, params) => {
if (!app) { if (!app) {
winston.warn('[emailer] App not ready!'); throw Error('[emailer] App not ready!');
return;
} }
const [userData, userSettings] = await Promise.all([ const [userData, userSettings] = await Promise.all([
@ -214,13 +215,13 @@ Emailer.send = async (template, uid, params) => {
]); ]);
if (!userData || !userData.email) { if (!userData || !userData.email) {
winston.warn('uid : ' + uid + ' has no email, not sending.'); winston.warn(`uid : ${uid} has no email, not sending "${template}" email.`);
return; return;
} }
const allowedTpls = ['verify_email', 'welcome', 'registration_accepted']; const allowedTpls = ['verify_email', 'welcome', 'registration_accepted'];
if (meta.config.requireEmailConfirmation && !userData['email:confirmed'] && !allowedTpls.includes(template)) { if (meta.config.requireEmailConfirmation && !userData['email:confirmed'] && !allowedTpls.includes(template)) {
winston.warn('uid : ' + uid + ' has not confirmed email, not sending "' + template + '" email.'); winston.warn(`uid : ${uid} (${userData.email}) has not confirmed email, not sending "${template}" email.`);
return; return;
} }
@ -229,11 +230,7 @@ Emailer.send = async (template, uid, params) => {
params.uid = uid; params.uid = uid;
params.username = userData.username; params.username = userData.username;
params.rtl = await translator.translate('[[language:dir]]', userSettings.userLang) === 'rtl'; params.rtl = await translator.translate('[[language:dir]]', userSettings.userLang) === 'rtl';
try {
await Emailer.sendToEmail(template, userData.email, userSettings.userLang, params); await Emailer.sendToEmail(template, userData.email, userSettings.userLang, params);
} catch (err) {
winston.error(err.stack);
}
}; };
Emailer.sendToEmail = async (template, email, language, params) => { Emailer.sendToEmail = async (template, email, language, params) => {
@ -313,7 +310,7 @@ Emailer.sendToEmail = async (template, email, language, params) => {
} }
}; };
Emailer.sendViaFallback = (data, callback) => { Emailer.sendViaFallback = async (data) => {
// Some minor alterations to the data to conform to nodemailer standard // Some minor alterations to the data to conform to nodemailer standard
data.text = data.plaintext; data.text = data.plaintext;
delete data.plaintext; delete data.plaintext;
@ -323,12 +320,7 @@ Emailer.sendViaFallback = (data, callback) => {
delete data.from_name; delete data.from_name;
winston.verbose('[emailer] Sending email to uid ' + data.uid + ' (' + data.to + ')'); winston.verbose('[emailer] Sending email to uid ' + data.uid + ' (' + data.to + ')');
Emailer.fallbackTransport.sendMail(data, (err) => { await Emailer.fallbackTransport.sendMail(data);
if (err) {
winston.error(err.stack);
}
callback();
});
}; };
Emailer.renderAndTranslate = async (template, params, lang) => { Emailer.renderAndTranslate = async (template, params, lang) => {

@ -181,8 +181,8 @@ async function pushToUids(uids, notification) {
} }
body = posts.relativeToAbsolute(body, posts.urlRegex); body = posts.relativeToAbsolute(body, posts.urlRegex);
body = posts.relativeToAbsolute(body, posts.imgRegex); body = posts.relativeToAbsolute(body, posts.imgRegex);
await async.eachLimit(uids, 3, function (uid, next) { await async.eachLimit(uids, 3, async (uid) => {
emailer.send('notification', uid, { await emailer.send('notification', uid, {
path: notification.path, path: notification.path,
notification_url: notification.path.startsWith('http') ? notification.path : nconf.get('url') + notification.path, notification_url: notification.path.startsWith('http') ? notification.path : nconf.get('url') + notification.path,
subject: utils.stripHTMLTags(notification.subject || '[[notifications:new_notification]]'), subject: utils.stripHTMLTags(notification.subject || '[[notifications:new_notification]]'),
@ -190,7 +190,7 @@ async function pushToUids(uids, notification) {
body: body, body: body,
notification: notification, notification: notification,
showUnsubscribe: true, showUnsubscribe: true,
}, next); }).catch(err => winston.error('[emailer.send] ' + err.stack));
}); });
} }

@ -1,25 +1,24 @@
'use strict'; 'use strict';
const async = require('async');
const userDigest = require('../../user/digest'); const userDigest = require('../../user/digest');
const userEmail = require('../../user/email'); const userEmail = require('../../user/email');
const notifications = require('../../notifications'); const notifications = require('../../notifications');
const emailer = require('../../emailer'); const emailer = require('../../emailer');
const utils = require('../../../public/src/utils'); const utils = require('../../utils');
const Email = module.exports; const Email = module.exports;
Email.test = function (socket, data, callback) { Email.test = async function (socket, data) {
const payload = { const payload = {
subject: '[[email:test-email.subject]]', subject: '[[email:test-email.subject]]',
}; };
switch (data.template) { switch (data.template) {
case 'digest': case 'digest':
userDigest.execute({ await userDigest.execute({
interval: 'alltime', interval: 'alltime',
subscribers: [socket.uid], subscribers: [socket.uid],
}, callback); });
break; break;
case 'banned': case 'banned':
@ -28,42 +27,36 @@ Email.test = function (socket, data, callback) {
until: utils.toISOString(Date.now()), until: utils.toISOString(Date.now()),
reason: 'Test Reason', reason: 'Test Reason',
}); });
emailer.send(data.template, socket.uid, payload, callback); await emailer.send(data.template, socket.uid, payload);
break; break;
case 'welcome': case 'welcome':
userEmail.sendValidationEmail(socket.uid, { await userEmail.sendValidationEmail(socket.uid, {
force: 1, force: 1,
}, callback); });
break; break;
case 'notification': case 'notification': {
async.waterfall([ const notification = await notifications.create({
function (next) {
notifications.create({
type: 'test', type: 'test',
bodyShort: '[[email:notif.test.short]]', bodyShort: '[[email:notif.test.short]]',
bodyLong: '[[email:notif.test.long]]', bodyLong: '[[email:notif.test.long]]',
nid: 'uid:' + socket.uid + ':test', nid: 'uid:' + socket.uid + ':test',
path: '/', path: '/',
from: socket.uid, from: socket.uid,
}, next); });
}, await emailer.send('notification', socket.uid, {
function (notifObj, next) { path: notification.path,
emailer.send('notification', socket.uid, { subject: utils.stripHTMLTags(notification.subject || '[[notifications:new_notification]]'),
path: notifObj.path, intro: utils.stripHTMLTags(notification.bodyShort),
subject: utils.stripHTMLTags(notifObj.subject || '[[notifications:new_notification]]'), body: notification.bodyLong || '',
intro: utils.stripHTMLTags(notifObj.bodyShort), notification,
body: notifObj.bodyLong || '',
notification: notifObj,
showUnsubscribe: true, showUnsubscribe: true,
}, next); });
}, } break;
], callback);
break;
default: default:
emailer.send(data.template, socket.uid, payload, callback); await emailer.send(data.template, socket.uid, payload);
break; break;
} }
}; };

@ -87,9 +87,18 @@ User.sendValidationEmail = async function (socket, uids) {
throw new Error('[[error:email-confirmations-are-disabled]]'); throw new Error('[[error:email-confirmations-are-disabled]]');
} }
const failed = [];
await async.eachLimit(uids, 50, async function (uid) { await async.eachLimit(uids, 50, async function (uid) {
await user.email.sendValidationEmail(uid, { force: true }); await user.email.sendValidationEmail(uid, { force: true }).catch((err) => {
winston.error('[user.create] Validation email failed to send\n[emailer.send] ' + err.stack);
failed.push(uid);
});
}); });
if (failed.length) {
throw Error(`Email sending failed for the following uids, check server logs for more info: ${failed.join(',')}`);
}
}; };
User.sendPasswordResetEmail = async function (socket, uids) { User.sendPasswordResetEmail = async function (socket, uids) {

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const util = require('util'); const util = require('util');
const winston = require('winston');
const sleep = util.promisify(setTimeout); const sleep = util.promisify(setTimeout);
const api = require('../api'); const api = require('../api');
@ -117,7 +118,7 @@ SocketUser.reset.commit = async function (socket, data) {
username: username, username: username,
date: parsedDate, date: parsedDate,
subject: '[[email:reset.notify.subject]]', subject: '[[email:reset.notify.subject]]',
}); }).catch(err => winston.error('[emailer.send] ' + err.stack));
}; };
SocketUser.isFollowing = async function (socket, data) { SocketUser.isFollowing = async function (socket, data) {

@ -1,6 +1,7 @@
'use strict'; 'use strict';
const validator = require('validator'); const validator = require('validator');
const winston = require('winston');
const cronJob = require('cron').CronJob; const cronJob = require('cron').CronJob;
const db = require('../database'); const db = require('../database');
@ -78,7 +79,7 @@ module.exports = function (User) {
subject: '[[email:welcome-to, ' + (meta.config.title || meta.config.browserTitle || 'NodeBB') + ']]', subject: '[[email:welcome-to, ' + (meta.config.title || meta.config.browserTitle || 'NodeBB') + ']]',
template: 'registration_accepted', template: 'registration_accepted',
uid: uid, uid: uid,
}); }).catch(err => winston.error('[emailer.send] ' + err.stack));
const total = await db.incrObjectField('registration:queue:approval:times', 'totalTime', Math.floor((Date.now() - creation_time) / 60000)); const total = await db.incrObjectField('registration:queue:approval:times', 'totalTime', Math.floor((Date.now() - creation_time) / 60000));
const counter = await db.incrObjectField('registration:queue:approval:times', 'counter', 1); const counter = await db.incrObjectField('registration:queue:approval:times', 'counter', 1);
await db.setObjectField('registration:queue:approval:times', 'average', total / counter); await db.setObjectField('registration:queue:approval:times', 'average', total / counter);

@ -53,11 +53,7 @@ module.exports = function (User) {
until: until ? (new Date(until)).toUTCString().replace(/,/g, '\\,') : false, until: until ? (new Date(until)).toUTCString().replace(/,/g, '\\,') : false,
reason: reason, reason: reason,
}; };
try { await emailer.send('banned', uid, data).catch(err => winston.error('[emailer.send] ' + err.stack));
await emailer.send('banned', uid, data);
} catch (err) {
winston.error('[emailer.send] ' + err.message);
}
return banData; return banData;
}; };

@ -1,6 +1,8 @@
'use strict'; 'use strict';
const zxcvbn = require('zxcvbn'); const zxcvbn = require('zxcvbn');
const winston = require('winston');
const db = require('../database'); const db = require('../database');
const utils = require('../utils'); const utils = require('../utils');
const slugify = require('../slugify'); const slugify = require('../slugify');
@ -116,7 +118,7 @@ module.exports = function (User) {
if (userData.email && userData.uid > 1 && meta.config.requireEmailConfirmation) { if (userData.email && userData.uid > 1 && meta.config.requireEmailConfirmation) {
User.email.sendValidationEmail(userData.uid, { User.email.sendValidationEmail(userData.uid, {
email: userData.email, email: userData.email,
}); }).catch(err => winston.error('[user.create] Validation email failed to send\n[emailer.send] ' + err.stack));
} }
if (userNameChanged) { if (userNameChanged) {
await User.notifications.sendNameChangeNotification(userData.uid, userData.username); await User.notifications.sendNameChangeNotification(userData.uid, userData.username);

@ -125,7 +125,6 @@ Digest.send = async function (data) {
emailsSent += 1; emailsSent += 1;
const now = new Date(); const now = new Date();
try {
await emailer.send('digest', userObj.uid, { await emailer.send('digest', userObj.uid, {
subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]', subject: '[[email:digest.subject, ' + (now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate()) + ']]',
username: userObj.username, username: userObj.username,
@ -136,10 +135,7 @@ Digest.send = async function (data) {
popularTopics: popularTopics, popularTopics: popularTopics,
interval: data.interval, interval: data.interval,
showUnsubscribe: true, showUnsubscribe: true,
}); }).catch(err => winston.error('[user/jobs] Could not send digest email\n[emailer.send] ' + err.stack));
} catch (err) {
winston.error('[user/jobs] Could not send digest email\n' + err.stack);
}
if (data.interval !== 'alltime') { if (data.interval !== 'alltime') {
await db.sortedSetAdd('digest:delivery', now.getTime(), userObj.uid); await db.sortedSetAdd('digest:delivery', now.getTime(), userObj.uid);

@ -49,7 +49,6 @@ module.exports = function (User) {
} }
const data = await prepareInvitation(uid, email, groupsToJoin); const data = await prepareInvitation(uid, email, groupsToJoin);
await emailer.sendToEmail('invitation', email, meta.config.defaultLang, data); await emailer.sendToEmail('invitation', email, meta.config.defaultLang, data);
}; };

@ -3,6 +3,7 @@
const async = require('async'); const async = require('async');
const validator = require('validator'); const validator = require('validator');
const winston = require('winston');
const utils = require('../utils'); const utils = require('../utils');
const slugify = require('../slugify'); const slugify = require('../slugify');
@ -246,7 +247,7 @@ module.exports = function (User) {
email: newEmail, email: newEmail,
subject: '[[email:email.verify-your-email.subject]]', subject: '[[email:email.verify-your-email.subject]]',
template: 'verify_email', template: 'verify_email',
}); }).catch(err => winston.error('[user.create] Validation email failed to send\n[emailer.send] ' + err.stack));
} }
} }

@ -55,7 +55,7 @@ UserReset.send = async function (email) {
subject: '[[email:password-reset-requested]]', subject: '[[email:password-reset-requested]]',
template: 'reset', template: 'reset',
uid: uid, uid: uid,
}); }).catch(err => winston.error('[emailer.send] ' + err.stack));
}; };
UserReset.commit = async function (code, password) { UserReset.commit = async function (code, password) {

@ -68,9 +68,13 @@ describe('API', async () => {
async function dummySearchHook(data) { async function dummySearchHook(data) {
return [1]; return [1];
} }
async function dummyEmailerHook(data) {
// pretend to handle sending emails
}
after(async function () { after(async function () {
plugins.unregisterHook('core', 'filter:search.query', dummySearchHook); plugins.unregisterHook('core', 'filter:search.query', dummySearchHook);
plugins.unregisterHook('emailer-test', 'filter:email.send');
}); });
async function setupData() { async function setupData() {
@ -145,6 +149,11 @@ describe('API', async () => {
hook: 'filter:search.query', hook: 'filter:search.query',
method: dummySearchHook, method: dummySearchHook,
}); });
// Attach an emailer hook so related requests do not error
plugins.registerHook('emailer-test', {
hook: 'filter:email.send',
method: dummyEmailerHook,
});
jar = await helpers.loginUser('admin', '123456'); jar = await helpers.loginUser('admin', '123456');

@ -240,6 +240,21 @@ describe('socket.io', function () {
describe('validation emails', function () { describe('validation emails', function () {
var meta = require('../src/meta'); var meta = require('../src/meta');
var plugins = require('../src/plugins');
async function dummyEmailerHook(data) {
// pretend to handle sending emails
}
before(function () {
// Attach an emailer hook so related requests do not error
plugins.registerHook('emailer-test', {
hook: 'filter:email.send',
method: dummyEmailerHook,
});
});
after(function () {
plugins.unregisterHook('emailer-test', 'filter:email.send');
});
it('should validate emails', function (done) { it('should validate emails', function (done) {
socketAdmin.user.validateEmail({ uid: adminUid }, [regularUid], function (err) { socketAdmin.user.validateEmail({ uid: adminUid }, [regularUid], function (err) {

@ -25,7 +25,18 @@ describe('User', function () {
var testUid; var testUid;
var testCid; var testCid;
var plugins = require('../src/plugins');
async function dummyEmailerHook(data) {
// pretend to handle sending emails
}
before(function (done) { before(function (done) {
// Attach an emailer hook so related requests do not error
plugins.registerHook('emailer-test', {
hook: 'filter:email.send',
method: dummyEmailerHook,
});
Categories.create({ Categories.create({
name: 'Test Category', name: 'Test Category',
description: 'A test', description: 'A test',
@ -39,6 +50,9 @@ describe('User', function () {
done(); done();
}); });
}); });
after(function () {
plugins.unregisterHook('emailer-test', 'filter:email.send');
});
beforeEach(function () { beforeEach(function () {
userData = { userData = {

Loading…
Cancel
Save