meshmail.js 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759
  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. // No email templates
  108. if ((htmlfile == null) || (txtfile == null)) { return null; }
  109. // Decode the HTML file
  110. htmlfile = htmlfile.split('<html>').join('').split('</html>').join('').split('<head>').join('').split('</head>').join('').split('<body>').join('').split('</body>').join('').split(' notrans="1"').join('');
  111. var lines = htmlfile.split('\r\n').join('\n').split('\n');
  112. r.htmlSubject = lines.shift();
  113. if (r.htmlSubject.startsWith('<div>')) { r.htmlSubject = r.htmlSubject.substring(5); }
  114. if (r.htmlSubject.endsWith('</div>')) { r.htmlSubject = r.htmlSubject.substring(0, r.htmlSubject.length - 6); }
  115. r.html = lines.join('\r\n');
  116. // Decode the TXT file
  117. lines = txtfile.split('\r\n').join('\n').split('\n');
  118. r.txtSubject = lines.shift();
  119. var txtbody = [];
  120. for (var i in lines) { var line = lines[i]; if ((line.length > 0) && (line[0] == '~')) { txtbody.push(line.substring(1)); } else { txtbody.push(line); } }
  121. r.txt = txtbody.join('\r\n');
  122. return r;
  123. }
  124. // Get the string between two markers
  125. function getStrBetween(str, start, end) {
  126. var si = str.indexOf(start), ei = str.indexOf(end);
  127. if ((si == -1) || (ei == -1) || (si > ei)) return null;
  128. return str.substring(si + start.length, ei);
  129. }
  130. // Remove the string between two markers
  131. function removeStrBetween(str, start, end) {
  132. var si = str.indexOf(start), ei = str.indexOf(end);
  133. if ((si == -1) || (ei == -1) || (si > ei)) return str;
  134. return str.substring(0, si) + str.substring(ei + end.length);
  135. }
  136. // Keep or remove all lines between two lines with markers
  137. function strZone(str, marker, keep) {
  138. var lines = str.split('\r\n'), linesEx = [], removing = false;
  139. const startMarker = '<area-' + marker + '>', endMarker = '</area-' + marker + '>';
  140. for (var i in lines) {
  141. var line = lines[i];
  142. if (removing) {
  143. if (line.indexOf(endMarker) >= 0) { removing = false; } else { if (keep) { linesEx.push(line); } }
  144. } else {
  145. if (line.indexOf(startMarker) >= 0) { removing = true; } else { linesEx.push(line); }
  146. }
  147. }
  148. return linesEx.join('\r\n');
  149. }
  150. // Perform all e-mail substitution
  151. function mailReplacements(text, domain, options) {
  152. var httpsport = (typeof obj.parent.args.aliasport == 'number') ? obj.parent.args.aliasport : obj.parent.args.port;
  153. if (domain.dns == null) {
  154. // Default domain or subdomain of the default.
  155. options.serverurl = 'https://' + obj.parent.certificates.CommonName + ':' + httpsport + domain.url;
  156. } else {
  157. // Domain with a DNS name.
  158. options.serverurl = 'https://' + domain.dns + ':' + httpsport + domain.url;
  159. }
  160. if (options.serverurl.endsWith('/')) { options.serverurl = options.serverurl.substring(0, options.serverurl.length - 1); } // Remove the ending / if present
  161. for (var i in options) {
  162. text = strZone(text, i.toLowerCase(), options[i]); // Adjust this text area
  163. text = text.split('[[[' + i.toUpperCase() + ']]]').join(options[i]); // Replace this value
  164. }
  165. return text;
  166. }
  167. // Send a generic email
  168. obj.sendMail = function (to, subject, text, html) {
  169. if (obj.config.sendgrid != null) {
  170. obj.pendingMails.push({ to: to, from: obj.config.sendgrid.from, subject: subject, text: text, html: html });
  171. } else if (obj.config.smtp != null) {
  172. obj.pendingMails.push({ to: to, from: obj.config.smtp.from, subject: subject, text: text, html: html });
  173. }
  174. sendNextMail();
  175. };
  176. // Send account login mail / 2 factor token
  177. obj.sendAccountLoginMail = function (domain, email, token, language, loginkey) {
  178. obj.checkEmail(email, function (checked) {
  179. if (checked) {
  180. parent.debug('email', "Sending login token to " + email);
  181. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  182. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  183. return;
  184. }
  185. var template = getTemplate('account-login', domain, language);
  186. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  187. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  188. return;
  189. }
  190. // Set all the options.
  191. var options = { email: email, servername: domain.title ? domain.title : 'MeshCentral', token: token };
  192. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  193. // Get from field
  194. var from = null;
  195. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  196. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  197. // Send the email
  198. 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) });
  199. sendNextMail();
  200. }
  201. });
  202. };
  203. // Send account invitation mail
  204. obj.sendAccountInviteMail = function (domain, username, accountname, email, password, language, loginkey) {
  205. obj.checkEmail(email, function (checked) {
  206. if (checked) {
  207. parent.debug('email', "Sending account invitation to " + email);
  208. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  209. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  210. return;
  211. }
  212. var template = getTemplate('account-invite', domain, language);
  213. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  214. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  215. return;
  216. }
  217. // Set all the options.
  218. var options = { username: username, accountname: accountname, email: email, servername: domain.title ? domain.title : 'MeshCentral', password: password };
  219. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  220. // Get from field
  221. var from = null;
  222. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  223. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  224. // Send the email
  225. 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) });
  226. sendNextMail();
  227. }
  228. });
  229. };
  230. // Send account check mail
  231. obj.sendAccountCheckMail = function (domain, username, userid, email, language, loginkey) {
  232. obj.checkEmail(email, function (checked) {
  233. if (checked) {
  234. parent.debug('email', "Sending email verification to " + email);
  235. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  236. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  237. return;
  238. }
  239. var template = getTemplate('account-check', domain, language);
  240. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  241. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  242. return;
  243. }
  244. // Set all the options.
  245. var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' };
  246. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  247. options.cookie = obj.parent.encodeCookie({ u: userid, e: email, a: 1 }, obj.mailCookieEncryptionKey);
  248. // Get from field
  249. var from = null;
  250. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  251. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  252. // Send the email
  253. 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) });
  254. sendNextMail();
  255. }
  256. });
  257. };
  258. // Send account reset mail
  259. obj.sendAccountResetMail = function (domain, username, userid, email, language, loginkey) {
  260. obj.checkEmail(email, function (checked) {
  261. if (checked) {
  262. parent.debug('email', "Sending account password reset to " + email);
  263. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  264. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  265. return;
  266. }
  267. var template = getTemplate('account-reset', domain, language);
  268. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  269. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  270. return;
  271. }
  272. // Set all the options.
  273. var options = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral' };
  274. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  275. options.cookie = obj.parent.encodeCookie({ u: userid, e: email, a: 2 }, obj.mailCookieEncryptionKey);
  276. // Get from field
  277. var from = null;
  278. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  279. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  280. // Send the email
  281. 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) });
  282. sendNextMail();
  283. }
  284. });
  285. };
  286. // Send agent invite mail
  287. obj.sendAgentInviteMail = function (domain, username, email, meshid, name, os, msg, flags, expirehours, language, loginkey) {
  288. obj.checkEmail(email, function (checked) {
  289. if (checked) {
  290. parent.debug('email', "Sending agent install invitation to " + email);
  291. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  292. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  293. return;
  294. }
  295. var template = getTemplate('mesh-invite', domain, language);
  296. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  297. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  298. return;
  299. }
  300. // Set all the template replacement options and generate the final email text (both in txt and html formats).
  301. var options = { username: username, name: name, email: email, installflags: flags, msg: msg, meshid: meshid, meshidhex: meshid.split('/')[2], servername: domain.title ? domain.title : 'MeshCentral' };
  302. if (loginkey != null) { options.urlargs1 = '?key=' + loginkey; options.urlargs2 = '&key=' + loginkey; } else { options.urlargs1 = ''; options.urlargs2 = ''; }
  303. options.windows = ((os == 0) || (os == 1)) ? 1 : 0;
  304. options.linux = ((os == 0) || (os == 2)) ? 1 : 0;
  305. options.assistant = ((os == 0) || (os == 5)) ? 1 : 0;
  306. options.osx = ((os == 0) || (os == 3)) ? 1 : 0;
  307. options.link = (os == 4) ? 1 : 0;
  308. options.linkurl = createInviteLink(domain, meshid, flags, expirehours);
  309. // Get from field
  310. var from = null;
  311. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  312. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  313. // Send the email
  314. 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) });
  315. sendNextMail();
  316. }
  317. });
  318. };
  319. // Send device connect/disconnect notification mail
  320. obj.sendDeviceNotifyMail = function (domain, username, email, connections, disconnections, language, loginkey) {
  321. obj.checkEmail(email, function (checked) {
  322. if (checked) {
  323. parent.debug('email', "Sending device notification to " + email);
  324. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  325. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  326. return;
  327. }
  328. var template = getTemplate('device-notify', domain, language);
  329. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  330. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  331. return;
  332. }
  333. // Set all the template replacement options and generate the final email text (both in txt and html formats).
  334. const optionsHtml = { username: EscapeHtml(username), email: EscapeHtml(email), servername: EscapeHtml(domain.title ? domain.title : 'MeshCentral'), header: true, footer: false };
  335. const optionsTxt = { username: username, email: email, servername: domain.title ? domain.title : 'MeshCentral', header: true, footer: false };
  336. if ((connections == null) || (connections.length == 0)) {
  337. optionsHtml.connections = false;
  338. optionsTxt.connections = false;
  339. } else {
  340. optionsHtml.connections = connections.join('<br />\r\n');
  341. optionsTxt.connections = connections.join('\r\n');
  342. }
  343. if ((disconnections == null) || (disconnections.length == 0)) {
  344. optionsHtml.disconnections = false;
  345. optionsTxt.disconnections = false;
  346. } else {
  347. optionsHtml.disconnections = disconnections.join('<br />\r\n');
  348. optionsTxt.disconnections = disconnections.join('\r\n');
  349. }
  350. // Get from field
  351. var from = null;
  352. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  353. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  354. // Send the email
  355. 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) });
  356. sendNextMail();
  357. }
  358. });
  359. };
  360. // Send device help request notification mail
  361. obj.sendDeviceHelpMail = function (domain, username, email, devicename, nodeid, helpusername, helprequest, language) {
  362. obj.checkEmail(email, function (checked) {
  363. if (checked) {
  364. parent.debug('email', "Sending device help notification to " + email);
  365. if ((parent.certificates == null) || (parent.certificates.CommonName == null) || (parent.certificates.CommonName.indexOf('.') == -1)) {
  366. parent.debug('email', "Error: Server name not set."); // If the server name is not set, email not possible.
  367. return;
  368. }
  369. var template = getTemplate('device-help', domain, language);
  370. if ((template == null) || (template.htmlSubject == null) || (template.txtSubject == null)) {
  371. parent.debug('email', "Error: Failed to get mail template."); // No email template found
  372. return;
  373. }
  374. // Set all the template replacement options and generate the final email text (both in txt and html formats).
  375. const optionsHtml = { devicename: EscapeHtml(devicename), helpusername: EscapeHtml(helpusername), helprequest: EscapeHtml(helprequest), nodeid: nodeid.split('/')[2], servername: EscapeHtml(domain.title ? domain.title : 'MeshCentral') };
  376. const optionsTxt = { devicename: devicename, helpusername: helpusername, helprequest: helprequest, nodeid: nodeid.split('/')[2], servername: domain.title ? domain.title : 'MeshCentral' };
  377. // Get from field
  378. var from = null;
  379. if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; }
  380. else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; }
  381. // Send the email
  382. 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) });
  383. sendNextMail();
  384. }
  385. });
  386. };
  387. // Send out the next mail in the pending list
  388. function sendNextMail() {
  389. if ((obj.sendingMail == true) || (obj.pendingMails.length == 0)) { return; }
  390. var mailToSend = obj.pendingMails[0];
  391. obj.sendingMail = true;
  392. if (obj.sendGridServer != null) {
  393. // SendGrid send
  394. parent.debug('email', 'SendGrid sending mail to ' + mailToSend.to + '.');
  395. obj.sendGridServer
  396. .send(mailToSend)
  397. .then(function () {
  398. obj.sendingMail = false;
  399. parent.debug('email', 'SendGrid sending success.');
  400. obj.pendingMails.shift();
  401. obj.retry = 0;
  402. sendNextMail();
  403. }, function (error) {
  404. obj.sendingMail = false;
  405. parent.debug('email', 'SendGrid sending error: ' + JSON.stringify(error));
  406. obj.retry++;
  407. // Wait and try again
  408. if (obj.retry < 3) {
  409. setTimeout(sendNextMail, 10000);
  410. } else {
  411. // Failed, send the next mail
  412. parent.debug('email', 'SendGrid server failed (Skipping): ' + JSON.stringify(err));
  413. console.log('SendGrid server failed (Skipping): ' + JSON.stringify(err));
  414. obj.pendingMails.shift();
  415. obj.retry = 0;
  416. sendNextMail();
  417. }
  418. });
  419. } else if (obj.smtpServer != null) {
  420. parent.debug('email', 'SMTP sending mail to ' + mailToSend.to + '.');
  421. if (obj.smtpServer == 'console') {
  422. // Display the email on the console, this is for easy debugging
  423. if (mailToSend.from == null) { delete mailToSend.from; }
  424. if (mailToSend.html == null) { delete mailToSend.html; }
  425. console.log('Email', mailToSend);
  426. obj.sendingMail = false;
  427. obj.pendingMails.shift();
  428. obj.retry = 0;
  429. sendNextMail();
  430. } else {
  431. // SMTP send
  432. obj.smtpServer.sendMail(mailToSend, function (err, info) {
  433. parent.debug('email', 'SMTP response: ' + JSON.stringify(err) + ', ' + JSON.stringify(info));
  434. obj.sendingMail = false;
  435. if (err == null) {
  436. // Send the next mail
  437. obj.pendingMails.shift();
  438. obj.retry = 0;
  439. sendNextMail();
  440. } else {
  441. obj.retry++;
  442. parent.debug('email', 'SMTP server failed (Retry:' + obj.retry + '): ' + JSON.stringify(err));
  443. console.log('SMTP server failed (Retry:' + obj.retry + '/3): ' + JSON.stringify(err));
  444. // Wait and try again
  445. if (obj.retry < 3) {
  446. setTimeout(sendNextMail, 10000);
  447. } else {
  448. // Failed, send the next mail
  449. parent.debug('email', 'SMTP server failed (Skipping): ' + JSON.stringify(err));
  450. console.log('SMTP server failed (Skipping): ' + JSON.stringify(err));
  451. obj.pendingMails.shift();
  452. obj.retry = 0;
  453. sendNextMail();
  454. }
  455. }
  456. });
  457. }
  458. }
  459. }
  460. // Send out the next mail in the pending list
  461. obj.verify = function () {
  462. if ((obj.smtpServer == null) || (obj.smtpServer == 'console')) return;
  463. obj.smtpServer.verify(function (err, info) {
  464. if (err == null) {
  465. if (obj.config.smtp.host == 'gmail') {
  466. console.log('Gmail server with OAuth working as expected.');
  467. } else {
  468. console.log('SMTP mail server ' + obj.config.smtp.host + ' working as expected.');
  469. }
  470. } else {
  471. // Remove all non-object types from error to avoid a JSON stringify error.
  472. var err2 = {};
  473. for (var i in err) { if (typeof (err[i]) != 'object') { err2[i] = err[i]; } }
  474. parent.debug('email', 'SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2));
  475. console.log('SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2));
  476. }
  477. });
  478. };
  479. // Load the cookie encryption key from the database
  480. obj.parent.db.Get('MailCookieEncryptionKey', function (err, docs) {
  481. if ((docs.length > 0) && (docs[0].key != null) && (obj.parent.mailtokengen == null)) {
  482. // Key is present, use it.
  483. obj.mailCookieEncryptionKey = Buffer.from(docs[0].key, 'hex');
  484. } else {
  485. // Key is not present, generate one.
  486. obj.mailCookieEncryptionKey = obj.parent.generateCookieKey();
  487. obj.parent.db.Set({ _id: 'MailCookieEncryptionKey', key: obj.mailCookieEncryptionKey.toString('hex'), time: Date.now() });
  488. }
  489. });
  490. // Create a agent invitation link
  491. function createInviteLink(domain, meshid, flags, expirehours) {
  492. return '/agentinvite?c=' + parent.encodeCookie({ a: 4, mid: meshid, f: flags, expire: expirehours * 60 }, parent.invitationLinkEncryptionKey);
  493. }
  494. // Check the email domain DNS MX record.
  495. obj.approvedEmailDomains = {};
  496. obj.checkEmail = function (email, func) {
  497. if (obj.verifyemail == false) { func(true); return; }
  498. var emailSplit = email.split('@');
  499. if (emailSplit.length != 2) { func(false); return; }
  500. if (obj.approvedEmailDomains[emailSplit[1]] === true) { func(true); return; }
  501. require('dns').resolveMx(emailSplit[1], function (err, addresses) {
  502. parent.debug('email', "checkEmail: " + email + ", " + (err == null));
  503. if (err == null) { obj.approvedEmailDomains[emailSplit[1]] = true; }
  504. func(err == null);
  505. });
  506. }
  507. //
  508. // Device connection and disconnection notifications
  509. //
  510. obj.deviceNotifications = {}; // UserId --> { timer, nodes: nodeid --> connectType }
  511. // A device connected and a user needs to be notified about it.
  512. obj.notifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
  513. const mesh = parent.webserver.meshes[meshid];
  514. if (mesh == null) return;
  515. // Add the user and start a timer
  516. if (obj.deviceNotifications[user._id] == null) {
  517. obj.deviceNotifications[user._id] = { nodes: {} };
  518. obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, obj.emailDelay);
  519. }
  520. // Add the device
  521. if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
  522. obj.deviceNotifications[user._id].nodes[nodeid] = { c: connectType }; // This device connection need to be added
  523. } else {
  524. const info = obj.deviceNotifications[user._id].nodes[nodeid];
  525. if ((info.d != null) && ((info.d & connectType) != 0)) {
  526. info.d -= connectType; // This device disconnect cancels out a device connection
  527. if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
  528. // This device no longer needs a notification
  529. delete obj.deviceNotifications[user._id].nodes[nodeid];
  530. if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
  531. // This user no longer needs a notification
  532. clearTimeout(obj.deviceNotifications[user._id].timer);
  533. delete obj.deviceNotifications[user._id];
  534. }
  535. return;
  536. }
  537. } else {
  538. if (info.c != null) {
  539. info.c |= connectType; // This device disconnect needs to be added
  540. } else {
  541. info.c = connectType; // This device disconnect needs to be added
  542. }
  543. }
  544. }
  545. // Set the device group name
  546. if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
  547. obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
  548. }
  549. // Cancel a device disconnect notification
  550. obj.cancelNotifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
  551. const mesh = parent.webserver.meshes[meshid];
  552. if (mesh == null) return;
  553. if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
  554. const info = obj.deviceNotifications[user._id].nodes[nodeid];
  555. if ((info.d != null) && ((info.d & connectType) != 0)) {
  556. info.d -= connectType; // This device disconnect cancels out a device connection
  557. if (((info.c == null) || (info.c == 0)) && ((info.d == null) || (info.d == 0))) {
  558. // This device no longer needs a notification
  559. delete obj.deviceNotifications[user._id].nodes[nodeid];
  560. if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
  561. // This user no longer needs a notification
  562. clearTimeout(obj.deviceNotifications[user._id].timer);
  563. delete obj.deviceNotifications[user._id];
  564. }
  565. }
  566. }
  567. }
  568. }
  569. // A device disconnected and a user needs to be notified about it.
  570. obj.notifyDeviceDisconnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
  571. const mesh = parent.webserver.meshes[meshid];
  572. if (mesh == null) return;
  573. // Add the user and start a timer
  574. if (obj.deviceNotifications[user._id] == null) {
  575. obj.deviceNotifications[user._id] = { nodes: {} };
  576. obj.deviceNotifications[user._id].timer = setTimeout(function () { sendDeviceNotifications(user._id); }, obj.emailDelay);
  577. }
  578. // Add the device
  579. if (obj.deviceNotifications[user._id].nodes[nodeid] == null) {
  580. obj.deviceNotifications[user._id].nodes[nodeid] = { d: connectType }; // This device disconnect need to be added
  581. } else {
  582. const info = obj.deviceNotifications[user._id].nodes[nodeid];
  583. if ((info.c != null) && ((info.c & connectType) != 0)) {
  584. info.c -= connectType; // This device disconnect cancels out a device connection
  585. if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
  586. // This device no longer needs a notification
  587. delete obj.deviceNotifications[user._id].nodes[nodeid];
  588. if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
  589. // This user no longer needs a notification
  590. clearTimeout(obj.deviceNotifications[user._id].timer);
  591. delete obj.deviceNotifications[user._id];
  592. }
  593. return;
  594. }
  595. } else {
  596. if (info.d != null) {
  597. info.d |= connectType; // This device disconnect needs to be added
  598. } else {
  599. info.d = connectType; // This device disconnect needs to be added
  600. }
  601. }
  602. }
  603. // Set the device group name
  604. if ((extraInfo != null) && (extraInfo.name != null)) { obj.deviceNotifications[user._id].nodes[nodeid].nn = extraInfo.name; }
  605. obj.deviceNotifications[user._id].nodes[nodeid].mn = mesh.name;
  606. }
  607. // Cancel a device connect notification
  608. obj.cancelNotifyDeviceConnect = function (user, meshid, nodeid, connectTime, connectType, powerState, serverid, extraInfo) {
  609. const mesh = parent.webserver.meshes[meshid];
  610. if (mesh == null) return;
  611. if ((obj.deviceNotifications[user._id] != null) && (obj.deviceNotifications[user._id].nodes[nodeid] != null)) {
  612. const info = obj.deviceNotifications[user._id].nodes[nodeid];
  613. if ((info.c != null) && ((info.c & connectType) != 0)) {
  614. info.c -= connectType; // This device disconnect cancels out a device connection
  615. if (((info.d == null) || (info.d == 0)) && ((info.c == null) || (info.c == 0))) {
  616. // This device no longer needs a notification
  617. delete obj.deviceNotifications[user._id].nodes[nodeid];
  618. if (Object.keys(obj.deviceNotifications[user._id].nodes).length == 0) {
  619. // This user no longer needs a notification
  620. clearTimeout(obj.deviceNotifications[user._id].timer);
  621. delete obj.deviceNotifications[user._id];
  622. }
  623. }
  624. }
  625. }
  626. }
  627. // Send a notification about device connections and disconnections to a user
  628. function sendDeviceNotifications(userid) {
  629. if (obj.deviceNotifications[userid] == null) return;
  630. clearTimeout(obj.deviceNotifications[userid].timer);
  631. var connections = [];
  632. var disconnections = [];
  633. for (var nodeid in obj.deviceNotifications[userid].nodes) {
  634. var info = obj.deviceNotifications[userid].nodes[nodeid];
  635. if ((info.c != null) && (info.c > 0) && (info.nn != null) && (info.mn != null)) {
  636. var c = [];
  637. if (info.c & 1) { c.push("Agent"); }
  638. if (info.c & 2) { c.push("CIRA"); }
  639. if (info.c & 4) { c.push("AMT"); }
  640. if (info.c & 8) { c.push("AMT-Relay"); }
  641. if (info.c & 16) { c.push("MQTT"); }
  642. connections.push(info.mn + ', ' + info.nn + ': ' + c.join(', '));
  643. }
  644. if ((info.d != null) && (info.d > 0) && (info.nn != null) && (info.mn != null)) {
  645. var d = [];
  646. if (info.d & 1) { d.push("Agent"); }
  647. if (info.d & 2) { d.push("CIRA"); }
  648. if (info.d & 4) { d.push("AMT"); }
  649. if (info.d & 8) { d.push("AMT-Relay"); }
  650. if (info.d & 16) { d.push("MQTT"); }
  651. disconnections.push(info.mn + ', ' + info.nn + ': ' + d.join(', '));
  652. }
  653. }
  654. // Sort the notifications
  655. connections.sort(sortCollator.compare);
  656. disconnections.sort(sortCollator.compare);
  657. // Get the user and domain
  658. const user = parent.webserver.users[userid];
  659. if ((user == null) || (user.email == null) || (user.emailVerified !== true)) return;
  660. const domain = obj.parent.config.domains[user.domain];
  661. if (domain == null) return;
  662. // Send the email
  663. obj.sendDeviceNotifyMail(domain, user.name, user.email, connections, disconnections, user.llang, null);
  664. // Clean up
  665. delete obj.deviceNotifications[userid];
  666. }
  667. return obj;
  668. };