From e39157b0ad434b3457b597267522ebca54741945 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Thu, 6 Sep 2018 15:58:34 -0700 Subject: [PATCH] Improved email system, mail templates in /meshcentral-data/mail-templates --- meshmail.js | 165 +++++++++++++++++++++------------- views/default.handlebars | 4 +- views/login-mobile.handlebars | 2 +- views/login.handlebars | 2 +- 4 files changed, 107 insertions(+), 66 deletions(-) diff --git a/meshmail.js b/meshmail.js index 11370a33..6550bd5f 100644 --- a/meshmail.js +++ b/meshmail.js @@ -22,23 +22,9 @@ module.exports.CreateMeshMail = function (parent) { obj.retry = 0; obj.sendingMail = false; obj.mailCookieEncryptionKey = null; + obj.mailTemplates = {}; const nodemailer = require('nodemailer'); - // Default account email validation mail - const accountCheckSubject = '[[[SERVERNAME]]] - Email Verification'; - const accountCheckMailHtml = '
[[[SERVERNAME]]] - Verification

Hi [[[USERNAME]]], [[[SERVERNAME]]] is requesting email verification, click on the following link to complete the process.

Click here to verify your e-mail address.

If you did not initiate this request, please ignore this mail.
'; - const accountCheckMailText = '[[[SERVERNAME]]] - Verification\r\n\r\nHi [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]]) is performing an e-mail verification. Nagivate to the following link to complete the process: [[[CALLBACKURL]]]\r\nIf you did not initiate this request, please ignore this mail.\r\n'; - - // Default account reset mail - const accountResetSubject = '[[[SERVERNAME]]] - Account Reset'; - const accountResetMailHtml = '
[[[SERVERNAME]]] - Verification

Hi [[[USERNAME]]], [[[SERVERNAME]]] is requesting an account password reset, click on the following link to complete the process.

Click here to reset your account password.

If you did not initiate this request, please ignore this mail.
'; - const accountResetMailText = '[[[SERVERNAME]]] - Account Reset\r\n\r\nHi [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]]) is requesting an account password reset. Nagivate to the following link to complete the process: [[[CALLBACKURL]]]\r\nIf you did not initiate this request, please ignore this mail.\r\n'; - - // Default account invite mail - const accountInviteSubject = '[[[SERVERNAME]]] - Invitation'; - const accountInviteMailHtml = '
[[[SERVERNAME]]] - Agent Installation

[[[INTROHTML]]]User [[[USERNAME]]] on server [[[SERVERNAME]]] is requesting you to download the following software to start the remote control session.

[[[MSGHTML]]][[[AGENTHTML]]]

If you did not initiate this request, please ignore this mail.

