meshmail.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. /**
  2. * @description MeshCentral e-mail server communication modules
  3. * @author Ylian Saint-Hilaire
  4. * @copyright Intel Corporation 2018-2022
  5. * @license Apache-2.0
  6. * @version v0.0.1
  7. */
  8. /*xjslint node: true */
  9. /*xjslint plusplus: true */
  10. /*xjslint maxlen: 256 */
  11. /*jshint node: true */
  12. /*jshint strict: false */
  13. /*jshint esversion: 6 */
  14. "use strict";
  15. // TODO: Add NTML support with "nodemailer-ntlm-auth" https://github.com/nodemailer/nodemailer-ntlm-auth
  16. // Construct a MeshAgent object, called upon connection
  17. module.exports.CreateMeshMail = function (parent, domain) {
  18. var obj = {};
  19. obj.pendingMails = [];
  20. obj.parent = parent;
  21. obj.retry = 0;
  22. obj.sendingMail = false;
  23. obj.mailCookieEncryptionKey = null;
  24. obj.verifyemail = false;
  25. obj.domain = domain;
  26. obj.emailDelay = 5 * 60 * 1000; // Default of 5 minute email delay.
  27. //obj.mailTemplates = {};
  28. const sortCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' })
  29. 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.
  30. function EscapeHtml(x) { if (typeof x == 'string') return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
  31. //function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, '&nbsp;&nbsp;'); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
  32. // Setup where we read our configuration from
  33. if (obj.domain == null) { obj.config = parent.config; } else { obj.config = domain; }
  34. if (obj.config.sendgrid != null) {
  35. // Setup SendGrid mail server
  36. obj.sendGridServer = require('@sendgrid/mail');
  37. obj.sendGridServer.setApiKey(obj.config.sendgrid.apikey);
  38. if (obj.config.sendgrid.verifyemail == true) { obj.verifyemail = true; }
  39. if ((typeof obj.config.sendgrid.emaildelayseconds == 'number') && (obj.config.sendgrid.emaildelayseconds > 0)) { obj.emailDelay = obj.config.sendgrid.emaildelayseconds * 1000; }
  40. } else if (obj.config.smtp != null) {
  41. // Setup SMTP mail server
  42. if ((typeof obj.config.smtp.emaildelayseconds == 'number') && (obj.config.smtp.emaildelayseconds > 0)) { obj.emailDelay = obj.config.smtp.emaildelayseconds * 1000; }
  43. if (obj.config.smtp.name == 'console') {
  44. // This is for debugging, the mails will be displayed on the console
  45. obj.smtpServer = 'console';
  46. } else {
  47. const nodemailer = require('nodemailer');
  48. var options = { name: obj.config.smtp.name, host: obj.config.smtp.host, secure: (obj.config.smtp.tls == true), tls: {} };
  49. //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 } };
  50. if (obj.config.smtp.port != null) { options.port = obj.config.smtp.port; }
  51. if (obj.config.smtp.tlscertcheck === false) { options.tls.rejectUnauthorized = false; }
  52. 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; }
  53. if ((obj.config.smtp.auth != null) && (typeof obj.config.smtp.auth == 'object')) {
  54. var user = obj.config.smtp.from;
  55. if ((user == null) && (obj.config.smtp.user != null)) { user = obj.config.smtp.user; }
  56. if ((obj.config.smtp.auth.user != null) && (typeof obj.config.smtp.auth.user == 'string')) { user = obj.config.smtp.auth.user; }
  57. if (user.toLowerCase().endsWith('@gmail.com')) { options = { service: 'gmail', auth: { user: user } }; obj.config.smtp.host = 'gmail'; } else { options.auth = { user: user } }
  58. if (obj.config.smtp.auth.type) { options.auth.type = obj.config.smtp.auth.type; }
  59. if (obj.config.smtp.auth.clientid) { options.auth.clientId = obj.config.smtp.auth.clientid; options.auth.type = 'OAuth2'; }
  60. if (obj.config.smtp.auth.clientsecret) { options.auth.clientSecret = obj.config.smtp.auth.clientsecret; }
  61. if (obj.config.smtp.auth.refreshtoken) { options.auth.refreshToken = obj.config.smtp.auth.refreshtoken; }
  62. }
  63. else if ((obj.config.smtp.user != null) && (obj.config.smtp.pass != null)) { options.auth = { user: obj.config.smtp.user, pass: obj.config.smtp.pass }; }
  64. if (obj.config.smtp.verifyemail == true) { obj.verifyemail = true; }
  65. obj.smtpServer = nodemailer.createTransport(options);
  66. }
  67. } else if (obj.config.sendmail != null) {
  68. // Setup Sendmail
  69. if ((typeof obj.config.sendmail.emaildelayseconds == 'number') && (obj.config.sendmail.emaildelayseconds > 0)) { obj.emailDelay = obj.config.sendmail.emaildelayseconds * 1000; }
  70. const nodemailer = require('nodemailer');
  71. var options = { sendmail: true };
  72. if (typeof obj.config.sendmail.newline == 'string') { options.newline = obj.config.sendmail.newline; }
  73. if (typeof obj.config.sendmail.path == 'string') { options.path = obj.config.sendmail.path; }
  74. if (Array.isArray(obj.config.sendmail.args)) { options.args = obj.config.sendmail.args; }
  75. obj.smtpServer = nodemailer.createTransport(options);
  76. }
  77. // Get the correct mail template object
  78. function getTemplate(name, domain, lang) {
  79. parent.debug('email', 'Getting mail template for: ' + name + ', lang: ' + lang);
  80. if (Array.isArray(lang)) { lang = lang[0]; } // TODO: For now, we only use the first language given.
  81. if (lang != null) { lang = lang.split('-')[0]; } // Take the first part of the language, "xx-xx"
  82. var r = {}, emailsPath = null;
  83. if ((domain != null) && (domain.webemailspath != null)) { emailsPath = domain.webemailspath; }
  84. else if (obj.parent.webEmailsOverridePath != null) { emailsPath = obj.parent.webEmailsOverridePath; }
  85. else if (obj.parent.webEmailsPath != null) { emailsPath = obj.parent.webEmailsPath; }
  86. if ((emailsPath == null) || (obj.parent.fs.existsSync(emailsPath) == false)) { return null }
  87. // Get the non-english email if needed
  88. var htmlfile = null, txtfile = null;
  89. if ((lang != null) && (lang != 'en')) {
  90. var translationsPath = obj.parent.path.join(emailsPath, 'translations');
  91. var translationsPathHtml = obj.parent.path.join(emailsPath, 'translations', name + '_' + lang + '.html');
  92. var translationsPathTxt = obj.parent.path.join(emailsPath, 'translations', name + '_' + lang + '.txt');
  93. if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathHtml) && obj.parent.fs.existsSync(translationsPathTxt)) {
  94. htmlfile = obj.parent.fs.readFileSync(translationsPathHtml).toString();
  95. txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString();
  96. }
  97. }
  98. // Get the english email
  99. if ((htmlfile == null) || (txtfile == null)) {
  100. var pathHtml = obj.parent.path.join(emailsPath, name + '.html');
  101. var pathTxt = obj.parent.path.join(emailsPath, name + '.txt');
  102. if (obj.parent.fs.existsSync(pathHtml) && obj.parent.fs.existsSync(pathTxt)) {
  103. htmlfile = obj.parent.fs.readFileSync(pathHtml).toString();
  104. txtfile = obj.parent.fs.readFileSync(pathTxt).toString();
  105. }
  106. }
  107. // If no email template found, use the default translated email template
  108. if (((htmlfile == null) || (txtfile == null)) && (lang != null) && (lang != 'en')) {
  109. var translationsPath = obj.parent.path.join(obj.parent.webEmailsPath, 'translations');
  110. var translationsPathHtml = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', name + '_' + lang + '.html');
  111. var translationsPathTxt = obj.parent.path.join(obj.parent.webEmailsPath, 'translations', name + '_' + lang + '.txt');
  112. if (obj.parent.fs.existsSync(translationsPath) && obj.parent.fs.existsSync(translationsPathHtml) && obj.parent.fs.existsSync(translationsPathTxt)) {
  113. htmlfile = obj.parent.fs.readFileSync(translationsPathHtml).toString();
  114. txtfile = obj.parent.fs.readFileSync(translationsPathTxt).toString();
  115. }
  116. }
  117. // If no translated email template found, use the default email template
  118. if ((htmlfile == null) || (txtfile == null)) {
  119. var pathHtml = obj.parent.path.join(obj.parent.webEmailsPath, name + '.html');
  120. var pathTxt = obj.parent.path.join(obj.parent.webEmailsPath, name + '.txt');
  121. if (obj.parent.fs.existsSync(pathHtml) && obj.parent.fs.existsSync(pathTxt)) {
  122. htmlfile = obj.parent.fs.readFileSync(pathHtml).toString();
  123. txtfile = obj.parent.fs.readFileSync(pathTxt).toString();
  124. }
  125. }
  126. // No email templates
  127. if ((htmlfile == null) || (txtfile == null)) { return null; }
  128. // Decode the HTML file
  129. htmlfile = htmlfile.split('<html>').join('').split('</html>').join('').split('<head>').join('').split('</head>').join('').split('<body>').join('').split('</body>').join('').split(' notrans="1"').join('');
  130. var lines = htmlfile.split('\r\n').join('\n').split('\n');
  131. r.htmlSubject = lines.shift();
  132. if (r.htmlSubject.startsWith('<div>')) { r.htmlSubject = r.htmlSubject.substring(5); }
  133. if (r.htmlSubject.endsWith('</div>')) { r.htmlSubject = r.htmlSubject.substring(0, r.htmlSubject.length - 6); }
  134. r.html = lines.join('\r\n');
  135. // Decode the TXT file
  136. lines = txtfile.split('\r\n').join('\n').split('\n');
  137. r.txtSubject = lines.shift();
  138. var txtbody = [];
  139. for (var i in lines) { var line = lines[i]; if ((line.length > 0) && (line[0] == '~')) { txtbody.push(line.substring(1)); } else { txtbody.push(line); } }
  140. r.txt = txtbody.join('\r\n');
  141. return r;
  142. }
  143. // Get the string between two markers
  144. function getStrBetween(str, start, end) {
  145. var si = str.indexOf(start), ei = str.indexOf(end);
  146. if ((si == -1) || (ei == -1) || (si > ei)) return null;
  147. return str.substring(si + start.length, ei);
  148. }
  149. // Remove the string between two markers
  150. function removeStrBetween(str, start, end) {
  151. var si = str.indexOf(start), ei = str.indexOf(end);
  152. if ((si == -1) || (ei == -1) || (si > ei)) return str;
  153. return str.substring(0, si) + str.substring(ei + end.length);
  154. }
  155. // Keep or remove all lines between two lines with markers
  156. function strZone(str, marker, keep) {
  157. var lines = str.split('\r\n'), linesEx = [], removing = false;
  158. const startMarker = '<area-' + marker + '>', endMarker = '</area-' + marker + '>';
  159. for (var i in lines) {
  160. var line = lines[i];
  161. if (removing) {
  162. if (line.indexOf(endMarker) >= 0) { removing = false; } else { if (keep) { linesEx.push(line); } }
  163. } else {
  164. if (line.indexOf(startMarker) >= 0) { removing = true; } else { linesEx.push(line); }
  165. }
  166. }
  167. return linesEx.join('\r\n');
  168. }
  169. // Perform all e-mail substitution
  170. function mailReplacements(text, domain, options) {
  171. var httpsport = (typeof obj.parent.args.aliasport == 'number') ? obj.parent.args.aliasport : obj.parent.args.port;
  172. if (domain.dns == null) {
  173. // Default domain or subdomain of the default.
  174. options.serverurl = 'https://' + obj.parent.certificates.CommonName + ':' + httpsport + domain.url;
  175. } else {
  176. // Domain with a DNS name.
  177. options.serverurl = 'https://' + domain.dns + ':' + httpsport + domain.url;
  178. }
  179. if (options.serverurl.endsWith('/')) { options.serverurl = options.serverurl.substring(0, options.serverurl.length - 1); } // Remove the ending / if present
  180. for (var i in options) {
  181. text = strZone(text, i.toLowerCase(), options[i]); // Adjust this text area
  182. text = text.split('[[[' + i.toUpperCase() + ']]]').join(options[i]); // Replace this value
  183. }
  184. return text;
  185. }
  186. // Send a generic email
  187. obj.sendMail = function (to, subject, text, html) {
  188. if (obj.config.sendgrid != null) {
  189. obj.pendingMails.push({ to: to, from: obj.config.sendgrid.from, subject: subject, text: text, html: html });
  190. } else if (obj.config.smtp != null) {
  191. obj.pendingMails.push({ to: to, from: obj.config.smtp.from, subject: subject, text: text, html: html });
  192. }
  193. sendNextMail();
  194. };
  195. // Send account login mail / 2 factor token
  196. obj.sendAccountLoginMail = function (domain, email, token, language, loginkey) {
  197. obj.checkEmail(email, function (checked) {
  198. if (checked) {
  199. parent.debug('email', "Sending login token to " + email);
  200. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  201. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  202. return;
  203. }
  204. var template = getTemplate('account-login', domain, language);
  205. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  206. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  207. return;
  208. }
  209. // Set all the options.
  210. var options = { email: email, servername: domain.title ? domain.title : 'MeshCentral', token: token };
  211. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  212. // Get from field
  213. var from = null;
  214. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  215. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  216. // Send the email
  217. 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) });
  218. sendNextMail();
  219. }
  220. });
  221. };
  222. // Send account invitation mail
  223. obj.sendAccountInviteMail = function (domain, username, accountname, email, password, language, loginkey) {
  224. obj.checkEmail(email, function (checked) {
  225. if (checked) {
  226. parent.debug('email', "Sending account invitation to " + email);
  227. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  228. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  229. return;
  230. }
  231. var template = getTemplate('account-invite', domain, language);
  232. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  233. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  234. return;
  235. }
  236. // Set all the options.
  237. var options = { username: username, accountname: accountname, email: email, servername: domain.title ? domain.title : 'MeshCentral', password: password };
  238. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  239. // Get from field
  240. var from = null;
  241. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  242. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  243. // Send the email
  244. 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) });
  245. sendNextMail();
  246. }
  247. });
  248. };
  249. // Send account check mail
  250. obj.sendAccountCheckMail = function (domain, username, userid, email, language, loginkey) {
  251. obj.checkEmail(email, function (checked) {
  252. if (checked) {
  253. parent.debug('email', "Sending email verification to " + email);
  254. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  255. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  256. return;
  257. }
  258. var template = getTemplate('account-check', domain, language);
  259. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  260. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  261. return;
  262. }
  263. // Set all the options.
  264. var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' };
  265. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  266. options.cookie = obj.parent.encodeCookie({ u: userid, e: email, a: 1 }, obj.mailCookieEncryptionKey);
  267. // Get from field
  268. var from = null;
  269. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  270. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  271. // Send the email
  272. 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) });
  273. sendNextMail();
  274. }
  275. });
  276. };
  277. // Send account reset mail
  278. obj.sendAccountResetMail = function (domain, username, userid, email, language, loginkey) {
  279. obj.checkEmail(email, function (checked) {
  280. if (checked) {
  281. parent.debug('email', "Sending account password reset to " + email);
  282. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  283. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  284. return;
  285. }
  286. var template = getTemplate('account-reset', domain, language);
  287. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  288. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  289. return;
  290. }
  291. // Set all the options.
  292. var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' };
  293. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  294. options.cookie = obj.parent.encodeCookie({ u: userid, e: email, a: 2 }, obj.mailCookieEncryptionKey);
  295. // Get from field
  296. var from = null;
  297. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  298. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  299. // Send the email
  300. 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) });
  301. sendNextMail();
  302. }
  303. });
  304. };
  305. // Send agent invite mail
  306. obj.sendAgentInviteMail = function (domain, username, email, meshid, name, os, msg, flags, expirehours, language, loginkey) {
  307. obj.checkEmail(email, function (checked) {
  308. if (checked) {
  309. parent.debug('email', "Sending agent install invitation to " + email);
  310. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  311. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  312. return;
  313. }
  314. var template = getTemplate('mesh-invite', domain, language);
  315. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  316. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  317. return;
  318. }
  319. // Set all the template replacement options and generate the final email text (both in txt and html formats).
  320. 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)};
  321. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  322. options.windows = ((os == 0) || (os == 1)) ? 1 : 0;
  323. options.linux = ((os == 0) || (os == 2)) ? 1 : 0;
  324. options.assistant = ((os == 0) || (os == 5)) ? 1 : 0;
  325. options.osx = ((os == 0) || (os == 3)) ? 1 : 0;
  326. options.link = (os == 4) ? 1 : 0;
  327. options.linkurl = createInviteLink(domain, meshid, flags, expirehours);
  328. // Get from field
  329. var from = null;
  330. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  331. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  332. // Send the email
  333. 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) });
  334. sendNextMail();
  335. }
  336. });
  337. };
  338. // Send device connect/disconnect notification mail
  339. obj.sendDeviceNotifyMail = function (domain, username, email, connections, disconnections, language, loginkey) {
  340. obj.checkEmail(email, function (checked) {
  341. if (checked) {
  342. parent.debug('email', "Sending device notification to " + email);
  343. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  344. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  345. return;
  346. }
  347. var template = getTemplate('device-notify', domain, language);
  348. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  349. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  350. return;
  351. }
  352. // Set all the template replacement options and generate the final email text (both in txt and html formats).
  353. const optionsHtml = { username: EscapeHtml(username), email: EscapeHtml(email), servername: EscapeHtml(domain.title ? domain.title : 'MeshCentral'), header: true, footer: false };
  354. const optionsTxt = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral', header: true, footer: false };
  355. if ((connections == null) || (connections.length == 0)) {
  356. optionsHtml.connections = false;
  357. optionsTxt.connections = false;
  358. } else {
  359. optionsHtml.connections = connections.join('<br />\r\n');
  360. optionsTxt.connections = connections.join('\r\n');
  361. }
  362. if ((disconnections == null) || (disconnections.length == 0)) {
  363. optionsHtml.disconnections = false;
  364. optionsTxt.disconnections = false;
  365. } else {
  366. optionsHtml.disconnections = disconnections.join('<br />\r\n');
  367. optionsTxt.disconnections = disconnections.join('\r\n');
  368. }
  369. // Get from field
  370. var from = null;
  371. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  372. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  373. // Send the email
  374. 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) });
  375. sendNextMail();
  376. }
  377. });
  378. };
  379. // Send device help request notification mail
  380. obj.sendDeviceHelpMail = function (domain, username, email, devicename, nodeid, helpusername, helprequest, language) {
  381. obj.checkEmail(email, function (checked) {
  382. if (checked) {
  383. parent.debug('email', "Sending device help notification to " + email);
  384. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  385. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  386. return;
  387. }
  388. var template = getTemplate('device-help', domain, language);
  389. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  390. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  391. return;
  392. }
  393. // Set all the template replacement options and generate the final email text (both in txt and html formats).
  394. const optionsHtml = { devicename: EscapeHtml(devicename), helpusername: EscapeHtml(helpusername), helprequest: EscapeHtml(helprequest), nodeid: nodeid.split('/')[2], servername: EscapeHtml(domain.title ? domain.title : 'MeshCentral') };
  395. const optionsTxt = { devicename: devicename, helpusername: helpusername, helprequest: helprequest, nodeid: nodeid.split('/')[2], servername: domain.title ? domain.title : 'MeshCentral' };
  396. // Get from field
  397. var from = null;
  398. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  399. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  400. // Send the email
  401. 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) });
  402. sendNextMail();
  403. }
  404. });
  405. };
  406. // Send out the next mail in the pending list
  407. function sendNextMail() {
  408. if ((obj.sendingMail == true) || (obj.pendingMails.length == 0)) { return; }
  409. var mailToSend = obj.pendingMails[0];
  410. obj.sendingMail = true;
  411. if (obj.sendGridServer != null) {
  412. // SendGrid send
  413. parent.debug('email', 'SendGrid sending mail to ' + mailToSend.to + '.');
  414. obj.sendGridServer
  415. .send(mailToSend)
  416. .then(function () {
  417. obj.sendingMail = false;
  418. parent.debug('email', 'SendGrid sending success.');
  419. obj.pendingMails.shift();
  420. obj.retry = 0;
  421. sendNextMail();
  422. }, function (error) {
  423. obj.sendingMail = false;
  424. parent.debug('email', 'SendGrid sending error: ' + JSON.stringify(error));
  425. obj.retry++;
  426. // Wait and try again
  427. if (obj.retry < 3) {
  428. setTimeout(sendNextMail, 10000);
  429. } else {
  430. // Failed, send the next mail
  431. parent.debug('email', 'SendGrid server failed (Skipping): ' + JSON.stringify(err));
  432. console.log('SendGrid server failed (Skipping): ' + JSON.stringify(err));
  433. obj.pendingMails.shift();
  434. obj.retry = 0;
  435. sendNextMail();
  436. }
  437. });
  438. } else if (obj.smtpServer != null) {
  439. parent.debug('email', 'SMTP sending mail to ' + mailToSend.to + '.');
  440. if (obj.smtpServer == 'console') {
  441. // Display the email on the console, this is for easy debugging
  442. if (mailToSend.from == null) { delete mailToSend.from; }
  443. if (mailToSend.html == null) { delete mailToSend.html; }
  444. console.log('Email', mailToSend);
  445. obj.sendingMail = false;
  446. obj.pendingMails.shift();
  447. obj.retry = 0;
  448. sendNextMail();
  449. } else {
  450. // SMTP send
  451. obj.smtpServer.sendMail(mailToSend, function (err, info) {
  452. parent.debug('email', 'SMTP response: ' + JSON.stringify(err) + ', ' + JSON.stringify(info));
  453. obj.sendingMail = false;
  454. if (err == null) {
  455. // Send the next mail
  456. obj.pendingMails.shift();
  457. obj.retry = 0;
  458. sendNextMail();
  459. } else {
  460. obj.retry++;
  461. parent.debug('email', 'SMTP server failed (Retry:' + obj.retry + '): ' + JSON.stringify(err));
  462. console.log('SMTP server failed (Retry:' + obj.retry + '/3): ' + JSON.stringify(err));
  463. // Wait and try again
  464. if (obj.retry < 3) {
  465. setTimeout(sendNextMail, 10000);
  466. } else {
  467. // Failed, send the next mail
  468. parent.debug('email', 'SMTP server failed (Skipping): ' + JSON.stringify(err));
  469. console.log('SMTP server failed (Skipping): ' + JSON.stringify(err));
  470. obj.pendingMails.shift();
  471. obj.retry = 0;
  472. sendNextMail();
  473. }
  474. }
  475. });
  476. }
  477. }
  478. }
  479. // Send out the next mail in the pending list
  480. obj.verify = function () {
  481. if ((obj.smtpServer == null) || (obj.smtpServer == 'console')) return;
  482. obj.smtpServer.verify(function (err, info) {
  483. if (err == null) {
  484. if (obj.config.smtp.host == 'gmail') {
  485. console.log('Gmail server with OAuth working as expected.');
  486. } else {
  487. console.log('SMTP mail server ' + obj.config.smtp.host + ' working as expected.');
  488. }
  489. } else {
  490. // Remove all non-object types from error to avoid a JSON stringify error.
  491. var err2 = {};
  492. for (var i in err) { if (typeof (err[i]) != 'object') { err2[i] = err[i]; } }
  493. parent.debug('email', 'SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2));
  494. console.log('SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2));
  495. }
  496. });
  497. };
  498. // Load the cookie encryption key from the database
  499. obj.parent.db.Get('MailCookieEncryptionKey', function (err, docs) {
  500. if ((docs.length > 0) && (docs[0].key != null) && (obj.parent.mailtokengen == null)) {
  501. // Key is present, use it.
  502. obj.mailCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
  503. } else {
  504. // Key is not present, generate one.
  505. obj.mailCookieEncryptionKey = obj.parent.generateCookieKey();
  506. obj.parent.db.Set({ _id: 'MailCookieEncryptionKey', key: obj.mailCookieEncryptionKey.toString('hex'), time: Date.now() });
  507. }
  508. });
  509. // Create a agent invitation link
  510. function createInviteLink(domain, meshid, flags, expirehours) {
  511. return '/agentinvite?c=' + parent.encodeCookie({ a: 4, mid: meshid, f: flags, expire: expirehours * 60 }, parent.invitationLinkEncryptionKey);
  512. }
  513. // Check the email domain DNS MX record.
  514. obj.approvedEmailDomains = {};
  515. obj.checkEmail = function (email, func) {
  516. if (obj.verifyemail == false) { func(true); return; }
  517. var emailSplit = email.split('@');
  518. if (emailSplit.length != 2) { func(false); return; }
  519. if (obj.approvedEmailDomains[emailSplit[1]] === true) { func(true); return; }
  520. require('dns').resolveMx(emailSplit[1], function (err, addresses) {
  521. parent.debug('email', "checkEmail: " + email + ", " + (err == null));
  522. if (err == null) { obj.approvedEmailDomains[emailSplit[1]] = true; }
  523. func(err == null);
  524. });
  525. }
  526. //
  527. // Device connection and disconnection notifications
  528. //
  529. obj.deviceNotifications = {}; // UserId --> { timer, nodes: nodeid --> connectType }
  530. // A device connected and a user needs to be notified about it.
  531. obj.notifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
  532. const mesh = parent.webserver.meshes[meshid];
  533. if (mesh == null) return;
  534. // Add the user and start a timer
  535. if (obj.deviceNotifications[user._id] == null) {
  536. obj.deviceNotifications[user._id] = { nodes: {} };
  537. obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, obj.emailDelay);
  538. }
  539. // Add the device
  540. if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
  541. obj.deviceNotifications[user._id].nodes[nodeid] = { c: connectType }; // This device connection need to be added
  542. } else {
  543. const info = obj.deviceNotifications[user._id].nodes[nodeid];
  544. if ((info.d != null) && ((info.d & connectType) != 0)) {
  545. info.d -= connectType; // This device disconnect cancels out a device connection
  546. if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
  547. // This device no longer needs a notification
  548. delete obj.deviceNotifications[user._id].nodes[nodeid];
  549. if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
  550. // This user no longer needs a notification
  551. clearTimeout(obj.deviceNotifications[user._id].timer);
  552. delete obj.deviceNotifications[user._id];
  553. }
  554. return;
  555. }
  556. } else {
  557. if (info.c != null) {
  558. info.c |= connectType; // This device disconnect needs to be added
  559. } else {
  560. info.c = connectType; // This device disconnect needs to be added
  561. }
  562. }
  563. }
  564. // Set the device group name
  565. if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
  566. obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
  567. }
  568. // Cancel a device disconnect notification
  569. obj.cancelNotifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
  570. const mesh = parent.webserver.meshes[meshid];
  571. if (mesh == null) return;
  572. if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
  573. const info = obj.deviceNotifications[user._id].nodes[nodeid];
  574. if ((info.d != null) && ((info.d & connectType) != 0)) {
  575. info.d -= connectType; // This device disconnect cancels out a device connection
  576. if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
  577. // This device no longer needs a notification
  578. delete obj.deviceNotifications[user._id].nodes[nodeid];
  579. if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
  580. // This user no longer needs a notification
  581. clearTimeout(obj.deviceNotifications[user._id].timer);
  582. delete obj.deviceNotifications[user._id];
  583. }
  584. }
  585. }
  586. }
  587. }
  588. // A device disconnected and a user needs to be notified about it.
  589. obj.notifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
  590. const mesh = parent.webserver.meshes[meshid];
  591. if (mesh == null) return;
  592. // Add the user and start a timer
  593. if (obj.deviceNotifications[user._id] == null) {
  594. obj.deviceNotifications[user._id] = { nodes: {} };
  595. obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, obj.emailDelay);
  596. }
  597. // Add the device
  598. if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
  599. obj.deviceNotifications[user._id].nodes[nodeid] = { d: connectType }; // This device disconnect need to be added
  600. } else {
  601. const info = obj.deviceNotifications[user._id].nodes[nodeid];
  602. if ((info.c != null) && ((info.c & connectType) != 0)) {
  603. info.c -= connectType; // This device disconnect cancels out a device connection
  604. if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
  605. // This device no longer needs a notification
  606. delete obj.deviceNotifications[user._id].nodes[nodeid];
  607. if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
  608. // This user no longer needs a notification
  609. clearTimeout(obj.deviceNotifications[user._id].timer);
  610. delete obj.deviceNotifications[user._id];
  611. }
  612. return;
  613. }
  614. } else {
  615. if (info.d != null) {
  616. info.d |= connectType; // This device disconnect needs to be added
  617. } else {
  618. info.d = connectType; // This device disconnect needs to be added
  619. }
  620. }
  621. }
  622. // Set the device group name
  623. if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
  624. obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
  625. }
  626. // Cancel a device connect notification
  627. obj.cancelNotifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
  628. const mesh = parent.webserver.meshes[meshid];
  629. if (mesh == null) return;
  630. if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
  631. const info = obj.deviceNotifications[user._id].nodes[nodeid];
  632. if ((info.c != null) && ((info.c & connectType) != 0)) {
  633. info.c -= connectType; // This device disconnect cancels out a device connection
  634. if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
  635. // This device no longer needs a notification
  636. delete obj.deviceNotifications[user._id].nodes[nodeid];
  637. if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
  638. // This user no longer needs a notification
  639. clearTimeout(obj.deviceNotifications[user._id].timer);
  640. delete obj.deviceNotifications[user._id];
  641. }
  642. }
  643. }
  644. }
  645. }
  646. // Send a notification about device connections and disconnections to a user
  647. function sendDeviceNotifications(userid) {
  648. if (obj.deviceNotifications[userid] == null) return;
  649. clearTimeout(obj.deviceNotifications[userid].timer);
  650. var connections = [];
  651. var disconnections = [];
  652. for (var nodeid in obj.deviceNotifications[userid].nodes) {
  653. var info = obj.deviceNotifications[userid].nodes[nodeid];
  654. if ((info.c != null) && (info.c > 0) && (info.nn != null) && (info.mn != null)) {
  655. var c = [];
  656. if (info.c & 1) { c.push("Agent"); }
  657. if (info.c & 2) { c.push("CIRA"); }
  658. if (info.c & 4) { c.push("AMT"); }
  659. if (info.c & 8) { c.push("AMT-Relay"); }
  660. if (info.c & 16) { c.push("MQTT"); }
  661. connections.push(info.mn + ', ' + info.nn + ': ' + c.join(', '));
  662. }
  663. if ((info.d != null) && (info.d > 0) && (info.nn != null) && (info.mn != null)) {
  664. var d = [];
  665. if (info.d & 1) { d.push("Agent"); }
  666. if (info.d & 2) { d.push("CIRA"); }
  667. if (info.d & 4) { d.push("AMT"); }
  668. if (info.d & 8) { d.push("AMT-Relay"); }
  669. if (info.d & 16) { d.push("MQTT"); }
  670. disconnections.push(info.mn + ', ' + info.nn + ': ' + d.join(', '));
  671. }
  672. }
  673. // Sort the notifications
  674. connections.sort(sortCollator.compare);
  675. disconnections.sort(sortCollator.compare);
  676. // Get the user and domain
  677. const user = parent.webserver.users[userid];
  678. if ((user == null) || (user.email == null) || (user.emailVerified !== true)) return;
  679. const domain = obj.parent.config.domains[user.domain];
  680. if (domain == null) return;
  681. // Send the email
  682. obj.sendDeviceNotifyMail(domain, user.name, user.email, connections, disconnections, user.llang, null);
  683. // Clean up
  684. delete obj.deviceNotifications[userid];
  685. }
  686. return obj;
  687. };