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