From d96bf4b4f51de0715af64941dd0c9266e6f6d9e4 Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Wed, 10 Feb 2021 11:28:21 -0800 Subject: [PATCH] Added per-domain SMTP/SendGrid support. --- meshcentral-config-schema.json | 26 ++++++++++++++ meshcentral.js | 28 +++++++++++++-- meshmail.js | 64 +++++++++++++++++++--------------- meshuser.js | 30 ++++++++-------- webserver.js | 52 +++++++++++++-------------- 5 files changed, 127 insertions(+), 73 deletions(-) diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 87801fb7..e7625324 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -506,6 +506,32 @@ "required": [ "protocols" ] }, "showPasswordLogin": { "type": "boolean", "default": true, "description": "When set to false, hides the username and password prompt on login screen." }, + "sendgrid": { + "title" : "SendGrid.com Email server", + "description": "Connects MeshCentral to the SendGrid email server, allows MeshCentral to send email messages for 2FA or user notification.", + "type": "object", + "properties": { + "from": { "type": "string", "format": "email", "description": "Email address used in the messages from field." }, + "apikey": { "type": "string", "description": "The SendGrid API key." }, + "verifyemail": { "type": "boolean", "default": true, "description": "When set to false, the email format and DNS MX record are not checked." } + }, + "required": [ "from", "apikey" ] + }, + "smtp": { + "title" : "SMTP email server", + "description": "Connects MeshCentral to a SMTP email server, allows MeshCentral to send email messages for 2FA or user notification.", + "type": "object", + "properties": { + "host": { "type": "string", "format": "hostname" }, + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "from": { "type": "string", "format": "email", "description": "Email address used in the messages from field." }, + "tls": { "type": "boolean" }, + "tlscertcheck": { "type": "boolean" }, + "tlsstrict": { "type": "boolean" }, + "verifyemail": { "type": "boolean", "default": true, "description": "When set to false, the email format and DNS MX record are not checked." } + }, + "required": [ "host", "port", "from", "tls" ] + }, "authStrategies": { "type": "object", "additionalProperties": false, diff --git a/meshcentral.js b/meshcentral.js index 0150ee67..5ebd1361 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -1525,7 +1525,7 @@ function CreateMeshCentralServer(config, args) { obj.swarmserver = require('./swarmserver.js').CreateSwarmServer(obj, obj.db, obj.args, obj.certificates); } - // Setup email server + // Setup the main email server if (obj.config.sendgrid != null) { // Sendgrid server obj.mailserver = require('./meshmail.js').CreateMeshMail(obj); @@ -1538,6 +1538,24 @@ function CreateMeshCentralServer(config, args) { if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode."); } } + // Setup the email server for each domain + for (i in obj.config.domains) { + if (obj.config.domains[i].sendgrid != null) { + // Sendgrid server + obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]); + obj.config.domains[i].mailserver.verify(); + if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode."); } + } else if ((obj.config.domains[i].smtp != null) && (obj.config.domains[i].smtp.host != null) && (obj.config.domains[i].smtp.from != null)) { + // SMTP server + obj.config.domains[i].mailserver = require('./meshmail.js').CreateMeshMail(obj, obj.config.domains[i]); + obj.config.domains[i].mailserver.verify(); + if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode."); } + } else { + // Setup the parent mail server for this domain + if (obj.mailserver != null) { obj.config.domains[i].mailserver = obj.mailserver; } + } + } + // Setup SMS gateway if (config.sms != null) { obj.smsserver = require('./meshsms.js').CreateMeshSMS(obj); @@ -2994,10 +3012,14 @@ function mainStart() { var recordingIndex = false; var domainCount = 0; var wildleek = false; + var nodemailer = false; + var sendgrid = false; if (require('os').platform() == 'win32') { for (var i in config.domains) { domainCount++; if (config.domains[i].auth == 'sspi') { sspi = true; } else { allsspi = false; } } } else { allsspi = false; } if (domainCount == 0) { allsspi = false; } for (var i in config.domains) { if (i.startsWith('_')) continue; + if (config.domains[i].smtp != null) { nodemailer = true; } + if (config.domains[i].sendgrid != null) { sendgrid = true; } if (config.domains[i].yubikey != null) { yubikey = true; } if (config.domains[i].auth == 'ldap') { ldap = true; } if (config.domains[i].mstsc === true) { mstsc = true; } @@ -3030,8 +3052,8 @@ function mainStart() { if (config.settings.plugins != null) { modules.push('semver'); } // Required for version compat testing and update checks if ((config.settings.plugins != null) && (config.settings.plugins.proxy != null)) { modules.push('https-proxy-agent'); } // Required for HTTP/HTTPS proxy support else if (config.settings.xmongodb != null) { modules.push('mongojs'); } // Add MongoJS, old driver. - if (config.smtp != null) { modules.push('nodemailer'); } // Add SMTP support - if (config.sendgrid != null) { modules.push('@sendgrid/mail'); } // Add SendGrid support + if (nodemailer || (config.smtp != null)) { modules.push('nodemailer'); } // Add SMTP support + if (sendgrid || (config.sendgrid != null)) { modules.push('@sendgrid/mail'); } // Add SendGrid support if (args.translate) { modules.push('jsdom'); modules.push('esprima'); modules.push('minify-js'); modules.push('html-minifier'); } // Translation support // If running NodeJS < 8, install "util.promisify" diff --git a/meshmail.js b/meshmail.js index 8fe8c659..2b1fba08 100644 --- a/meshmail.js +++ b/meshmail.js @@ -17,32 +17,39 @@ // TODO: Add NTML support with "nodemailer-ntlm-auth" https://github.com/nodemailer/nodemailer-ntlm-auth // Construct a MeshAgent object, called upon connection -module.exports.CreateMeshMail = function (parent) { +module.exports.CreateMeshMail = function (parent, domain) { var obj = {}; obj.pendingMails = []; obj.parent = parent; obj.retry = 0; obj.sendingMail = false; obj.mailCookieEncryptionKey = null; + obj.verifyemail = false; + obj.domain = domain; //obj.mailTemplates = {}; 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. function EscapeHtml(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(//g, '>').replace(/').replace(/\n/g, '').replace(/\t/g, '  '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; } - if (parent.config.sendgrid != null) { + // Setup where we read our configuration from + if (obj.domain == null) { obj.config = parent.config; } else { obj.config = domain; } + + if (obj.config.sendgrid != null) { // Setup SendGrid mail server obj.sendGridServer = require('@sendgrid/mail'); - obj.sendGridServer.setApiKey(parent.config.sendgrid.apikey); - } else if (parent.config.smtp != null) { + obj.sendGridServer.setApiKey(obj.config.sendgrid.apikey); + if (obj.config.sendgrid.verifyemail == true) { obj.verifyemail = true; } + } else if (obj.config.smtp != null) { // Setup SMTP mail server const nodemailer = require('nodemailer'); - var options = { host: parent.config.smtp.host, secure: (parent.config.smtp.tls == true), tls: {} }; - //var options = { host: parent.config.smtp.host, secure: (parent.config.smtp.tls == true), tls: { secureProtocol: 'SSLv23_method', ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE, rejectUnauthorized: false } }; - if (parent.config.smtp.port != null) { options.port = parent.config.smtp.port; } - if (parent.config.smtp.tlscertcheck === false) { options.tls.rejectUnauthorized = false; } - if (parent.config.smtp.tlsstrict === true) { options.tls.secureProtocol = 'SSLv23_method'; options.tls.ciphers = 'RSA+AES:!aNULL:!MD5:!DSS'; options.tls.secureOptions = constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE; } - if ((parent.config.smtp.user != null) && (parent.config.smtp.pass != null)) { options.auth = { user: parent.config.smtp.user, pass: parent.config.smtp.pass }; } + var options = { host: obj.config.smtp.host, secure: (obj.config.smtp.tls == true), tls: {} }; + //var options = { host: obj.config.smtp.host, secure: (obj.config.smtp.tls == true), tls: { secureProtocol: 'SSLv23_method', ciphers: 'RSA+AES:!aNULL:!MD5:!DSS', secureOptions: constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE, rejectUnauthorized: false } }; + if (obj.config.smtp.port != null) { options.port = obj.config.smtp.port; } + if (obj.config.smtp.tlscertcheck === false) { options.tls.rejectUnauthorized = false; } + if (obj.config.smtp.tlsstrict === true) { options.tls.secureProtocol = 'SSLv23_method'; options.tls.ciphers = 'RSA+AES:!aNULL:!MD5:!DSS'; options.tls.secureOptions = constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE; } + if ((obj.config.smtp.user != null) && (obj.config.smtp.pass != null)) { options.auth = { user: obj.config.smtp.user, pass: obj.config.smtp.pass }; } + if (obj.config.smtp.verifyemail == true) { obj.verifyemail = true; } obj.smtpServer = nodemailer.createTransport(options); } @@ -149,10 +156,10 @@ module.exports.CreateMeshMail = function (parent) { // Send a generic email obj.sendMail = function (to, subject, text, html) { - if (parent.config.sendgrid != null) { - obj.pendingMails.push({ to: to, from: parent.config.sendgrid.from, subject: subject, text: text, html: html }); - } else if (parent.config.smtp != null) { - obj.pendingMails.push({ to: to, from: parent.config.smtp.from, subject: subject, text: text, html: html }); + if (obj.config.sendgrid != null) { + obj.pendingMails.push({ to: to, from: obj.config.sendgrid.from, subject: subject, text: text, html: html }); + } else if (obj.config.smtp != null) { + obj.pendingMails.push({ to: to, from: obj.config.smtp.from, subject: subject, text: text, html: html }); } sendNextMail(); }; @@ -180,8 +187,8 @@ module.exports.CreateMeshMail = function (parent) { // Get from field var from = null; - if (parent.config.sendgrid && (typeof parent.config.sendgrid.from == 'string')) { from = parent.config.sendgrid.from; } - else if (parent.config.smtp && (typeof parent.config.smtp.from == 'string')) { from = parent.config.smtp.from; } + if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; } + else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; } // Send the email obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) }); @@ -213,8 +220,8 @@ module.exports.CreateMeshMail = function (parent) { // Get from field var from = null; - if (parent.config.sendgrid && (typeof parent.config.sendgrid.from == 'string')) { from = parent.config.sendgrid.from; } - else if (parent.config.smtp && (typeof parent.config.smtp.from == 'string')) { from = parent.config.smtp.from; } + if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; } + else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; } // Send the email obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) }); @@ -247,8 +254,8 @@ module.exports.CreateMeshMail = function (parent) { // Get from field var from = null; - if (parent.config.sendgrid && (typeof parent.config.sendgrid.from == 'string')) { from = parent.config.sendgrid.from; } - else if (parent.config.smtp && (typeof parent.config.smtp.from == 'string')) { from = parent.config.smtp.from; } + if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; } + else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; } // Send the email obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) }); @@ -281,8 +288,8 @@ module.exports.CreateMeshMail = function (parent) { // Get from field var from = null; - if (parent.config.sendgrid && (typeof parent.config.sendgrid.from == 'string')) { from = parent.config.sendgrid.from; } - else if (parent.config.smtp && (typeof parent.config.smtp.from == 'string')) { from = parent.config.smtp.from; } + if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; } + else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; } // Send the email obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) }); @@ -319,8 +326,8 @@ module.exports.CreateMeshMail = function (parent) { // Get from field var from = null; - if (parent.config.sendgrid && (typeof parent.config.sendgrid.from == 'string')) { from = parent.config.sendgrid.from; } - else if (parent.config.smtp && (typeof parent.config.smtp.from == 'string')) { from = parent.config.smtp.from; } + if (obj.config.sendgrid && (typeof obj.config.sendgrid.from == 'string')) { from = obj.config.sendgrid.from; } + else if (obj.config.smtp && (typeof obj.config.smtp.from == 'string')) { from = obj.config.smtp.from; } // Send the email obj.pendingMails.push({ to: email, from: from, subject: mailReplacements(template.htmlSubject, domain, options), text: mailReplacements(template.txt, domain, options), html: mailReplacements(template.html, domain, options) }); @@ -399,13 +406,13 @@ module.exports.CreateMeshMail = function (parent) { if (obj.smtpServer == null) return; obj.smtpServer.verify(function (err, info) { if (err == null) { - console.log('SMTP mail server ' + parent.config.smtp.host + ' working as expected.'); + console.log('SMTP mail server ' + obj.config.smtp.host + ' working as expected.'); } else { // Remove all non-object types from error to avoid a JSON stringify error. var err2 = {}; for (var i in err) { if (typeof (err[i]) != 'object') { err2[i] = err[i]; } } - parent.debug('email', 'SMTP mail server ' + parent.config.smtp.host + ' failed: ' + JSON.stringify(err2)); - console.log('SMTP mail server ' + parent.config.smtp.host + ' failed: ' + JSON.stringify(err2)); + parent.debug('email', 'SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2)); + console.log('SMTP mail server ' + obj.config.smtp.host + ' failed: ' + JSON.stringify(err2)); } }); }; @@ -430,8 +437,7 @@ module.exports.CreateMeshMail = function (parent) { // Check the email domain DNS MX record. obj.approvedEmailDomains = {}; obj.checkEmail = function (email, func) { - if ((parent.config.smtp) && (parent.config.smtp.verifyemail === false)) { func(true); return; } - if ((parent.config.sendgrid) && (parent.config.sendgrid.verifyemail === false)) { func(true); return; } + if (obj.verifyemail == false) { func(true); return; } var emailSplit = email.split('@'); if (emailSplit.length != 2) { func(false); return; } if (obj.approvedEmailDomains[emailSplit[1]] === true) { func(true); return; } diff --git a/meshuser.js b/meshuser.js index 403e6991..65643e39 100644 --- a/meshuser.js +++ b/meshuser.js @@ -446,7 +446,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use var httpport = ((args.aliasport != null) ? args.aliasport : args.port); // Build server information object - var serverinfo = { domain: domain.id, name: domain.dns ? domain.dns : parent.certificates.CommonName, mpsname: parent.certificates.AmtMpsName, mpsport: mpsport, mpspass: args.mpspass, port: httpport, emailcheck: ((parent.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (args.lanonly != true) && (parent.certificates.CommonName != null) && (parent.certificates.CommonName.indexOf('.') != -1) && (user._id.split('/')[2].startsWith('~') == false)), domainauth: (domain.auth == 'sspi'), serverTime: Date.now() }; + var serverinfo = { domain: domain.id, name: domain.dns ? domain.dns : parent.certificates.CommonName, mpsname: parent.certificates.AmtMpsName, mpsport: mpsport, mpspass: args.mpspass, port: httpport, emailcheck: ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (args.lanonly != true) && (parent.certificates.CommonName != null) && (parent.certificates.CommonName.indexOf('.') != -1) && (user._id.split('/')[2].startsWith('~') == false)), domainauth: (domain.auth == 'sspi'), serverTime: Date.now() }; serverinfo.languages = parent.renderLanguages; serverinfo.tlshash = Buffer.from(parent.webCertificateFullHashs[domain.id], 'binary').toString('hex').toUpperCase(); // SHA384 of server HTTPS certificate serverinfo.agentCertHash = parent.agentCertificateHashBase64; @@ -978,13 +978,13 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use break; } case 'email': { - if (parent.parent.mailserver == null) { + if (domain.mailserver == null) { r = "No email service enabled."; } else { if (cmdargs['_'].length != 3) { r = "Usage: email \"user@sample.com\" \"Subject\" \"Message\"."; } else { - parent.parent.mailserver.sendMail(cmdargs['_'][0], cmdargs['_'][1], cmdargs['_'][2]); + domain.mailserver.sendMail(cmdargs['_'][0], cmdargs['_'][1], cmdargs['_'][2]); r = "Done."; } } @@ -1626,7 +1626,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if (parent.parent.authlog) { parent.parent.authLog('https', 'User ' + user.name + ' changed email from ' + oldemail + ' to ' + user.email); } // Send the verification email - if (parent.parent.mailserver != null) { parent.parent.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, parent.getLanguageCodes(req)); } + if (domain.mailserver != null) { domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, parent.getLanguageCodes(req)); } } }); } @@ -1644,9 +1644,9 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Always lowercase the email address command.email = command.email.toLowerCase(); - if ((parent.parent.mailserver != null) && (obj.user.email.toLowerCase() == command.email)) { + if ((domain.mailserver != null) && (obj.user.email.toLowerCase() == command.email)) { // Send the verification email - parent.parent.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, parent.getLanguageCodes(req)); + domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, parent.getLanguageCodes(req)); } break; } @@ -2039,8 +2039,8 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use parent.parent.DispatchEvent(targets, obj, event); // Perform email invitation - if ((command.emailInvitation == true) && (command.emailVerified == true) && command.email && parent.parent.mailserver) { - parent.parent.mailserver.sendAccountInviteMail(newuserdomain, (user.realname ? user.realname : user.name), newusername, command.email.toLowerCase(), command.pass, parent.getLanguageCodes(req)); + if ((command.emailInvitation == true) && (command.emailVerified == true) && command.email && domain.mailserver) { + domain.mailserver.sendAccountInviteMail(newuserdomain, (user.realname ? user.realname : user.name), newusername, command.email.toLowerCase(), command.pass, parent.getLanguageCodes(req)); } // Log in the auth log @@ -2237,7 +2237,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } // In some situations, we need a verified email address to create a device group. - if ((err == null) && (parent.parent.mailserver != null) && (ugrpdomain.auth != 'sspi') && (ugrpdomain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != SITERIGHT_ADMIN)) { err = "Email verification required"; } // User must verify it's email first. + if ((err == null) && (domain.mailserver != null) && (ugrpdomain.auth != 'sspi') && (ugrpdomain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != SITERIGHT_ADMIN)) { err = "Email verification required"; } // User must verify it's email first. } catch (ex) { err = "Validation exception: " + ex; } // Handle any errors @@ -2861,7 +2861,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use if ((user.siteadmin != SITERIGHT_ADMIN) && ((user.siteadmin & 64) != 0)) { err = 'Permission denied'; } // In some situations, we need a verified email address to create a device group. - else if ((parent.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != SITERIGHT_ADMIN)) { err = 'Email verification required'; } // User must verify it's email first. + else if ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (user.emailVerified !== true) && (user.siteadmin != SITERIGHT_ADMIN)) { err = 'Email verification required'; } // User must verify it's email first. // Create mesh else if (common.validateString(command.meshname, 1, 128) == false) { err = 'Invalid group name'; } // Meshname is between 1 and 64 characters @@ -4166,7 +4166,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } try { - if ((parent.parent.mailserver == null) || (args.lanonly == true)) { err = 'Unsupported feature'; } // This operation requires the email server + if ((domain.mailserver == null) || (args.lanonly == true)) { err = 'Unsupported feature'; } // This operation requires the email server else if ((parent.parent.certificates.CommonName == null) || (parent.parent.certificates.CommonName.indexOf('.') == -1)) { err = 'Unsupported feature'; } // Server name must be configured else if (common.validateString(command.meshid, 1, 1024) == false) { err = 'Invalid group identifier'; } // Check meshid else { @@ -4190,7 +4190,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } // Perform email invitation - parent.parent.mailserver.sendAgentInviteMail(domain, (user.realname ? user.realname : user.name), command.email.toLowerCase(), command.meshid, command.name, command.os, command.msg, command.flags, command.expire, parent.getLanguageCodes(req), req.query.key); + domain.mailserver.sendAgentInviteMail(domain, (user.realname ? user.realname : user.name), command.email.toLowerCase(), command.meshid, command.name, command.os, command.msg, command.flags, command.expire, parent.getLanguageCodes(req), req.query.key); // Send a response if needed if (command.responseid != null) { try { ws.send(JSON.stringify({ action: 'inviteAgent', responseid: command.responseid, result: 'ok' })); } catch (ex) { } } @@ -4632,7 +4632,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } case 'emailuser': { // Send a email message to a user var errMsg = null, emailuser = null; - if (parent.parent.mailserver == null) { errMsg = 'Email server not enabled'; } + if (domain.mailserver == null) { errMsg = 'Email server not enabled'; } else if ((user.siteadmin & 2) == 0) { errMsg = 'No user management rights'; } else if (common.validateString(command.userid, 1, 2048) == false) { errMsg = 'Invalid userid'; } else if (common.validateString(command.subject, 1, 1000) == false) { errMsg = 'Invalid subject message'; } @@ -4645,7 +4645,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use } if (errMsg != null) { displayNotificationMessage(errMsg); break; } - parent.parent.mailserver.sendMail(emailuser.email, command.subject, command.msg); + domain.mailserver.sendMail(emailuser.email, command.subject, command.msg); displayNotificationMessage("Email sent.", null, null, null, 14); break; } @@ -5526,7 +5526,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use // Return the number of 2nd factor for this account function count2factoraAuths() { - var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.parent.mailserver != null)); + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)); var sms2fa = ((parent.parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))); var authFactorCount = 0; if (typeof user.otpsecret == 'string') { authFactorCount++; } // Authenticator time factor diff --git a/webserver.js b/webserver.js index da488931..ffb604b9 100644 --- a/webserver.js +++ b/webserver.js @@ -706,7 +706,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); // Check if a 2nd factor is present - return ((parent.config.settings.no2factorauth !== true) && (sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null)) || ((user.otphkeys != null) && (user.otphkeys.length > 0)))); + return ((parent.config.settings.no2factorauth !== true) && (sms2fa || (user.otpsecret != null) || ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)) || ((user.otphkeys != null) && (user.otphkeys.length > 0)))); } // Check the 2-step auth token @@ -716,7 +716,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (twoStepLoginSupported == false) { parent.debug('web', 'checkUserOneTimePassword: not supported.'); func(true); return; }; // Check if we can use OTP tokens with email - var otpemail = (parent.mailserver != null); + var otpemail = (domain.mailserver != null); if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; } var otpsms = (parent.smsserver != null); if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; } @@ -919,7 +919,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { return; } - var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null)); + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.email != null) && (user.emailVerified == true) && (user.otpekey != null)); var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); // Check if this user has 2-step login active @@ -928,7 +928,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; obj.db.SetUser(user); parent.debug('web', 'Sending 2FA email to: ' + user.email); - parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key); + domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key); req.session.messageid = 2; // "Email sent" message req.session.loginmode = '4'; if (direct === true) { handleRootRequestEx(req, res, domain); } else { res.redirect(domain.url + getQueryPortion(req)); } @@ -967,7 +967,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Wait and redirect the user setTimeout(function () { req.session.loginmode = '4'; - req.session.tokenemail = ((user.email != null) && (user.emailVerified == true) && (parent.mailserver != null) && (user.otpekey != null)); + req.session.tokenemail = ((user.email != null) && (user.emailVerified == true) && (domain.mailserver != null) && (user.otpekey != null)); req.session.tokensms = ((user.phone != null) && (parent.smsserver != null)); req.session.tokenuserid = userid; req.session.tokenusername = xusername; @@ -984,7 +984,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Check if email address needs to be confirmed - var emailcheck = ((obj.parent.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap')) + var emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap')) if (emailcheck && (user.emailVerified !== true)) { parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page'); req.session.messageid = 3; // "Email verification required" message @@ -1005,7 +1005,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Check if email address needs to be confirmed - var emailcheck = ((obj.parent.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap')) + var emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap')) if (emailcheck && (user.emailVerified !== true)) { parent.debug('web', 'Redirecting using ' + user.name + ' to email check login page'); req.session.messageid = 3; // "Email verification required" message @@ -1261,7 +1261,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { obj.db.SetUser(user); // Send the verification email - if ((obj.parent.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (obj.common.validateEmail(user.email, 1, 256) == true)) { obj.parent.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); } + if ((domain.mailserver != null) && (domain.auth != 'sspi') && (domain.auth != 'ldap') && (obj.common.validateEmail(user.email, 1, 256) == true)) { domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); } }, 0); var event = { etype: 'user', userid: user._id, username: user.name, account: obj.CloneSafeUser(user), action: 'accountcreate', msg: 'Account created, email is ' + req.body.email, domain: domain.id }; if (obj.db.changeStream) { event.noact = 1; } // If DB change stream is active, don't use this event to create the user. Another event will come. @@ -1458,8 +1458,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } else { // Send email to perform recovery. delete req.session.tokenemail; - if (obj.parent.mailserver != null) { - obj.parent.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); + if (domain.mailserver != null) { + domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); if (i == 0) { parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.'); req.session.loginmode = '1'; @@ -1478,8 +1478,8 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { }); } else { // No second factor, send email to perform recovery. - if (obj.parent.mailserver != null) { - obj.parent.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); + if (domain.mailserver != null) { + domain.mailserver.sendAccountResetMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); if (i == 0) { parent.debug('web', 'handleResetAccountRequest: Hold on, reset mail sent.'); req.session.loginmode = '1'; @@ -1505,7 +1505,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function handleCheckAccountEmailRequest(req, res, direct) { const domain = checkUserIpAddress(req, res); if (domain == null) { return; } - if ((obj.parent.mailserver == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.session.cuserid != 'string') || (obj.users[req.session.cuserid] == null) || (!obj.common.validateEmail(req.body.email, 1, 256))) { parent.debug('web', 'handleCheckAccountEmailRequest: failed checks.'); res.sendStatus(404); return; } + if ((domain.mailserver == null) || (domain.auth == 'sspi') || (domain.auth == 'ldap') || (typeof req.session.cuserid != 'string') || (obj.users[req.session.cuserid] == null) || (!obj.common.validateEmail(req.body.email, 1, 256))) { parent.debug('web', 'handleCheckAccountEmailRequest: failed checks.'); res.sendStatus(404); return; } if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key // Always lowercase the email address @@ -1563,7 +1563,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { } // Send the verification email - obj.parent.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); + domain.mailserver.sendAccountCheckMail(domain, user.name, user._id, user.email, obj.getLanguageCodes(req), req.query.key); // Send the response req.session.messageid = 2; // Email sent. @@ -1579,11 +1579,11 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { function handleCheckMailRequest(req, res) { const domain = checkUserIpAddress(req, res); if (domain == null) { return; } - if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (obj.parent.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; } + if ((domain.auth == 'sspi') || (domain.auth == 'ldap') || (domain.mailserver == null)) { parent.debug('web', 'handleCheckMailRequest: failed checks.'); res.sendStatus(404); return; } if ((domain.loginkey != null) && (domain.loginkey.indexOf(req.query.key) == -1)) { res.sendStatus(404); return; } // Check 3FA URL key if (req.query.c != null) { - var cookie = obj.parent.decodeCookie(req.query.c, obj.parent.mailserver.mailCookieEncryptionKey, 30); + var cookie = obj.parent.decodeCookie(req.query.c, domain.mailserver.mailCookieEncryptionKey, 30); if ((cookie != null) && (cookie.u != null) && (cookie.u.startsWith('user/')) && (cookie.e != null)) { var idsplit = cookie.u.split('/'); if ((idsplit.length != 3) || (idsplit[1] != domain.id)) { @@ -2427,7 +2427,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (domain.userQuota == -1) { features += 0x00000008; } // No server files mode if (obj.args.mpstlsoffload) { features += 0x00000010; } // No mutual-auth CIRA if ((parent.config.settings.allowframing != null) || (domain.allowframing != null)) { features += 0x00000020; } // Allow site within iframe - if ((obj.parent.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites + if ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true)) { features += 0x00000040; } // Email invites if (obj.args.webrtc == true) { features += 0x00000080; } // Enable WebRTC (Default false for now) // 0x00000100 --> This feature flag is free for future use. if (obj.args.allowhighqualitydesktop !== false) { features += 0x00000200; } // Enable AllowHighQualityDesktop (Default true) @@ -2453,7 +2453,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (domain.amtacmactivation) { features += 0x00100000; } // Intel AMT ACM activation/upgrade is possible if (domain.usernameisemail) { features += 0x00200000; } // Username is email address if (parent.mqttbroker != null) { features += 0x00400000; } // This server supports MQTT channels - if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed + if (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null)) { features += 0x00800000; } // using email for 2FA is allowed if (domain.agentinvitecodes == true) { features += 0x01000000; } // Support for agent invite codes if (parent.smsserver != null) { features += 0x02000000; } // SMS messaging is supported if ((parent.smsserver != null) && ((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false))) { features += 0x04000000; } // SMS 2FA is allowed @@ -2601,7 +2601,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { delete req.session.messageid; delete req.session.passhint; } - var emailcheck = ((obj.parent.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap')) + var emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap')) // Check if we are allowed to create new users using the login screen var newAccountsAllowed = true; @@ -2613,7 +2613,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (hardwareKeyChallenge) { hwstate = obj.parent.encodeCookie({ u: req.session.tokenusername, p: req.session.tokenpassword, c: req.session.u2fchallenge }, obj.parent.loginCookieEncryptionKey) } // Check if we can use OTP tokens with email. We can't use email for 2FA password recovery (loginmode 5). - var otpemail = (loginmode != 5) && (parent.mailserver != null) && (req.session != null) && ((req.session.tokenemail == true) || (typeof req.session.tokenemail == 'string')); + var otpemail = (loginmode != 5) && (domain.mailserver != null) && (req.session != null) && ((req.session.tokenemail == true) || (typeof req.session.tokenemail == 'string')); if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.email2factor == false)) { otpemail = false; } var otpsms = (parent.smsserver != null) && (req.session != null) && (req.session.tokensms == true); if ((typeof domain.passwordrequirements == 'object') && (domain.passwordrequirements.sms2factor == false)) { otpsms = false; } @@ -5669,7 +5669,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (domain == null) { parent.debug('web', 'WSERROR: Got no domain, user auth required.'); return; } } - var emailcheck = ((obj.parent.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap')) + var emailcheck = ((domain.mailserver != null) && (obj.parent.certificates.CommonName != null) && (obj.parent.certificates.CommonName.indexOf('.') != -1) && (obj.args.lanonly != true) && (domain.auth != 'sspi') && (domain.auth != 'ldap')) // A web socket session can be authenticated in many ways (Default user, session, user/pass and cookie). Check authentication here. if ((req.query.user != null) && (req.query.pass != null)) { @@ -5685,7 +5685,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Check if a 2nd factor is needed if (checkUserOneTimePasswordRequired(domain, user, req) == true) { // Figure out if email 2FA is allowed - var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.otpekey != null)); + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null)); var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); if ((typeof req.query.token != 'string') || (req.query.token == '**email**') || (req.query.token == '**sms**')) { if ((req.query.token == '**email**') && (email2fa == true)) { @@ -5693,7 +5693,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; obj.db.SetUser(user); parent.debug('web', 'Sending 2FA email to: ' + user.email); - parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key); + domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key); // Ask for a login token & confirm email was sent try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { } } else if ((req.query.token == '**sms**') && (sms2fa == true)) { @@ -5731,7 +5731,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { // Check email verification if (emailcheck && (user.email != null) && (user.emailVerified !== true)) { parent.debug('web', 'Invalid login, asking for email validation'); - var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.otpekey != null)); + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null)); var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); try { ws.send(JSON.stringify({ action: 'close', cause: 'emailvalidation', msg: 'emailvalidationrequired', email2fa: email2fa, sms2fa: sms2fa, email2fasent: true })); ws.close(); } catch (e) { } } else { @@ -5788,7 +5788,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { if (typeof domain.twofactorcookiedurationdays == 'number') { twoFactorCookieDays = domain.twofactorcookiedurationdays; } // Figure out if email 2FA is allowed - var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (parent.mailserver != null) && (user.otpekey != null)); + var email2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.email2factor != false)) && (domain.mailserver != null) && (user.otpekey != null)); var sms2fa = (((typeof domain.passwordrequirements != 'object') || (domain.passwordrequirements.sms2factor != false)) && (parent.smsserver != null) && (user.phone != null)); if (s.length != 3) { try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, sms2fa: sms2fa, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { } @@ -5800,7 +5800,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates) { user.otpekey = { k: obj.common.zeroPad(getRandomEightDigitInteger(), 8), d: Date.now() }; obj.db.SetUser(user); parent.debug('web', 'Sending 2FA email to: ' + user.email); - parent.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key); + domain.mailserver.sendAccountLoginMail(domain, user.email, user.otpekey.k, obj.getLanguageCodes(req), req.query.key); // Ask for a login token & confirm email was sent try { ws.send(JSON.stringify({ action: 'close', cause: 'noauth', msg: 'tokenrequired', email2fa: email2fa, email2fasent: true, twoFactorCookieDays: twoFactorCookieDays })); ws.close(); } catch (e) { } } else if ((s[2] == '**sms**') && (sms2fa == true)) {