From ddd34f56f5eee18120cad6064f7d6cd609a4bc1f Mon Sep 17 00:00:00 2001 From: Ylian Saint-Hilaire Date: Wed, 16 Dec 2020 14:55:22 -0800 Subject: [PATCH] Added native support for SendGrid. --- meshcentral-config-schema.json | 16 ++++-- meshcentral.js | 12 ++++- meshmail.js | 89 ++++++++++++++++++++++------------ sample-config-advanced.json | 4 ++ 4 files changed, 84 insertions(+), 37 deletions(-) diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 419e23d0..2810a78c 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -545,14 +545,24 @@ }, "required": [ "serverId", "servers" ] }, + "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." } + }, + "required": [ "from", "apikey" ] + }, "smtp": { - "title" : "Email server", - "description": "Connects MeshCentral to a email server, allows MeshCentral to send email messages for 2FA or user notification.", + "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" }, + "from": { "type": "string", "format": "email", "description": "Email address used in the messages from field." }, "tls": { "type": "boolean" }, "tlscertcheck": { "type": "boolean" }, "tlsstrict": { "type": "boolean" }, diff --git a/meshcentral.js b/meshcentral.js index fe3d86b1..762844cd 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -678,7 +678,7 @@ function CreateMeshCentralServer(config, args) { } // Check top level configuration for any unreconized values - if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".'); } } } + if (config) { for (var i in config) { if ((typeof i == 'string') && (i.length > 0) && (i[0] != '_') && (['settings', 'domaindefaults', 'domains', 'configfiles', 'smtp', 'letsencrypt', 'peers', 'sms', 'sendgrid', '$schema'].indexOf(i) == -1)) { addServerWarning('Unrecognized configuration option \"' + i + '\".'); } } } if (typeof obj.args.userallowedip == 'string') { if (obj.args.userallowedip == '') { config.settings.userallowedip = obj.args.userallowedip = null; } else { config.settings.userallowedip = obj.args.userallowedip = obj.args.userallowedip.split(','); } } if (typeof obj.args.userblockedip == 'string') { if (obj.args.userblockedip == '') { config.settings.userblockedip = obj.args.userblockedip = null; } else { config.settings.userblockedip = obj.args.userblockedip = obj.args.userblockedip.split(','); } } @@ -1440,7 +1440,13 @@ function CreateMeshCentralServer(config, args) { } // Setup email server - if ((obj.config.smtp != null) && (obj.config.smtp.host != null) && (obj.config.smtp.from != null)) { + if (obj.config.sendgrid != null) { + // Sendgrid server + obj.mailserver = require('./meshmail.js').CreateMeshMail(obj); + obj.mailserver.verify(); + if (obj.args.lanonly == true) { addServerWarning("SendGrid server has limited use in LAN mode."); } + } else if ((obj.config.smtp != null) && (obj.config.smtp.host != null) && (obj.config.smtp.from != null)) { + // SMTP server obj.mailserver = require('./meshmail.js').CreateMeshMail(obj); obj.mailserver.verify(); if (obj.args.lanonly == true) { addServerWarning("SMTP server has limited use in LAN mode."); } @@ -2751,6 +2757,7 @@ function InstallModules(modules, func) { var moduleInfo = moduleNameAndVersion.split('@', 2); var moduleName = moduleInfo[0]; var moduleVersion = moduleInfo[1]; + if (moduleName == '') { moduleName = moduleNameAndVersion; moduleVersion = undefined; } // If the module name starts with @, don't use @ as a version seperator. try { // Does the module need a specific version? if (moduleVersion) { @@ -2888,6 +2895,7 @@ function mainStart() { 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 (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 93059326..fe5433c6 100644 --- a/meshmail.js +++ b/meshmail.js @@ -26,19 +26,25 @@ module.exports.CreateMeshMail = function (parent) { obj.mailCookieEncryptionKey = null; //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. - const nodemailer = require('nodemailer'); 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; } - // Setup mail server - 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 }; } - obj.smtpServer = nodemailer.createTransport(options); + if (parent.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) { + // 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 }; } + obj.smtpServer = nodemailer.createTransport(options); + } // Get the correct mail template object function getTemplate(name, domain, lang) { @@ -143,7 +149,11 @@ module.exports.CreateMeshMail = function (parent) { // Send a generic email obj.sendMail = function (to, subject, text, html) { - obj.pendingMails.push({ to: to, from: parent.config.smtp.from, subject: subject, text: text, html: 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 }); + } sendNextMail(); }; @@ -300,36 +310,51 @@ module.exports.CreateMeshMail = function (parent) { var mailToSend = obj.pendingMails[0]; obj.sendingMail = true; - parent.debug('email', 'SMTP sending mail to ' + mailToSend.to + '.'); - obj.smtpServer.sendMail(mailToSend, function (err, info) { - parent.debug('email', 'SMTP response: ' + JSON.stringify(err) + ', ' + JSON.stringify(info)); - obj.sendingMail = false; - if (err == null) { - // Send the next mail - obj.pendingMails.shift(); - obj.retry = 0; - sendNextMail(); - } else { - obj.retry++; - parent.debug('email', 'SMTP server failed (Retry:' + obj.retry + '): ' + JSON.stringify(err)); - console.log('SMTP server failed (Retry:' + obj.retry + '/3): ' + JSON.stringify(err)); - // Wait and try again - if (obj.retry < 3) { - setTimeout(sendNextMail, 10000); - } else { - // Failed, send the next mail - parent.debug('email', 'SMTP server failed (Skipping): ' + JSON.stringify(err)); - console.log('SMTP server failed (Skipping): ' + JSON.stringify(err)); + + if (obj.sendGridServer != null) { + // SendGrid send + parent.debug('email', 'SendGrid sending mail to ' + mailToSend.to + '.'); + obj.sendGridServer + .send(mailToSend) + .then(function () { + parent.debug('email', 'SendGrid sending success.'); + }, function (error) { + parent.debug('email', 'SendGrid sending error: ' + JSON.stringify(error)); + }); + } else if (obj.smtpServer != null) { + // SMTP send + parent.debug('email', 'SMTP sending mail to ' + mailToSend.to + '.'); + obj.smtpServer.sendMail(mailToSend, function (err, info) { + parent.debug('email', 'SMTP response: ' + JSON.stringify(err) + ', ' + JSON.stringify(info)); + obj.sendingMail = false; + if (err == null) { + // Send the next mail obj.pendingMails.shift(); obj.retry = 0; sendNextMail(); + } else { + obj.retry++; + parent.debug('email', 'SMTP server failed (Retry:' + obj.retry + '): ' + JSON.stringify(err)); + console.log('SMTP server failed (Retry:' + obj.retry + '/3): ' + JSON.stringify(err)); + // Wait and try again + if (obj.retry < 3) { + setTimeout(sendNextMail, 10000); + } else { + // Failed, send the next mail + parent.debug('email', 'SMTP server failed (Skipping): ' + JSON.stringify(err)); + console.log('SMTP server failed (Skipping): ' + JSON.stringify(err)); + obj.pendingMails.shift(); + obj.retry = 0; + sendNextMail(); + } } - } - }); + }); + } } // Send out the next mail in the pending list obj.verify = function () { + 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.'); diff --git a/sample-config-advanced.json b/sample-config-advanced.json index b3eea7d4..2d66430d 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -385,6 +385,10 @@ "__tlsstrict__": "When set to true, TLS cypher setup is more limited, SSLv2 and SSLv3 are not allowed.", "_tlsstrict": true }, + "_sendgrid": { + "from": "myemail@myserver.com", + "apikey": "***********" + }, "_sms": { "provider": "twilio", "sid": "ACxxxxxxxxx",