mirror of
https://github.com/NodeBB/NodeBB.git
synced 2025-11-09 15:35:47 +01:00
Enable imports in custom email templates (#6052)
* Enable imports in custom email templates * Compile emails on config change * Add error logging * Add emailer tests * Fix tests * Only build when config changes
This commit is contained in:
committed by
Barış Soner Uşaklı
parent
415940af02
commit
4fcedc6f31
@@ -110,7 +110,8 @@
|
|||||||
"jsdom": "^11.3.0",
|
"jsdom": "^11.3.0",
|
||||||
"mocha": "^4.0.1",
|
"mocha": "^4.0.1",
|
||||||
"mocha-lcov-reporter": "^1.3.0",
|
"mocha-lcov-reporter": "^1.3.0",
|
||||||
"nyc": "^11.2.1"
|
"nyc": "^11.2.1",
|
||||||
|
"smtp-server": "^3.3.0"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/NodeBB/NodeBB/issues"
|
"url": "https://github.com/NodeBB/NodeBB/issues"
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var async = require('async');
|
var async = require('async');
|
||||||
var nconf = require('nconf');
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
var meta = require('../../meta');
|
var meta = require('../../meta');
|
||||||
var file = require('../../file');
|
|
||||||
var emailer = require('../../emailer');
|
var emailer = require('../../emailer');
|
||||||
|
|
||||||
var settingsController = module.exports;
|
var settingsController = module.exports;
|
||||||
@@ -26,42 +22,8 @@ settingsController.get = function (req, res, next) {
|
|||||||
|
|
||||||
|
|
||||||
function renderEmail(req, res, next) {
|
function renderEmail(req, res, next) {
|
||||||
var emailsPath = path.join(nconf.get('views_dir'), 'emails');
|
|
||||||
|
|
||||||
async.parallel({
|
async.parallel({
|
||||||
emails: function (cb) {
|
emails: async.apply(emailer.getTemplates, meta.config),
|
||||||
async.waterfall([
|
|
||||||
function (next) {
|
|
||||||
file.walk(emailsPath, next);
|
|
||||||
},
|
|
||||||
function (emails, next) {
|
|
||||||
// exclude .js files
|
|
||||||
emails = emails.filter(function (email) {
|
|
||||||
return !email.endsWith('.js');
|
|
||||||
});
|
|
||||||
|
|
||||||
async.map(emails, function (email, next) {
|
|
||||||
var path = email.replace(emailsPath, '').substr(1).replace('.tpl', '');
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
function (next) {
|
|
||||||
fs.readFile(email, 'utf8', next);
|
|
||||||
},
|
|
||||||
function (original, next) {
|
|
||||||
var text = meta.config['email:custom:' + path] ? meta.config['email:custom:' + path] : original;
|
|
||||||
|
|
||||||
next(null, {
|
|
||||||
path: path,
|
|
||||||
fullpath: email,
|
|
||||||
text: text,
|
|
||||||
original: original,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
], next);
|
|
||||||
}, next);
|
|
||||||
},
|
|
||||||
], cb);
|
|
||||||
},
|
|
||||||
services: emailer.listServices,
|
services: emailer.listServices,
|
||||||
}, function (err, results) {
|
}, function (err, results) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
152
src/emailer.js
152
src/emailer.js
@@ -8,14 +8,19 @@ var nodemailer = require('nodemailer');
|
|||||||
var wellKnownServices = require('nodemailer/lib/well-known/services');
|
var wellKnownServices = require('nodemailer/lib/well-known/services');
|
||||||
var htmlToText = require('html-to-text');
|
var htmlToText = require('html-to-text');
|
||||||
var url = require('url');
|
var url = require('url');
|
||||||
|
var path = require('path');
|
||||||
|
var fs = require('fs');
|
||||||
|
|
||||||
var User = require('./user');
|
var User = require('./user');
|
||||||
var Plugins = require('./plugins');
|
var Plugins = require('./plugins');
|
||||||
var meta = require('./meta');
|
var meta = require('./meta');
|
||||||
var translator = require('./translator');
|
var translator = require('./translator');
|
||||||
var pubsub = require('./pubsub');
|
var pubsub = require('./pubsub');
|
||||||
|
var file = require('./file');
|
||||||
|
|
||||||
var transports = {
|
var Emailer = module.exports;
|
||||||
|
|
||||||
|
Emailer.transports = {
|
||||||
sendmail: nodemailer.createTransport({
|
sendmail: nodemailer.createTransport({
|
||||||
sendmail: true,
|
sendmail: true,
|
||||||
newline: 'unix',
|
newline: 'unix',
|
||||||
@@ -25,9 +30,45 @@ var transports = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var app;
|
var app;
|
||||||
var fallbackTransport;
|
|
||||||
|
|
||||||
var Emailer = module.exports;
|
var viewsDir = nconf.get('views_dir');
|
||||||
|
var emailsPath = path.join(viewsDir, 'emails');
|
||||||
|
|
||||||
|
Emailer.getTemplates = function (config, cb) {
|
||||||
|
async.waterfall([
|
||||||
|
function (next) {
|
||||||
|
file.walk(emailsPath, next);
|
||||||
|
},
|
||||||
|
function (emails, next) {
|
||||||
|
// exclude .js files
|
||||||
|
emails = emails.filter(function (email) {
|
||||||
|
return !email.endsWith('.js');
|
||||||
|
});
|
||||||
|
|
||||||
|
async.map(emails, function (email, next) {
|
||||||
|
var path = email.replace(emailsPath, '').substr(1).replace('.tpl', '');
|
||||||
|
|
||||||
|
async.waterfall([
|
||||||
|
function (next) {
|
||||||
|
fs.readFile(email, 'utf8', next);
|
||||||
|
},
|
||||||
|
function (original, next) {
|
||||||
|
var isCustom = !!config['email:custom:' + path];
|
||||||
|
var text = config['email:custom:' + path] || original;
|
||||||
|
|
||||||
|
next(null, {
|
||||||
|
path: path,
|
||||||
|
fullpath: email,
|
||||||
|
text: text,
|
||||||
|
original: original,
|
||||||
|
isCustom: isCustom,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
], next);
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
], cb);
|
||||||
|
};
|
||||||
|
|
||||||
Emailer.listServices = function (callback) {
|
Emailer.listServices = function (callback) {
|
||||||
var services = Object.keys(wellKnownServices);
|
var services = Object.keys(wellKnownServices);
|
||||||
@@ -71,13 +112,30 @@ Emailer.setupFallbackTransport = function (config) {
|
|||||||
smtpOptions.service = config['email:smtpTransport:service'];
|
smtpOptions.service = config['email:smtpTransport:service'];
|
||||||
}
|
}
|
||||||
|
|
||||||
transports.smtp = nodemailer.createTransport(smtpOptions);
|
Emailer.transports.smtp = nodemailer.createTransport(smtpOptions);
|
||||||
fallbackTransport = transports.smtp;
|
Emailer.fallbackTransport = Emailer.transports.smtp;
|
||||||
} else {
|
} else {
|
||||||
fallbackTransport = transports.sendmail;
|
Emailer.fallbackTransport = Emailer.transports.sendmail;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var prevConfig = meta.config;
|
||||||
|
function smtpSettingsChanged(config) {
|
||||||
|
var settings = [
|
||||||
|
'email:smtpTransport:enabled',
|
||||||
|
'email:smtpTransport:user',
|
||||||
|
'email:smtpTransport:pass',
|
||||||
|
'email:smtpTransport:service',
|
||||||
|
'email:smtpTransport:port',
|
||||||
|
'email:smtpTransport:host',
|
||||||
|
'email:smtpTransport:security',
|
||||||
|
];
|
||||||
|
|
||||||
|
return settings.some(function (key) {
|
||||||
|
return config[key] !== prevConfig[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Emailer.registerApp = function (expressApp) {
|
Emailer.registerApp = function (expressApp) {
|
||||||
app = expressApp;
|
app = expressApp;
|
||||||
|
|
||||||
@@ -97,16 +155,21 @@ Emailer.registerApp = function (expressApp) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
Emailer.setupFallbackTransport(meta.config);
|
Emailer.setupFallbackTransport(meta.config);
|
||||||
|
buildCustomTemplates(meta.config);
|
||||||
|
|
||||||
// Update default payload if new logo is uploaded
|
// Update default payload if new logo is uploaded
|
||||||
pubsub.on('config:update', function (config) {
|
pubsub.on('config:update', function (config) {
|
||||||
if (config) {
|
if (config) {
|
||||||
if ('email:smtpTransport:enabled' in config) {
|
|
||||||
Emailer.setupFallbackTransport(config);
|
|
||||||
}
|
|
||||||
Emailer._defaultPayload.logo.src = config['brand:emailLogo'];
|
Emailer._defaultPayload.logo.src = config['brand:emailLogo'];
|
||||||
Emailer._defaultPayload.logo.height = config['brand:emailLogo:height'];
|
Emailer._defaultPayload.logo.height = config['brand:emailLogo:height'];
|
||||||
Emailer._defaultPayload.logo.width = config['brand:emailLogo:width'];
|
Emailer._defaultPayload.logo.width = config['brand:emailLogo:width'];
|
||||||
|
|
||||||
|
if (smtpSettingsChanged(config)) {
|
||||||
|
Emailer.setupFallbackTransport(config);
|
||||||
|
}
|
||||||
|
buildCustomTemplates(config);
|
||||||
|
|
||||||
|
prevConfig = config;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,7 +213,7 @@ Emailer.sendToEmail = function (template, email, language, params, callback) {
|
|||||||
function (next) {
|
function (next) {
|
||||||
async.parallel({
|
async.parallel({
|
||||||
html: function (next) {
|
html: function (next) {
|
||||||
renderAndTranslate('emails/' + template, params, lang, next);
|
Emailer.renderAndTranslate(template, params, lang, next);
|
||||||
},
|
},
|
||||||
subject: function (next) {
|
subject: function (next) {
|
||||||
translator.translate(params.subject, lang, function (translated) {
|
translator.translate(params.subject, lang, function (translated) {
|
||||||
@@ -203,7 +266,7 @@ Emailer.sendViaFallback = function (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 + ')');
|
||||||
fallbackTransport.sendMail(data, function (err) {
|
Emailer.fallbackTransport.sendMail(data, function (err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
winston.error(err);
|
winston.error(err);
|
||||||
}
|
}
|
||||||
@@ -211,23 +274,64 @@ Emailer.sendViaFallback = function (data, callback) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function render(tpl, params, next) {
|
function buildCustomTemplates(config) {
|
||||||
var customTemplate = meta.config['email:custom:' + tpl.replace('emails/', '')];
|
async.waterfall([
|
||||||
if (customTemplate) {
|
function (next) {
|
||||||
Benchpress.compileParse(customTemplate, params, next);
|
Emailer.getTemplates(config, next);
|
||||||
} else {
|
},
|
||||||
app.render(tpl, params, next);
|
function (templates, next) {
|
||||||
}
|
templates = templates.filter(function (template) {
|
||||||
}
|
return template.isCustom && template.text !== prevConfig['email:custom:' + path];
|
||||||
|
});
|
||||||
|
async.each(templates, function (template, next) {
|
||||||
|
async.waterfall([
|
||||||
|
function (next) {
|
||||||
|
file.walk(viewsDir, next);
|
||||||
|
},
|
||||||
|
function (paths, next) {
|
||||||
|
paths = paths.reduce(function (obj, p) {
|
||||||
|
var relative = path.relative(viewsDir, p);
|
||||||
|
obj['/' + relative] = p;
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
meta.templates.processImports(paths, template.path, template.text, next);
|
||||||
|
},
|
||||||
|
function (source, next) {
|
||||||
|
Benchpress.precompile(source, {
|
||||||
|
minify: global.env !== 'development',
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function (compiled, next) {
|
||||||
|
fs.writeFile(template.fullpath.replace(/\.tpl$/, '.js'), compiled, next);
|
||||||
|
},
|
||||||
|
], next);
|
||||||
|
}, next);
|
||||||
|
},
|
||||||
|
function (next) {
|
||||||
|
Benchpress.flush();
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
], function (err) {
|
||||||
|
if (err) {
|
||||||
|
winston.error('[emailer] Failed to build custom email templates', err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
function renderAndTranslate(tpl, params, lang, callback) {
|
winston.verbose('[emailer] Built custom email templates');
|
||||||
render(tpl, params, function (err, html) {
|
|
||||||
translator.translate(html, lang, function (translated) {
|
|
||||||
callback(err, translated);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Emailer.renderAndTranslate = function (template, params, lang, callback) {
|
||||||
|
app.render('emails/' + template, params, function (err, html) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
translator.translate(html, lang, function (translated) {
|
||||||
|
callback(null, translated);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function getHostname() {
|
function getHostname() {
|
||||||
var configUrl = nconf.get('url');
|
var configUrl = nconf.get('url');
|
||||||
var parsed = url.parse(configUrl);
|
var parsed = url.parse(configUrl);
|
||||||
|
|||||||
@@ -15,40 +15,40 @@ var viewsPath = nconf.get('views_dir');
|
|||||||
|
|
||||||
var Templates = module.exports;
|
var Templates = module.exports;
|
||||||
|
|
||||||
|
function processImports(paths, templatePath, source, callback) {
|
||||||
|
var regex = /<!-- IMPORT (.+?) -->/;
|
||||||
|
|
||||||
|
var matches = source.match(regex);
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
return callback(null, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
var partial = '/' + matches[1];
|
||||||
|
if (paths[partial] && templatePath !== partial) {
|
||||||
|
fs.readFile(paths[partial], 'utf8', function (err, partialSource) {
|
||||||
|
if (err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
source = source.replace(regex, partialSource);
|
||||||
|
processImports(paths, templatePath, source, callback);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
winston.warn('[meta/templates] Partial not loaded: ' + matches[1]);
|
||||||
|
source = source.replace(regex, '');
|
||||||
|
|
||||||
|
processImports(paths, templatePath, source, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Templates.processImports = processImports;
|
||||||
|
|
||||||
Templates.compile = function (callback) {
|
Templates.compile = function (callback) {
|
||||||
callback = callback || function () {};
|
callback = callback || function () {};
|
||||||
|
|
||||||
var themeConfig = require(nconf.get('theme_config'));
|
var themeConfig = require(nconf.get('theme_config'));
|
||||||
var baseTemplatesPaths = themeConfig.baseTheme ? getBaseTemplates(themeConfig.baseTheme) : [nconf.get('base_templates_path')];
|
var baseTemplatesPaths = themeConfig.baseTheme ? getBaseTemplates(themeConfig.baseTheme) : [nconf.get('base_templates_path')];
|
||||||
|
|
||||||
function processImports(paths, relativePath, source, callback) {
|
|
||||||
var regex = /<!-- IMPORT (.+?) -->/;
|
|
||||||
|
|
||||||
var matches = source.match(regex);
|
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
return callback(null, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
var partial = '/' + matches[1];
|
|
||||||
if (paths[partial] && relativePath !== partial) {
|
|
||||||
fs.readFile(paths[partial], 'utf8', function (err, partialSource) {
|
|
||||||
if (err) {
|
|
||||||
return callback(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
source = source.replace(regex, partialSource);
|
|
||||||
|
|
||||||
processImports(paths, relativePath, source, callback);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
winston.warn('[meta/templates] Partial not loaded: ' + matches[1]);
|
|
||||||
source = source.replace(regex, '');
|
|
||||||
|
|
||||||
processImports(paths, relativePath, source, callback);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async.waterfall([
|
async.waterfall([
|
||||||
function (next) {
|
function (next) {
|
||||||
preparePaths(baseTemplatesPaths, next);
|
preparePaths(baseTemplatesPaths, next);
|
||||||
|
|||||||
141
test/emailer.js
Normal file
141
test/emailer.js
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var SMTPServer = require('smtp-server').SMTPServer;
|
||||||
|
var assert = require('assert');
|
||||||
|
var fs = require('fs');
|
||||||
|
var path = require('path');
|
||||||
|
|
||||||
|
var Plugins = require('../src/plugins');
|
||||||
|
var Emailer = require('../src/emailer');
|
||||||
|
var Meta = require('../src/meta');
|
||||||
|
|
||||||
|
describe('emailer', function () {
|
||||||
|
var onMail = function (address, session, callback) { callback(); };
|
||||||
|
var onTo = function (address, session, callback) { callback(); };
|
||||||
|
|
||||||
|
var template = 'test';
|
||||||
|
var email = 'test@example.org';
|
||||||
|
var language = 'en-GB';
|
||||||
|
var params = {
|
||||||
|
subject: 'Welcome to NodeBB',
|
||||||
|
};
|
||||||
|
|
||||||
|
before(function (done) {
|
||||||
|
var server = new SMTPServer({
|
||||||
|
allowInsecureAuth: true,
|
||||||
|
onAuth: function (auth, session, callback) {
|
||||||
|
callback(null, {
|
||||||
|
user: auth.username,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onMailFrom: function (address, session, callback) {
|
||||||
|
onMail(address, session, callback);
|
||||||
|
},
|
||||||
|
onRcptTo: function (address, session, callback) {
|
||||||
|
onTo(address, session, callback);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('error', function (err) {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
server.listen(4000, done);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: test sendmail here at some point
|
||||||
|
|
||||||
|
it('plugin hook should work', function (done) {
|
||||||
|
var error = new Error();
|
||||||
|
|
||||||
|
Plugins.registerHook('emailer-test', {
|
||||||
|
hook: 'filter:email.send',
|
||||||
|
method: function (data, next) {
|
||||||
|
assert(data);
|
||||||
|
assert.equal(data.to, email);
|
||||||
|
assert.equal(data.subject, params.subject);
|
||||||
|
|
||||||
|
next(error);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Emailer.sendToEmail(template, email, language, params, function (err) {
|
||||||
|
assert.equal(err, error);
|
||||||
|
|
||||||
|
Plugins.unregisterHook('emailer-test', 'filter:email.send');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build custom template on config change', function (done) {
|
||||||
|
var text = 'a random string of text';
|
||||||
|
|
||||||
|
// make sure it's not already set
|
||||||
|
Emailer.renderAndTranslate('test', {}, 'en-GB', function (err, output) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
assert.notEqual(output, text);
|
||||||
|
|
||||||
|
Meta.configs.set('email:custom:test', text, function (err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
// wait for pubsub stuff
|
||||||
|
setTimeout(function () {
|
||||||
|
Emailer.renderAndTranslate('test', {}, 'en-GB', function (err, output) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
assert.equal(output, text);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send via SMTP', function (done) {
|
||||||
|
var from = 'admin@example.org';
|
||||||
|
var username = 'another@example.com';
|
||||||
|
|
||||||
|
onMail = function (address, session, callback) {
|
||||||
|
assert.equal(address.address, from);
|
||||||
|
assert.equal(session.user, username);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
onTo = function (address, session, callback) {
|
||||||
|
assert.equal(address.address, email);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
done();
|
||||||
|
};
|
||||||
|
|
||||||
|
Meta.configs.setMultiple({
|
||||||
|
'email:smtpTransport:enabled': '1',
|
||||||
|
'email:smtpTransport:user': username,
|
||||||
|
'email:smtpTransport:service': 'nodebb-custom-smtp',
|
||||||
|
'email:smtpTransport:port': 4000,
|
||||||
|
'email:smtpTransport:host': 'localhost',
|
||||||
|
'email:smtpTransport:security': 'NONE',
|
||||||
|
'email:from': from,
|
||||||
|
}, function (err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
// delay so emailer has a chance to update after config changes
|
||||||
|
setTimeout(function () {
|
||||||
|
assert.equal(Emailer.fallbackTransport, Emailer.transports.smtp);
|
||||||
|
|
||||||
|
Emailer.sendToEmail(template, email, language, params, function (err) {
|
||||||
|
assert.ifError(err);
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
after(function (done) {
|
||||||
|
fs.unlinkSync(path.join(__dirname, '../build/public/templates/emails/test.js'));
|
||||||
|
Meta.configs.setMultiple({
|
||||||
|
'email:smtpTransport:enabled': '0',
|
||||||
|
'email:custom:test': '',
|
||||||
|
}, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user