From 12585229a35bd7f9d7c8ef2485518312e39ca183 Mon Sep 17 00:00:00 2001 From: Andris Reinman Date: Mon, 31 Jul 2017 14:16:50 +0300 Subject: [PATCH] Allow specifying defualt emails for created users --- .gitignore | 1 + api.js | 4 +- emails/01.json.example | 17 ++++++++ emails/README.md | 23 +++++++++++ lib/tools.js | 92 +++++++++++++++++++++++++++++++++++++++++- lib/user-handler.js | 71 +++++++++++++++++++++++++++++++- package.json | 1 + 7 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 emails/01.json.example create mode 100644 emails/README.md diff --git a/.gitignore b/.gitignore index afca561e..465b35b8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ npm-debug.log .npmrc config/production.* config/development.* +emails/*.json diff --git a/api.js b/api.js index ab17541b..a08a9a92 100644 --- a/api.js +++ b/api.js @@ -100,9 +100,9 @@ module.exports = done => { database: db.database, redis: db.redis }); - userHandler = new UserHandler({ database: db.database, users: db.users, redis: db.redis }); - mailboxHandler = new MailboxHandler({ database: db.database, users: db.users, redis: db.redis, notifier }); messageHandler = new MessageHandler({ database: db.database, gridfs: db.gridfs, redis: db.redis }); + userHandler = new UserHandler({ database: db.database, users: db.users, redis: db.redis, messageHandler }); + mailboxHandler = new MailboxHandler({ database: db.database, users: db.users, redis: db.redis, notifier }); usersRoutes(db, server, userHandler); addressesRoutes(db, server); diff --git a/emails/01.json.example b/emails/01.json.example new file mode 100644 index 00000000..21ec7dbb --- /dev/null +++ b/emails/01.json.example @@ -0,0 +1,17 @@ +{ + "mailbox": "INBOX", + "seen": true, + "flag": true, + + "from": { + "name": "Support", + "address": "info@example.com" + }, + "to": { + "name": "[NAME]", + "address": "[EMAIL]" + }, + "subject": "[FNAME], welcome to our awesome service!", + "text": "[FNAME], your new email account [EMAIL] is now ready to be used!", + "html": "

[FNAME], your new email account [EMAIL] is now ready to be used!

" +} diff --git a/emails/README.md b/emails/README.md new file mode 100644 index 00000000..fa3cccbf --- /dev/null +++ b/emails/README.md @@ -0,0 +1,23 @@ +# Default messages + +Add here messages that should be inserted to new users INBOX. Messages are formatted according to [Nodemailer message structure](https://nodemailer.com/message/) and sorted by filename. Only files with .json extension are used. + +All string values can take the following template tags (case sensitive): + +- **[USERNAME]** will be replaced by the username of the user +- **[DOMAIN]** will be replaced by the service domain +- **[EMAIL]** will be replaced by the email address of the user +- **[NAME]** will be replaced by the registered name of the user +- **[FNAME]** will be replaced by the first part of the registered name of the user + +You can also specify some extra options with the mail data object + +- **flag** is a boolean. If true, then the message is flagged +- **seen** is a boolean. If true, then the message is marked as seen +- **mailbox** is a string with one of the following values (case insensitive): + - **'INBOX'** (the default) to store the message to INBOX + - **'Sent'** to store the message to the Sent Mail folder + - **'Trash'** to store the message to the Trash folder + - **'Junk'** to store the message to the Spam folder + - **'Drafts'** to store the message to the Drafts folder + - **'Archive'** to store the message to the Archive folder diff --git a/lib/tools.js b/lib/tools.js index 57561cff..ce3d3f83 100644 --- a/lib/tools.js +++ b/lib/tools.js @@ -3,6 +3,11 @@ const punycode = require('punycode'); const libmime = require('libmime'); const consts = require('./consts'); +const fs = require('fs'); +const he = require('he'); +const pathlib = require('path'); + +let templates = false; function checkRangeQuery(uids, ne) { // check if uids is a straight continous array and if such then return a range query, @@ -156,10 +161,95 @@ function getMailboxCounter(db, mailbox, type, done) { }); } +function renderEmailTemplate(tags, template) { + let result = JSON.parse(JSON.stringify(template)); + + let walk = (node, nodeKey) => { + if (!node) { + return; + } + + Object.keys(node || {}).forEach(key => { + if (!node[key]) { + return; + } + + if (Array.isArray(node[key])) { + return node[key].forEach(child => walk(child, nodeKey)); + } + + if (typeof node[key] === 'object') { + return walk(node[key], key); + } + + if (typeof node[key] === 'string') { + let isHTML = /html/i.test(key); + node[key] = node[key].replace(/\[([^\]]+)\]/g, (match, tag) => { + if (tag in tags) { + return isHTML ? he.encode(tags[tag]) : tags[tag]; + } + return match; + }); + return; + } + }); + }; + + walk(result, false); + + return result; +} + +function getEmailTemplates(tags, callback) { + if (templates) { + return callback(null, templates.map(template => renderEmailTemplate(tags, template))); + } + let templateFolder = pathlib.join(__dirname, '..', 'emails'); + fs.readdir(templateFolder, (err, files) => { + if (err) { + return callback(err); + } + + files = files.sort((a, b) => a.localeCompare(b)); + + let pos = 0; + let newTemplates = []; + let checkFiles = () => { + if (pos >= files.length) { + templates = newTemplates; + return callback(null, templates.map(template => renderEmailTemplate(tags, template))); + } + let file = files[pos++]; + if (!/\.json$/i.test(file)) { + return checkFiles(); + } + fs.readFile(pathlib.join(templateFolder, file), 'utf-8', (err, email) => { + if (err) { + // ignore? + return checkFiles(); + } + let parsed; + try { + parsed = JSON.parse(email); + } catch (E) { + //ignore? + } + if (parsed) { + newTemplates.push(parsed); + } + return checkFiles(); + }); + }; + + checkFiles(); + }); +} + module.exports = { normalizeAddress, redisConfig, checkRangeQuery, decodeAddresses, - getMailboxCounter + getMailboxCounter, + getEmailTemplates }; diff --git a/lib/user-handler.js b/lib/user-handler.js index 0c532a1d..aecbe63a 100644 --- a/lib/user-handler.js +++ b/lib/user-handler.js @@ -13,12 +13,14 @@ const os = require('os'); const crypto = require('crypto'); const mailboxTranslations = require('./translations'); const base32 = require('base32.js'); +const MailComposer = require('nodemailer/lib/mail-composer'); class UserHandler { constructor(options) { this.database = options.database; this.users = options.users || options.database; this.redis = options.redis; + this.messageHandler = options.messageHandler; } /** @@ -463,7 +465,20 @@ class UserHandler { return callback(new Error('Database Error, failed to create user')); } - return callback(null, id); + if (!this.messageHandler) { + return callback(null, id); + } + + this.pushDefaultMessages( + id, + { + NAME: data.name || address, + FNAME: (data.name || '').trim().replace(/\s+/g, ' ').split(' ').shift() || address, + DOMAIN: address.substr(address.indexOf('@') + 1), + EMAIL: address + }, + () => callback(null, id) + ); }); }); }); @@ -471,6 +486,60 @@ class UserHandler { }); } + pushDefaultMessages(user, tags, callback) { + tools.getEmailTemplates(tags, (err, messages) => { + if (err || !messages || !messages.length) { + return callback(); + } + + let pos = 0; + let insertMessages = () => { + if (pos >= messages.length) { + return callback(); + } + let data = messages[pos++]; + let compiler = new MailComposer(data); + + compiler.compile().build((err, message) => { + if (err) { + return insertMessages(); + } + + let mailboxQueryKey = 'path'; + let mailboxQueryValue = 'INBOX'; + + if (['sent', 'trash', 'junk', 'drafts', 'archive'].includes((data.mailbox || '').toString().toLowerCase())) { + mailboxQueryKey = 'specialUse'; + mailboxQueryValue = '\\' + data.mailbox.toLowerCase().replace(/^./g, c => c.toUpperCase()); + } + + let flags = []; + if (data.seen) { + flags.push('\\Seen'); + } + if (data.flag) { + flags.push('\\Flagged'); + } + + this.messageHandler.add( + { + user, + [mailboxQueryKey]: mailboxQueryValue, + meta: { + source: 'AUTO', + time: Date.now() + }, + flags, + raw: message + }, + insertMessages + ); + }); + }; + insertMessages(); + }); + } + reset(username, callback) { let password = generatePassword.generate({ length: 12, diff --git a/package.json b/package.json index ab97b7e1..cd191f89 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "addressparser": "^1.0.1", "bcryptjs": "^2.4.3", "generate-password": "^1.3.0", + "he": "^1.1.1", "html-to-text": "^3.3.0", "iconv-lite": "^0.4.18", "joi": "^10.6.0",