Best regards,
[[[USERNAME]]]
'; - const accountInviteMailText = '[[[SERVERNAME]]] - Agent Installation Invitation\r\n\r\n[[[INTROTEXT]]]User [[[USERNAME]]] on server [[[SERVERNAME]]] ([[[SERVERURL]]]) is requesting you to download the following software to start the remote control session. [[[MSGTEXT]]][[[AGENTTEXT]]]If you did not initiate this request, please ignore this mail.\r\n\r\nBest regards,\r\n[[[USERNAME]]]\r\n'; - function EscapeHtml(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(//g, '>').replace(/').replace(/\n/g, '').replace(/\t/g, '  '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; } @@ -48,36 +34,87 @@ module.exports.CreateMeshMail = function (parent) { if ((parent.config.smtp.user != null) && (parent.config.smtp.pass != null)) { options.auth = { user: parent.config.smtp.user, pass: parent.config.smtp.pass }; } obj.smtpServer = nodemailer.createTransport(options); + // Set default mail templates + // You can override these by placing a file with the same name in "meshcentral-data/mail" + // If the server hash many domains, just add the domainid to the file like this: 'account-check-customer1.html', 'mesh-invite-customer1.txt'. + obj.mailTemplates['account-check.html'] = '[[[SERVERNAME]]] - Email Verification\r\n
[[[SERVERNAME]]] - Verification

Hi [[[USERNAME]]], [[[SERVERNAME]]] is requesting email verification, click on the following link to complete the process.

Click here to verify your e-mail address.

If you did not initiate this request, please ignore this mail.
'; + obj.mailTemplates['account-reset.html'] = '[[[SERVERNAME]]] - Account Reset\r\n
[[[SERVERNAME]]] - Verification

Hi [[[USERNAME]]], [[[SERVERNAME]]] is requesting an account password reset, click on the following link to complete the process.

Click here to reset your account password.

If you did not initiate this request, please ignore this mail.
'; + obj.mailTemplates['mesh-invite.html'] = '[[[SERVERNAME]]] - Invitation\r\n
[[[SERVERNAME]]] - Agent Installation
[[[AREA-NAME]]]

Hello [[[NAME]]],

[[[/AREA-NAME]]]

User [[[USERNAME]]] on server [[[SERVERNAME]]] is requesting you to download the following software to start the remote control session.

[[[AREA-MSG]]]

Message: [[[MSG]]]

[[[/AREA-MSG]]][[[AREA-WINDOWS]]]

Click here to download the MeshAgent for Windows.

[[[/AREA-WINDOWS]]][[[AREA-LINUX]]]

For Linux, cut & paste the following in a terminal to install the agent:

wget -q [[[SERVERURL]]]/meshagents?script=1 --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] [[[MESHIDHEX]]]

[[[/AREA-LINUX]]]

If you did not initiate this request, please ignore this mail.

