meshcentral
Version:
Web based remote computer management server
784 lines (690 loc) • 46.3 kB
JavaScript
/**
* @description MeshCentral e-mail server communication modules
* @author Ylian Saint-Hilaire
* @copyright Intel Corporation 2018-2022
* @license Apache-2.0
* @version v0.0.1
*/
/*xjslint node: true */
/*xjslint plusplus: true */
/*xjslint maxlen: 256 */
/*jshint node: true */
/*jshint strict: false */
/*jshint esversion: 6 */
;
// TODO: Add NTML support with "nodemailer-ntlm-auth" https://github.com/nodemailer/nodemailer-ntlm-auth
// Construct a MeshAgent object, called upon connection
module.exports.CreateMeshMail = function (parent, domain) {
var obj = {};
obj.pendingMails = [];
obj.parent = parent;
obj.retry = 0;
obj.sendingMail = false;
obj.mailCookieEncryptionKey = null;
obj.verifyemail = false;
obj.domain = domain;
obj.emailDelay = 5 * 60 * 1000; // Default of 5 minute email delay.
//obj.mailTemplates = {};
const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
const constants = (obj.parent.crypto.constants ? obj.parent.crypto.constants : require('constants')); // require('constants') is deprecated in Node 11.10, use require('crypto').constants instead.
// Safe JSON stringify resistant to circular references.
// Node TLS errors (e.g. SMTP cert mismatch) include the certificate chain, where each X509 cert has an 'issuerCertificate' field pointing back at its issuer, closing the cycle. Plain JSON.stringify throws "Converting circular structure to JSON" and crashes the server. This helper drops cert-related fields and tracks seen objects via a WeakSet.
function safeStringify(o) { try { const seen = new WeakSet(); return JSON.stringify(o, function (k, v) { if (typeof v === 'object' && v !== null) { if (seen.has(v)) return '[Circular]'; seen.add(v); } if (k === 'issuerCertificate' || k === 'cert' || k === 'raw') return '[Cert]'; return v; }); } catch (ex) { return '[stringify-error: ' + ex.message + ']'; } }
function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, '''); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
//function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
// Setup where we read our configuration from
if (obj.domain == null) { obj.config = parent.config; } else { obj.config = domain; }
if (obj.config.sendgrid != null) {
// Setup SendGrid mail server
obj.sendGridServer = require('@sendgrid/mail');
obj.sendGridServer.setApiKey(obj.config.sendgrid.apikey);
if (obj.config.sendgrid.verifyemail == true) { obj.verifyemail = true; }
if ((typeof obj.config.sendgrid.emaildelayseconds == 'number') && (obj.config.sendgrid.emaildelayseconds > 0)) { obj.emailDelay = obj.config.sendgrid.emaildelayseconds * 1000; }
} else if (obj.config.smtp != null) {
// Setup SMTP mail server
if ((typeof obj.config.smtp.emaildelayseconds == 'number') && (obj.config.smtp.emaildelayseconds > 0)) { obj.emailDelay = obj.config.smtp.emaildelayseconds * 1000; }
if (obj.config.smtp.name == 'console') {
// This is for debugging, the mails will be displayed on the console
obj.smtpServer = 'console';
} else {
const nodemailer = require('nodemailer');
var options = { name: obj.config.smtp.name, host: obj.config.smtp.host, secure: (obj.config.smtp.tls == true), tls: {} };
//var options = { host: obj.config.smtp.host, secure: (obj.config.smtp.tls == true), tls: { secureProtocol: 'SSLv23_method', ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE, rejectUnauthorized: false } };
if (obj.config.smtp.port != null) { options.port = obj.config.smtp.port; }
if (obj.config.smtp.tlscertcheck === false) { options.tls.rejectUnauthorized = false; }
if (obj.config.smtp.tlsstrict === true) { options.tls.secureProtocol = 'SSLv23_method'; options.tls.ciphers = 'RSA+AES:!aNULL:!MD5:!DSS'; options.tls.secureOptions = constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE; }
if ((obj.config.smtp.auth != null) && (typeof obj.config.smtp.auth == 'object')) {
var user = obj.config.smtp.from;
if ((user == null) && (obj.config.smtp.user != null)) { user = obj.config.smtp.user; }
if ((obj.config.smtp.auth.user != null) && (typeof obj.config.smtp.auth.user == 'string')) { user = obj.config.smtp.auth.user; }
if (user.toLowerCase().endsWith('@gmail.com')) { options = { service: 'gmail', auth: { user: user } }; obj.config.smtp.host = 'gmail'; } else { options.auth = { user: user } }
if (obj.config.smtp.auth.type) { options.auth.type = obj.config.smtp.auth.type; }
if (obj.config.smtp.auth.clientid) { options.auth.clientId = obj.config.smtp.auth.clientid; options.auth.type = 'OAuth2'; }
if (obj.config.smtp.auth.clientsecret) { options.auth.clientSecret = obj.config.smtp.auth.clientsecret; }
if (obj.config.smtp.auth.refreshtoken) { options.auth.refreshToken = obj.config.smtp.auth.refreshtoken; }
}
else if ((obj.config.smtp.user != null) && (obj.config.smtp.pass != null)) { options.auth = { user: obj.config.smtp.user, pass: obj.config.smtp.pass }; }
if (obj.config.smtp.verifyemail == true) { obj.verifyemail = true; }
obj.smtpServer = nodemailer.createTransport(options);
}
} else if (obj.config.sendmail != null) {
// Setup Sendmail
if ((typeof obj.config.sendmail.emaildelayseconds == 'number') && (obj.config.sendmail.emaildelayseconds > 0)) { obj.emailDelay = obj.config.sendmail.emaildelayseconds * 1000; }
const nodemailer = require('nodemailer');
var options = { sendmail: true };
if (typeof obj.config.sendmail.newline == 'string') { options.newline = obj.config.sendmail.newline; }
if (typeof obj.config.sendmail.path == 'string') { options.path = obj.config.sendmail.path; }
if (Array.isArray(obj.config.sendmail.args)) { options.args = obj.config.sendmail.args; }
obj.smtpServer = nodemailer.createTransport(options);
}
// Get the correct mail template object
function getTemplate(name, domain, lang) {
parent.debug('email', 'Getting mail template for: ' + name + ', lang: ' + lang);
if (Array.isArray(lang)) { lang = lang[0]; } // TODO: For now, we only use the first language given.
if (lang != null) { lang = lang.split('-')[0]; } // Take the first part of the language, "xx-xx"
var r = {}, emailsPath = null;
if ((domain != null) && (domain.webemailspath != null)) { emailsPath = domain.webemailspath; }
else if (obj.parent.webEmailsOverridePath != null) { emailsPath = obj.parent.webEmailsOverridePath; }
else if (obj.parent.webEmailsPath != null) { emailsPath = obj.parent.webEmailsPath; }
if ((emailsPath == null) || (obj.parent.fs.existsSync(emailsPath) == false)) { return null }
// Get the non-english email if needed
var htmlfile = null, txtfile = null;
if ((lang != null) && (lang != 'en')) {
var translationsPath = obj.parent.path.join(emailsPath, 'translations');
var translationsPathHtml = obj.parent.path.join(emailsPath, 'translations', name + '_' + lang + '.html');
var translationsPathTxt = obj.parent.path.join(emailsPath, 'translations', name + '_' + lang + '.txt');
if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathHtml) && obj.parent.fs.existsSync(translationsPathTxt)) {
htmlfile = obj.parent.fs.readFileSync(translationsPathHtml).toString();
txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString();
}
}
// Get the english email
if ((htmlfile == null) || (txtfile == null)) {
var pathHtml = obj.parent.path.join(emailsPath, name + '.html');
var pathTxt = obj.parent.path.join(emailsPath, name + '.txt');
if (obj.parent.fs.existsSync(pathHtml) && obj.parent.fs.existsSync(pathTxt)) {
htmlfile = obj.parent.fs.readFileSync(pathHtml).toString();
txtfile = obj.parent.fs.readFileSync(pathTxt).toString();
}
}
// If no email template found, use the default translated email template
if (((htmlfile == null) || (txtfile == null)) && (lang != null) && (lang != 'en')) {
var translationsPath = obj.parent.path.join(obj.parent.webEmailsPath, 'translations');
var translationsPathHtml = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', name + '_' + lang + '.html');
var translationsPathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', name + '_' + lang + '.txt');
if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathHtml) && obj.parent.fs.existsSync(translationsPathTxt)) {
htmlfile = obj.parent.fs.readFileSync(translationsPathHtml).toString();
txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString();
}
}
// If no translated email template found, use the default email template
if ((htmlfile == null) || (txtfile == null)) {
var pathHtml = obj.parent.path.join(obj.parent.webEmailsPath, name + '.html');
var pathTxt = obj.parent.path.join(obj.parent.webEmailsPath, name + '.txt');
if (obj.parent.fs.existsSync(pathHtml) && obj.parent.fs.existsSync(pathTxt)) {
htmlfile = obj.parent.fs.readFileSync(pathHtml).toString();
txtfile = obj.parent.fs.readFileSync(pathTxt).toString();
}
}
// No email templates
if ((htmlfile == null) || (txtfile == null)) { return null; }
// Decode the HTML file
htmlfile = htmlfile.split('<html>').join('').split('</html>').join('').split('<head>').join('').split('</head>').join('').split('<body>').join('').split('</body>').join('').split(' notrans="1"').join('');
var lines = htmlfile.split('\r\n').join('\n').split('\n');
r.htmlSubject = lines.shift();
if (r.htmlSubject.startsWith('<div>')) { r.htmlSubject = r.htmlSubject.substring(5); }
if (r.htmlSubject.endsWith('</div>')) { r.htmlSubject = r.htmlSubject.substring(0, r.htmlSubject.length - 6); }
r.html = lines.join('\r\n');
// Decode the TXT file
lines = txtfile.split('\r\n').join('\n').split('\n');
r.txtSubject = lines.shift();
var txtbody = [];
for (var i in lines) { var line = lines[i]; if ((line.length > 0) && (line[0] == '~')) { txtbody.push(line.substring(1)); } else { txtbody.push(line); } }
r.txt = txtbody.join('\r\n');
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);
}
// Keep or remove all lines between two lines with markers
function strZone(str, marker, keep) {
var lines = str.split('\r\n'), linesEx = [], removing = false;
const startMarker = '<area-' + marker + '>', endMarker = '</area-' + marker + '>';
for (var i in lines) {
var line = lines[i];
if (removing) {
if (line.indexOf(endMarker) >= 0) { removing = false; } else { if (keep) { linesEx.push(line); } }
} else {
if (line.indexOf(startMarker) >= 0) { removing = true; } else { linesEx.push(line); }
}
}
return linesEx.join('\r\n');
}
// Perform all e-mail substitution
function mailReplacements(text, domain, options) {
var httpsport = (typeof obj.parent.args.aliasport == 'number') ? obj.parent.args.aliasport : obj.parent.args.port;
if (domain.dns == null) {
// Default domain or subdomain of the default.
options.serverurl = 'https://' + obj.parent.certificates.CommonName + ':' + httpsport + domain.url;
} else {
// Domain with a DNS name.
options.serverurl = 'https://' + domain.dns + ':' + httpsport + domain.url;
}
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.toLowerCase(), options[i]); // Adjust this text area
text = text.split('[[[' + i.toUpperCase() + ']]]').join(options[i]); // Replace this value
}
return text;
}
// Send a generic email
obj.sendMail = function (to, subject, text, html) {
if (obj.config.sendgrid != null) {
obj.pendingMails.push({ to: to, from: obj.config.sendgrid.from, subject: subject, text: text, html: html });
} else if (obj.config.smtp != null) {
obj.pendingMails.push({ to: to, from: obj.config.smtp.from, subject: subject, text: text, html: html });
}
sendNextMail();
};
// Send account login mail / 2 factor token
obj.sendAccountLoginMail = function (domain, email, token, language, loginkey) {
obj.checkEmail(email, function (checked) {
if (checked) {
parent.debug('email', "Sending login token to " + email);
if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
return;
}
var template = getTemplate('account-login', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
// Set all the options.
var options = { email: email, servername: domain.title ? domain.title : 'MeshCentral', token: token };
if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
// Get from field
var from = null;
if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
// Send the email
obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) });
sendNextMail();
}
});
};
// Send account invitation mail
obj.sendAccountInviteMail = function (domain, username, accountname, email, password, language, loginkey) {
obj.checkEmail(email, function (checked) {
if (checked) {
parent.debug('email', "Sending account invitation to " + email);
if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
return;
}
var template = getTemplate('account-invite', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
// Set all the options.
var options = { username: username, accountname: accountname, email: email, servername: domain.title ? domain.title : 'MeshCentral', password: password };
if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
// Get from field
var from = null;
if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
// Send the email
obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) });
sendNextMail();
}
});
};
// Send account check mail
obj.sendAccountCheckMail = function (domain, username, userid, email, language, loginkey) {
obj.checkEmail(email, function (checked) {
if (checked) {
parent.debug('email', "Sending email verification to " + email);
if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
return;
}
var template = getTemplate('account-check', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
// Set all the options.
var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' };
if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
options.cookie = obj.parent.encodeCookie({ u: userid, e: email, a: 1 }, obj.mailCookieEncryptionKey);
// Get from field
var from = null;
if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
// Send the email
obj.pendingMails.push({ to: email, from: 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, userid, email, language, loginkey) {
obj.checkEmail(email, function (checked) {
if (checked) {
parent.debug('email', "Sending account password reset to " + email);
if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
return;
}
var template = getTemplate('account-reset', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
// Set all the options.
var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' };
if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
options.cookie = obj.parent.encodeCookie({ u: userid, e: email, a: 2 }, obj.mailCookieEncryptionKey);
// Get from field
var from = null;
if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
// Send the email
obj.pendingMails.push({ to: email, from: 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, flags, expirehours, language, loginkey) {
obj.checkEmail(email, function (checked) {
if (checked) {
parent.debug('email', "Sending agent install invitation to " + email);
if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
return;
}
var template = getTemplate('mesh-invite', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
// 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, installflags: flags, msg: msg, meshid: meshid, meshidhex: meshid.split('/')[2], servername: domain.title ? domain.title : 'MeshCentral', assistanttype: (domain.assistanttypeagentinvite ? domain.assistanttypeagentinvite : 0)};
if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
options.windows = ((os == 0) || (os == 1)) ? 1 : 0;
options.linux = ((os == 0) || (os == 2)) ? 1 : 0;
options.assistant = ((os == 0) || (os == 5)) ? 1 : 0;
options.osx = ((os == 0) || (os == 3)) ? 1 : 0;
options.link = (os == 4) ? 1 : 0;
options.linkurl = createInviteLink(domain, meshid, flags, expirehours);
// Get from field
var from = null;
if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
// Send the email
obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) });
sendNextMail();
}
});
};
// Send device connect/disconnect notification mail
obj.sendDeviceNotifyMail = function (domain, username, email, connections, disconnections, language, loginkey) {
obj.checkEmail(email, function (checked) {
if (checked) {
parent.debug('email', "Sending device notification to " + email);
if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
return;
}
var template = getTemplate('device-notify', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
// Set all the template replacement options and generate the final email text (both in txt and html formats).
const optionsHtml = { username: EscapeHtml(username), email: EscapeHtml(email), servername: EscapeHtml(domain.title ? domain.title : 'MeshCentral'), header: true, footer: false };
const optionsTxt = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral', header: true, footer: false };
if ((connections == null) || (connections.length == 0)) {
optionsHtml.connections = false;
optionsTxt.connections = false;
} else {
optionsHtml.connections = connections.join('<br />\r\n');
optionsTxt.connections = connections.join('\r\n');
}
if ((disconnections == null) || (disconnections.length == 0)) {
optionsHtml.disconnections = false;
optionsTxt.disconnections = false;
} else {
optionsHtml.disconnections = disconnections.join('<br />\r\n');
optionsTxt.disconnections = disconnections.join('\r\n');
}
// Get from field
var from = null;
if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
// Send the email
obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, optionsTxt), text: mailReplacements(template.txt, domain, optionsTxt), html: mailReplacements(template.html, domain, optionsHtml) });
sendNextMail();
}
});
};
// Send device help request notification mail
obj.sendDeviceHelpMail = function (domain, username, email, devicename, nodeid, helpusername, helprequest, language) {
obj.checkEmail(email, function (checked) {
if (checked) {
parent.debug('email', "Sending device help notification to " + email);
if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
return;
}
var template = getTemplate('device-help', domain, language);
if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
parent.debug('email', "Error: Failed to get mail template."); // No email template found
return;
}
// Set all the template replacement options and generate the final email text (both in txt and html formats).
const optionsHtml = { devicename: EscapeHtml(devicename), helpusername: EscapeHtml(helpusername), helprequest: EscapeHtml(helprequest), nodeid: nodeid.split('/')[2], servername: EscapeHtml(domain.title ? domain.title : 'MeshCentral') };
const optionsTxt = { devicename: devicename, helpusername: helpusername, helprequest: helprequest, nodeid: nodeid.split('/')[2], servername: domain.title ? domain.title : 'MeshCentral' };
// Get from field
var from = null;
if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
// Send the email
obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, optionsTxt), text: mailReplacements(template.txt, domain, optionsTxt), html: mailReplacements(template.html, domain, optionsHtml) });
sendNextMail();
}
});
};
// Send out the next mail in the pending list
function sendNextMail() {
if ((obj.sendingMail == true) || (obj.pendingMails.length == 0)) { return; }
var mailToSend = obj.pendingMails[0];
obj.sendingMail = true;
if (obj.sendGridServer != null) {
// SendGrid send
parent.debug('email', 'SendGrid sending mail to ' + mailToSend.to + '.');
obj.sendGridServer
.send(mailToSend)
.then(function () {
obj.sendingMail = false;
parent.debug('email', 'SendGrid sending success.');
obj.pendingMails.shift();
obj.retry = 0;
sendNextMail();
}, function (error) {
obj.sendingMail = false;
parent.debug('email', 'SendGrid sending error: ' + safeStringify(error));
obj.retry++;
// Wait and try again
if (obj.retry < 3) {
setTimeout(sendNextMail, 10000);
} else {
// Failed, send the next mail
parent.debug('email', 'SendGrid server failed (Skipping): ' + safeStringify(err));
console.log('SendGrid server failed (Skipping): ' + safeStringify(err));
obj.pendingMails.shift();
obj.retry = 0;
sendNextMail();
}
});
} else if (obj.smtpServer != null) {
parent.debug('email', 'SMTP sending mail to ' + mailToSend.to + '.');
if (obj.smtpServer == 'console') {
// Display the email on the console, this is for easy debugging
if (mailToSend.from == null) { delete mailToSend.from; }
if (mailToSend.html == null) { delete mailToSend.html; }
console.log('Email', mailToSend);
obj.sendingMail = false;
obj.pendingMails.shift();
obj.retry = 0;
sendNextMail();
} else {
// SMTP send
obj.smtpServer.sendMail(mailToSend, function (err, info) {
parent.debug('email', 'SMTP response: ' + safeStringify(err) + ', ' + JSON.stringify(info));
obj.sendingMail = false;
if (err == null) {
// Send the next mail
obj.pendingMails.shift();
obj.retry = 0;
sendNextMail();
} else {
obj.retry++;
parent.debug('email', 'SMTP server failed (Retry:' + obj.retry + '): ' + safeStringify(err));
console.log('SMTP server failed (Retry:' + obj.retry + '/3): ' + safeStringify(err));
// Wait and try again
if (obj.retry < 3) {
setTimeout(sendNextMail, 10000);
} else {
// Failed, send the next mail
parent.debug('email', 'SMTP server failed (Skipping): ' + safeStringify(err));
console.log('SMTP server failed (Skipping): ' + safeStringify(err));
obj.pendingMails.shift();
obj.retry = 0;
sendNextMail();
}
}
});
}
}
}
// Send out the next mail in the pending list
obj.verify = function () {
if ((obj.smtpServer == null) || (obj.smtpServer == 'console')) return;
obj.smtpServer.verify(function (err, info) {
if (err == null) {
if (obj.config.smtp.host == 'gmail') {
console.log('Gmail server with OAuth working as expected.');
} else {
console.log('SMTP mail server ' + obj.config.smtp.host + ' working as expected.');
}
} else {
// Remove all non-object types from error to avoid a JSON stringify error.
var err2 = {};
for (var i in err) { if (typeof (err[i]) != 'object') { err2[i] = err[i]; } }
parent.debug('email', 'SMTP mail server ' + obj.config.smtp.host + ' failed: ' + safeStringify(err2));
console.log('SMTP mail server ' + obj.config.smtp.host + ' failed: ' + safeStringify(err2));
}
});
};
// Load the cookie encryption key from the database
obj.parent.db.Get('MailCookieEncryptionKey', function (err, docs) {
if ((docs.length > 0) && (docs[0].key != null) && (obj.parent.mailtokengen == null)) {
// Key is present, use it.
obj.mailCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
} else {
// Key is not present, generate one.
obj.mailCookieEncryptionKey = obj.parent.generateCookieKey();
obj.parent.db.Set({ _id: 'MailCookieEncryptionKey', key: obj.mailCookieEncryptionKey.toString('hex'), time: Date.now() });
}
});
// Create a agent invitation link
function createInviteLink(domain, meshid, flags, expirehours) {
return '/agentinvite?c=' + parent.encodeCookie({ a: 4, mid: meshid, f: flags, expire: expirehours * 60 }, parent.invitationLinkEncryptionKey);
}
// Check the email domain DNS MX record.
obj.approvedEmailDomains = {};
obj.checkEmail = function (email, func) {
if (obj.verifyemail == false) { func(true); return; }
var emailSplit = email.split('@');
if (emailSplit.length != 2) { func(false); return; }
if (obj.approvedEmailDomains[emailSplit[1]] === true) { func(true); return; }
require('dns').resolveMx(emailSplit[1], function (err, addresses) {
parent.debug('email', "checkEmail: " + email + ", " + (err == null));
if (err == null) { obj.approvedEmailDomains[emailSplit[1]] = true; }
func(err == null);
});
}
//
// Device connection and disconnection notifications
//
obj.deviceNotifications = {}; // UserId --> { timer, nodes: nodeid --> connectType }
// A device connected and a user needs to be notified about it.
obj.notifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
const mesh = parent.webserver.meshes[meshid];
if (mesh == null) return;
// Add the user and start a timer
if (obj.deviceNotifications[user._id] == null) {
obj.deviceNotifications[user._id] = { nodes: {} };
obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, obj.emailDelay);
}
// Add the device
if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
obj.deviceNotifications[user._id].nodes[nodeid] = { c: connectType }; // This device connection need to be added
} else {
const info = obj.deviceNotifications[user._id].nodes[nodeid];
if ((info.d != null) && ((info.d & connectType) != 0)) {
info.d -= connectType; // This device disconnect cancels out a device connection
if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
// This device no longer needs a notification
delete obj.deviceNotifications[user._id].nodes[nodeid];
if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
// This user no longer needs a notification
clearTimeout(obj.deviceNotifications[user._id].timer);
delete obj.deviceNotifications[user._id];
}
return;
}
} else {
if (info.c != null) {
info.c |= connectType; // This device disconnect needs to be added
} else {
info.c = connectType; // This device disconnect needs to be added
}
}
}
// Set the device group name
if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
}
// Cancel a device disconnect notification
obj.cancelNotifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
const mesh = parent.webserver.meshes[meshid];
if (mesh == null) return;
if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
const info = obj.deviceNotifications[user._id].nodes[nodeid];
if ((info.d != null) && ((info.d & connectType) != 0)) {
info.d -= connectType; // This device disconnect cancels out a device connection
if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
// This device no longer needs a notification
delete obj.deviceNotifications[user._id].nodes[nodeid];
if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
// This user no longer needs a notification
clearTimeout(obj.deviceNotifications[user._id].timer);
delete obj.deviceNotifications[user._id];
}
}
}
}
}
// A device disconnected and a user needs to be notified about it.
obj.notifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
const mesh = parent.webserver.meshes[meshid];
if (mesh == null) return;
// Add the user and start a timer
if (obj.deviceNotifications[user._id] == null) {
obj.deviceNotifications[user._id] = { nodes: {} };
obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, obj.emailDelay);
}
// Add the device
if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
obj.deviceNotifications[user._id].nodes[nodeid] = { d: connectType }; // This device disconnect need to be added
} else {
const info = obj.deviceNotifications[user._id].nodes[nodeid];
if ((info.c != null) && ((info.c & connectType) != 0)) {
info.c -= connectType; // This device disconnect cancels out a device connection
if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
// This device no longer needs a notification
delete obj.deviceNotifications[user._id].nodes[nodeid];
if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
// This user no longer needs a notification
clearTimeout(obj.deviceNotifications[user._id].timer);
delete obj.deviceNotifications[user._id];
}
return;
}
} else {
if (info.d != null) {
info.d |= connectType; // This device disconnect needs to be added
} else {
info.d = connectType; // This device disconnect needs to be added
}
}
}
// Set the device group name
if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
}
// Cancel a device connect notification
obj.cancelNotifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
const mesh = parent.webserver.meshes[meshid];
if (mesh == null) return;
if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
const info = obj.deviceNotifications[user._id].nodes[nodeid];
if ((info.c != null) && ((info.c & connectType) != 0)) {
info.c -= connectType; // This device disconnect cancels out a device connection
if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
// This device no longer needs a notification
delete obj.deviceNotifications[user._id].nodes[nodeid];
if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
// This user no longer needs a notification
clearTimeout(obj.deviceNotifications[user._id].timer);
delete obj.deviceNotifications[user._id];
}
}
}
}
}
// Send a notification about device connections and disconnections to a user
function sendDeviceNotifications(userid) {
if (obj.deviceNotifications[userid] == null) return;
clearTimeout(obj.deviceNotifications[userid].timer);
var connections = [];
var disconnections = [];
for (var nodeid in obj.deviceNotifications[userid].nodes) {
var info = obj.deviceNotifications[userid].nodes[nodeid];
if ((info.c != null) && (info.c > 0) && (info.nn != null) && (info.mn != null)) {
var c = [];
if (info.c & 1) { c.push("Agent"); }
if (info.c & 2) { c.push("CIRA"); }
if (info.c & 4) { c.push("AMT"); }
if (info.c & 8) { c.push("AMT-Relay"); }
if (info.c & 16) { c.push("MQTT"); }
connections.push(info.mn + ', ' + info.nn + ': ' + c.join(', '));
}
if ((info.d != null) && (info.d > 0) && (info.nn != null) && (info.mn != null)) {
var d = [];
if (info.d & 1) { d.push("Agent"); }
if (info.d & 2) { d.push("CIRA"); }
if (info.d & 4) { d.push("AMT"); }
if (info.d & 8) { d.push("AMT-Relay"); }
if (info.d & 16) { d.push("MQTT"); }
disconnections.push(info.mn + ', ' + info.nn + ': ' + d.join(', '));
}
}
// Sort the notifications
connections.sort(sortCollator.compare);
disconnections.sort(sortCollator.compare);
// Get the user and domain
const user = parent.webserver.users[userid];
if ((user == null) || (user.email == null) || (user.emailVerified !== true)) return;
const domain = obj.parent.config.domains[user.domain];
if (domain == null) return;
// Send the email
obj.sendDeviceNotifyMail(domain, user.name, user.email, connections, disconnections, user.llang, null);
// Clean up
delete obj.deviceNotifications[userid];
}
return obj;
};