Best regards,
[[[USERNAME]]]
'; + obj.mailTemplates['account-check.txt'] = '[[[SERVERNAME]]] - Email Verification\r\nHi [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]]) is performing an e-mail verification. Nagivate to the following link to complete the process:\r\n\r\n[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]\r\n\r\nIf you did not initiate this request, please ignore this mail.\r\n'; + obj.mailTemplates['account-reset.txt'] = '[[[SERVERNAME]]] - Account Reset\r\nHi [[[USERNAME]]], [[[SERVERNAME]]] ([[[SERVERURL]]]) is requesting an account password reset. Nagivate to the following link to complete the process:\r\n\r\n[[[SERVERURL]]]/checkmail?c=[[[COOKIE]]]\r\n\r\nIf you did not initiate this request, please ignore this mail.'; + obj.mailTemplates['mesh-invite.txt'] = '[[[SERVERNAME]]] - Invitation\r\n[[[AREA-NAME]]]Hello [[[NAME]]],\r\n\r\n[[[/AREA-NAME]]]User [[[USERNAME]]] on server [[[SERVERNAME]]] ([[[SERVERURL]]]/) is requesting you to download the following software to start the remote control session.[[[AREA-MSG]]]\r\n\r\nMessage: [[[MSG]]]\r\n\r\n[[[/AREA-MSG]]][[[AREA-WINDOWS]]]For Windows, nagivate to the following link to complete the process:\r\n\r\n[[[SERVERURL]]]/meshagents?id=3&meshid=[[[MESHIDHEX]]]&tag=mailto:[[[EMAIL]]]\r\n\r\n[[[/AREA-WINDOWS]]][[[AREA-LINUX]]]For Linux, cut & paste the following in a terminal to install the agent:\r\n\r\nwget -q [[[SERVERURL]]]/meshagents?script=1 --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] [[[MESHIDHEX]]]\r\n\r\n[[[/AREA-LINUX]]]If you did not initiate this request, please ignore this mail.\r\n\r\nBest regards,\r\n[[[USERNAME]]]'; + + // Load all of the mail templates if present + if (obj.parent.fs.existsSync(obj.parent.path.join(obj.parent.datapath, 'mail-templates'))) { + var mailDir = null; + try { mailDir = obj.parent.fs.readdirSync(obj.parent.path.join(obj.parent.datapath, 'mail-templates')); } catch (e) { } + if (mailDir != null) { + // Load all mail templates + for (var i in mailDir) { + var templateName = mailDir[i].toLowerCase(); + if (templateName.endsWith('.html') || templateName.endsWith('.txt')) { obj.mailTemplates[templateName] = obj.parent.fs.readFileSync(obj.parent.path.join(obj.parent.datapath, 'mail-templates', mailDir[i])).toString(); } + } + } + } else { + // Save the default templates + try { + obj.parent.fs.mkdirSync(obj.parent.path.join(obj.parent.datapath, 'mail-templates')); + for (var i in obj.mailTemplates) { obj.parent.fs.writeFileSync(obj.parent.path.join(obj.parent.datapath, 'mail-templates', i), obj.mailTemplates[i], 'utf8'); } + } catch (e) { console.error(e); } + } + + // Get the correct mail template + function getTemplate(name, domain, html) { + if (domain != null) { var r = obj.mailTemplates[name + '-' + domain.id + (html ? '.html' : '.txt')]; if (r) return r; } + return obj.mailTemplates[name + (html ? '.html' : '.txt')]; + } + + // Get the correct mail template object + function getTemplateEx(name, domain) { + var r = {}, txt = getTemplate(name, domain, 0), html = getTemplate(name, domain, 1); + r.txtSubject = txt.split('\r\n')[0]; + r.htmlSubject = getStrBetween(html, '', '\r\n'); + r.txt = txt.substring(txt.indexOf('\r\n') + 2); + r.html = html.substring(html.indexOf('\r\n') + 2); + return r; + } + + // Get the string between two markers + function getStrBetween(str, start, end) { + var si = str.indexOf(start), ei = str.indexOf(end); + if ((si == -1) || (ei == -1) || (si > ei)) return null; + return str.substring(si + start.length, ei); + } + + // Remove the string between two markers + function removeStrBetween(str, start, end) { + var si = str.indexOf(start), ei = str.indexOf(end); + if ((si == -1) || (ei == -1) || (si > ei)) return str; + return str.substring(0, si) + str.substring(ei + end.length); + } + + // Remove the string between two markers + function strZone(str, marker, keep) { + marker = marker.toUpperCase(); + if (keep) { return str.split('[[[AREA-' + marker + ']]]').join('').split('[[[/AREA-' + marker + ']]]').join(''); } + return removeStrBetween(str, '[[[AREA-' + marker + ']]]', '[[[/AREA-' + marker + ']]]'); + } + // Perform all e-mail substitution - function mailReplacements(text, domain, username, email, options) { - var url; + function mailReplacements(text, domain, options) { if (domain.dns == null) { // Default domain or subdomain of the default. - url = 'http' + ((obj.parent.args.notls == null) ? 's' : '') + '://' + parent.certificates.CommonName + ':' + obj.parent.args.port + domain.url; + options.serverurl = 'http' + ((obj.parent.args.notls == null) ? 's' : '') + '://' + obj.parent.certificates.CommonName + ':' + obj.parent.args.port + domain.url; } else { // Domain with a DNS name. - url = 'http' + ((obj.parent.args.notls == null) ? 's' : '') + '://' + domain.dns + ':' + obj.parent.args.port + domain.url; + options.serverurl = 'http' + ((obj.parent.args.notls == null) ? 's' : '') + '://' + domain.dns + ':' + obj.parent.args.port + domain.url; } - if (options) { - if (options.cookie == null) { options.cookie = ''; } - if (options.agentlinkhtml == null) { options.agentlinkhtml = ''; } - if (options.agentlinktext == null) { options.agentlinktext = ''; } - if (options.meshid == null) { options.meshid = ''; } - if (options.introtext == null) { options.introtext = ''; } - if (options.introhtml == null) { options.introhtml = ''; } - if (options.msgtext == null) { options.msgtext = ''; } - if (options.msghtml == null) { options.msghtml = ''; } - - text = text.split('[[[CALLBACKURL]]]').join(url + 'checkmail?c=' + options.cookie); - text = text.split('[[[AGENTHTML]]]').join(options.agentlinkhtml); - text = text.split('[[[AGENTTEXT]]]').join(options.agentlinktext); - text = text.split('[[[MESHID]]]').join(options.meshid); - text = text.split('[[[INTROTEXT]]]').join(options.introtext); - text = text.split('[[[INTROHTML]]]').join(options.introhtml); - text = text.split('[[[MSGTEXT]]]').join(options.msgtext); - text = text.split('[[[MSGHTML]]]').join(options.msghtml); + if (options.serverurl.endsWith('/')) { options.serverurl = options.serverurl.substring(0, options.serverurl.length - 1); } // Remove the ending / if present + for (var i in options) { + text = strZone(text, i.toUpperCase(), options[i]); // Adjust this text area + text = text.split('[[[' + i.toUpperCase() + ']]]').join(options[i]); // Replace this value } - return text.split('[[[USERNAME]]]').join(username).split('[[[SERVERURL]]]').join(url).split('[[[SERVERNAME]]]').join(domain.title).split('[[[EMAIL]]]').join(EscapeHtml(email)).split('[[[URL]]]').join(url); + return text; } // Send a mail @@ -88,40 +125,44 @@ module.exports.CreateMeshMail = function (parent) { // Send account check mail obj.sendAccountCheckMail = function (domain, username, email) { - if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName == 'un-configured')) return; // If the server name is not set, no reset possible. - var cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username, e: email, a: 1 }, obj.mailCookieEncryptionKey); - obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(accountCheckSubject, domain, username, email), text: mailReplacements(accountCheckMailText, domain, username, email, { cookie: cookie }), html: mailReplacements(accountCheckMailHtml, domain, username, email, { cookie: cookie }) }); + var template = getTemplateEx('account-check', domain); + if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName == 'un-configured')) return; // If the server name is not set, no reset possible. + + // Set all the options. + var options = { username: username, email: email, servername: domain.title }; + options.cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username, e: email, a: 1 }, obj.mailCookieEncryptionKey); + + // Send the email + obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) }); sendNextMail(); }; // Send account reset mail obj.sendAccountResetMail = function (domain, username, email) { - if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName == 'un-configured')) return; // If the server name is not set, don't validate the email address. - var cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username, e: email, a: 2 }, obj.mailCookieEncryptionKey); - obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(accountResetSubject, domain, username, email), text: mailReplacements(accountResetMailText, domain, username, email, { cookie: cookie }), html: mailReplacements(accountResetMailHtml, domain, username, email, { cookie: cookie }) }); + var template = getTemplateEx('account-reset', domain); + if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName == 'un-configured')) return; // If the server name is not set, don't validate the email address. + + // Set all the options. + var options = { username: username, email: email, servername: domain.title }; + options.cookie = obj.parent.encodeCookie({ u: domain.id + '/' + username, e: email, a: 2 }, obj.mailCookieEncryptionKey); + + // Send the email + obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) }); sendNextMail(); }; // Send agent invite mail obj.sendAgentInviteMail = function (domain, username, email, meshid, name, os, msg) { - if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName == 'un-configured')) return; // If the server name is not set, can't do this. - var options = { meshid: meshid.split('/')[2] }; - var agentLinkHtml = ''; - var agentLinkText = ''; - if (os == 0 || os == 1) { // All OS or Windows - agentLinkHtml += '

Click here to download the MeshAgent for Windows.

'; - agentLinkText += 'For Windows, nagivate to the following link to complete the process:\r\n\r\n[[[SERVERURL]]]/meshagents?id=3&meshid=[[[MESHID]]]&tag=mailto:[[[EMAIL]]]\r\n\r\n'; - } - if (os == 0 || os == 2) { // All OS or Linux - agentLinkHtml += '

For Linux, cut & paste the following in a terminal to install the agent:

wget -q [[[SERVERURL]]]/meshagents?script=1 --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHID]]]\'

'; - agentLinkText += 'For Linux, cut & paste the following in a terminal to install the agent:\r\n\r\nwget -q [[[SERVERURL]]]/meshagents?script=1 --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh [[[SERVERURL]]] \'[[[MESHID]]]\'\r\n\r\n'; - } - options.agentlinkhtml = agentLinkHtml; - options.agentlinktext = agentLinkText; - if ((name != null) && (name != '')) { options.introtext = 'Hello ' + name + ',\r\n\r\n'; options.introhtml = '

Hello ' + name + ',

'; } - if ((msg != null) && (msg != '')) { options.msgtext = '\r\n\r\nMessage: ' + msg + '\r\n\r\n'; options.msghtml = '

Message: ' + msg + '

'; } - var mail = { to: email, from: parent.config.smtp.from, subject: mailReplacements(accountInviteSubject, domain, username, email), text: mailReplacements(accountInviteMailText, domain, username, email, options), html: mailReplacements(accountInviteMailHtml, domain, username, email, options) }; - obj.pendingMails.push(mail); + var template = getTemplateEx('mesh-invite', domain); + if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null) || (parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName == 'un-configured')) return; // If the server name is not set, don't validate the email address. + + // Set all the template replacement options and generate the final email text (both in txt and html formats). + var options = { username: username, name: name, email: email, msg: msg, meshid: meshid, meshidhex: meshid.split('/')[2], servername: domain.title }; + options.linux = ((os == 0) || (os == 2)) ? 1 : 0; + options.windows = ((os == 0) || (os == 1)) ? 1 : 0; + + // Send the email + obj.pendingMails.push({ to: email, from: parent.config.smtp.from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) }); sendNextMail(); }; diff --git a/views/default.handlebars b/views/default.handlebars index 74f1087a..0fb56911 100644 --- a/views/default.handlebars +++ b/views/default.handlebars @@ -2016,11 +2016,11 @@ if (serverinfo.https == true) { var portStr = (serverinfo.port == 443)?'':(":" + serverinfo.port); - Q('agins_linux_area').value = "wget -q https://" + servername + portStr + "/meshagents?script=1 --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh https://" + servername + portStr + " '" + meshid.split('/')[2] + "'\r\n"; + Q('agins_linux_area').value = "wget -q https://" + servername + portStr + "/meshagents?script=1 --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh https://" + servername + portStr + " " + meshid.split('/')[2] + "\r\n"; Q('agins_linux_area_un').value = "wget -q https://" + servername + portStr + "/meshagents?script=1 --no-check-certificate -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh uninstall\r\n"; } else { var portStr = (serverinfo.port == 80)?'':(":" + serverinfo.port); - Q('agins_linux_area').value = "wget -q http://" + servername + portStr + "/meshagents?script=1 -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh http://" + servername + portStr + " '" + meshid.split('/')[2] + "'\r\n"; + Q('agins_linux_area').value = "wget -q http://" + servername + portStr + "/meshagents?script=1 -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh http://" + servername + portStr + " " + meshid.split('/')[2] + "\r\n"; Q('agins_linux_area_un').value = "wget -q http://" + servername + portStr + "/meshagents?script=1 -O ./meshinstall.sh && chmod 755 ./meshinstall.sh && sudo ./meshinstall.sh uninstall\r\n"; } Q('aginsSelect').focus(); diff --git a/views/login-mobile.handlebars b/views/login-mobile.handlebars index 07efce09..c1a840ac 100644 --- a/views/login-mobile.handlebars +++ b/views/login-mobile.handlebars @@ -177,7 +177,7 @@ 'use strict'; var passhint = "{{{passhint}}}"; var newAccountPass = parseInt('{{{newAccountPass}}}'); - var emailCheck = parseInt('{{{emailcheck}}}'); + var emailCheck = ('{{{emailcheck}}}' == 'true'); var features = parseInt('{{{features}}}'); function startup() { diff --git a/views/login.handlebars b/views/login.handlebars index b24a6e79..74cb234f 100644 --- a/views/login.handlebars +++ b/views/login.handlebars @@ -171,7 +171,7 @@ 'use strict'; var passhint = "{{{passhint}}}"; var newAccountPass = parseInt('{{{newAccountPass}}}'); - var emailCheck = parseInt('{{{emailcheck}}}'); + var emailCheck = ('{{{emailcheck}}}' == 'true'); var features = parseInt('{{{features}}}'); function startup() {