2017-12-12 19:04:54 -05:00
/ * *
2018-01-04 15:15:21 -05:00
* @ description MeshCentral e - mail server communication modules
2017-12-12 19:04:54 -05:00
* @ author Ylian Saint - Hilaire
2020-01-02 21:30:12 -05:00
* @ copyright Intel Corporation 2018 - 2020
2018-01-04 15:15:21 -05:00
* @ license Apache - 2.0
2017-12-12 19:04:54 -05:00
* @ version v0 . 0.1
* /
2018-08-29 20:40:30 -04:00
/*xjslint node: true */
/*xjslint plusplus: true */
/*xjslint maxlen: 256 */
/*jshint node: true */
/*jshint strict: false */
/*jshint esversion: 6 */
"use strict" ;
2018-08-27 15:24:15 -04:00
2019-06-13 19:39:21 -04:00
// TODO: Add NTML support with "nodemailer-ntlm-auth" https://github.com/nodemailer/nodemailer-ntlm-auth
2017-12-12 19:04:54 -05:00
// Construct a MeshAgent object, called upon connection
2018-09-05 20:40:00 -04:00
module . exports . CreateMeshMail = function ( parent ) {
2017-12-12 19:04:54 -05:00
var obj = { } ;
obj . pendingMails = [ ] ;
obj . parent = parent ;
obj . retry = 0 ;
obj . sendingMail = false ;
2017-12-13 17:52:57 -05:00
obj . mailCookieEncryptionKey = null ;
2020-04-04 22:29:20 -04:00
//obj.mailTemplates = {};
2019-08-26 18:51:50 -04:00
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.
2017-12-12 19:04:54 -05:00
const nodemailer = require ( 'nodemailer' ) ;
2018-01-23 17:15:59 -05:00
function EscapeHtml ( x ) { if ( typeof x == "string" ) return x . replace ( /&/g , '&' ) . replace ( />/g , '>' ) . replace ( /</g , '<' ) . replace ( /"/g , '"' ) . replace ( /'/g , ''' ) ; if ( typeof x == "boolean" ) return x ; if ( typeof x == "number" ) return x ; }
2018-08-29 20:40:30 -04:00
//function EscapeHtmlBreaks(x) { if (typeof x == "string") return x.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"').replace(/'/g, ''').replace(/\r/g, '<br />').replace(/\n/g, '').replace(/\t/g, ' '); if (typeof x == "boolean") return x; if (typeof x == "number") return x; }
2018-01-23 17:15:59 -05:00
2017-12-12 19:04:54 -05:00
// Setup mail server
2019-09-16 13:49:35 -04:00
var options = { host : parent . config . smtp . host , secure : ( parent . config . smtp . tls == true ) , tls : { } } ;
2019-09-14 20:54:55 -04:00
//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 } };
2017-12-12 19:04:54 -05:00
if ( parent . config . smtp . port != null ) { options . port = parent . config . smtp . port ; }
2019-09-16 13:49:35 -04:00
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 ; }
2017-12-12 21:23:26 -05:00
if ( ( parent . config . smtp . user != null ) && ( parent . config . smtp . pass != null ) ) { options . auth = { user : parent . config . smtp . user , pass : parent . config . smtp . pass } ; }
2017-12-12 19:04:54 -05:00
obj . smtpServer = nodemailer . createTransport ( options ) ;
2020-04-04 22:29:20 -04:00
// Get the correct mail template object
function getTemplate ( name , domain , lang ) {
if ( Array . isArray ( lang ) ) { lang = lang [ 0 ] ; } // TODO: For now, we only use the first language given.
var r = { } , emailsPath = null ;
if ( ( domain != null ) && ( domain . webemailspath != null ) ) { emailsPath = domain . webemailspath ; }
else if ( obj . parent . webEmailsOverridePath != null ) { emailsPath = obj . parent . webEmailsOverridePath ; }
else if ( obj . parent . webEmailsPath != null ) { emailsPath = obj . parent . webEmailsPath ; }
if ( ( emailsPath == null ) || ( obj . parent . fs . existsSync ( emailsPath ) == false ) ) { return null }
// Get the non-english email if needed
var htmlfile = null , txtfile = null ;
if ( ( lang != null ) && ( lang != 'en' ) ) {
var translationsPath = obj . parent . path . join ( emailsPath , 'translations' ) ;
var translationsPathHtml = obj . parent . path . join ( emailsPath , 'translations' , name + '_' + lang + '.html' ) ;
var translationsPathTxt = obj . parent . path . join ( emailsPath , 'translations' , name + '_' + lang + '.txt' ) ;
if ( obj . parent . fs . existsSync ( translationsPath ) && obj . parent . fs . existsSync ( translationsPathHtml ) && obj . parent . fs . existsSync ( translationsPathTxt ) ) {
htmlfile = obj . parent . fs . readFileSync ( translationsPathHtml ) . toString ( ) ;
txtfile = obj . parent . fs . readFileSync ( translationsPathTxt ) . toString ( ) ;
2018-09-06 18:58:34 -04:00
}
}
2020-04-04 22:29:20 -04:00
// Get the english email
if ( htmlfile == null ) {
var pathHtml = obj . parent . path . join ( emailsPath , name + '.html' ) ;
var pathTxt = obj . parent . path . join ( emailsPath , name + '.txt' ) ;
if ( obj . parent . fs . existsSync ( pathHtml ) && obj . parent . fs . existsSync ( pathTxt ) ) {
htmlfile = obj . parent . fs . readFileSync ( pathHtml ) . toString ( ) ;
txtfile = obj . parent . fs . readFileSync ( pathTxt ) . toString ( ) ;
}
}
// No email templates
if ( ( htmlfile == null ) || ( txtfile == null ) ) { return null ; }
// Decode the HTML file
htmlfile = htmlfile . split ( '<html>' ) . join ( '' ) . split ( '</html>' ) . join ( '' ) . split ( '<head>' ) . join ( '' ) . split ( '</head>' ) . join ( '' ) . split ( '<body>' ) . join ( '' ) . split ( '</body>' ) . join ( '' ) . split ( ' notrans="1"' ) . join ( '' ) ;
var lines = htmlfile . split ( '\r\n' ) . join ( '\n' ) . split ( '\n' ) ;
r . htmlSubject = lines . shift ( ) ;
if ( r . htmlSubject . startsWith ( '<div>' ) ) { r . htmlSubject = r . htmlSubject . substring ( 5 ) ; }
if ( r . htmlSubject . endsWith ( '</div>' ) ) { r . htmlSubject = r . htmlSubject . substring ( 0 , r . htmlSubject . length - 6 ) ; }
r . html = lines . join ( '\r\n' ) ;
// Decode the TXT file
lines = txtfile . split ( '\r\n' ) . join ( '\n' ) . split ( '\n' ) ;
r . txtSubject = lines . shift ( ) ;
var txtbody = [ ] ;
for ( var i in lines ) { var line = lines [ i ] ; if ( ( line . length > 0 ) && ( line [ 0 ] == '~' ) ) { txtbody . push ( line . substring ( 1 ) ) ; } else { txtbody . push ( line ) ; } }
r . txt = txtbody . join ( '\r\n' ) ;
2018-09-06 18:58:34 -04:00
return r ;
}
// Get the string between two markers
function getStrBetween ( str , start , end ) {
var si = str . indexOf ( start ) , ei = str . indexOf ( end ) ;
if ( ( si == - 1 ) || ( ei == - 1 ) || ( si > ei ) ) return null ;
return str . substring ( si + start . length , ei ) ;
}
// Remove the string between two markers
function removeStrBetween ( str , start , end ) {
var si = str . indexOf ( start ) , ei = str . indexOf ( end ) ;
if ( ( si == - 1 ) || ( ei == - 1 ) || ( si > ei ) ) return str ;
return str . substring ( 0 , si ) + str . substring ( ei + end . length ) ;
}
2020-04-04 22:29:20 -04:00
// Keep or remove all lines between two lines with markers
2018-09-06 18:58:34 -04:00
function strZone ( str , marker , keep ) {
2020-04-04 22:29:20 -04:00
var lines = str . split ( '\r\n' ) , linesEx = [ ] , removing = false ;
const startMarker = '<area-' + marker + '>' , endMarker = '</area-' + marker + '>' ;
for ( var i in lines ) {
var line = lines [ i ] ;
if ( removing ) {
if ( line . indexOf ( endMarker ) >= 0 ) { removing = false ; } else { if ( keep ) { linesEx . push ( line ) ; } }
} else {
if ( line . indexOf ( startMarker ) >= 0 ) { removing = true ; } else { linesEx . push ( line ) ; }
}
}
return linesEx . join ( '\r\n' ) ;
2018-09-06 18:58:34 -04:00
}
2017-12-12 19:04:54 -05:00
// Perform all e-mail substitution
2018-09-06 18:58:34 -04:00
function mailReplacements ( text , domain , options ) {
2019-03-10 14:00:14 -04:00
var httpsport = ( typeof obj . parent . args . aliasport == 'number' ) ? obj . parent . args . aliasport : obj . parent . args . port ;
2018-01-04 15:15:21 -05:00
if ( domain . dns == null ) {
// Default domain or subdomain of the default.
2019-03-10 14:00:14 -04:00
options . serverurl = 'http' + ( ( obj . parent . args . notls == null ) ? 's' : '' ) + '://' + obj . parent . certificates . CommonName + ':' + httpsport + domain . url ;
2018-01-04 15:15:21 -05:00
} else {
// Domain with a DNS name.
2019-03-10 14:00:14 -04:00
options . serverurl = 'http' + ( ( obj . parent . args . notls == null ) ? 's' : '' ) + '://' + domain . dns + ':' + httpsport + domain . url ;
2018-01-04 15:15:21 -05:00
}
2018-09-06 18:58:34 -04:00
if ( options . serverurl . endsWith ( '/' ) ) { options . serverurl = options . serverurl . substring ( 0 , options . serverurl . length - 1 ) ; } // Remove the ending / if present
for ( var i in options ) {
2020-04-04 22:29:20 -04:00
text = strZone ( text , i . toLowerCase ( ) , options [ i ] ) ; // Adjust this text area
2018-09-06 18:58:34 -04:00
text = text . split ( '[[[' + i . toUpperCase ( ) + ']]]' ) . join ( options [ i ] ) ; // Replace this value
2018-01-23 17:15:59 -05:00
}
2018-09-06 18:58:34 -04:00
return text ;
2017-12-12 19:04:54 -05:00
}
2020-04-04 22:29:20 -04:00
// Send a generic email
2017-12-12 19:04:54 -05:00
obj . sendMail = function ( to , subject , text , html ) {
obj . pendingMails . push ( { to : to , from : parent . config . smtp . from , subject : subject , text : text , html : html } ) ;
sendNextMail ( ) ;
2018-08-29 20:40:30 -04:00
} ;
2017-12-12 19:04:54 -05:00
2020-03-13 23:39:21 -04:00
// Send account login mail / 2 factor token
2020-04-04 22:29:20 -04:00
obj . sendAccountLoginMail = function ( domain , email , token , language ) {
2020-03-22 02:13:53 -04:00
obj . checkEmail ( email , function ( checked ) {
if ( checked ) {
parent . debug ( 'email' , "Sending login token to " + email ) ;
2020-03-13 23:39:21 -04:00
2020-04-04 22:29:20 -04:00
var template = getTemplate ( 'account-login' , domain , language ) ;
2020-03-22 02:13:53 -04:00
if ( ( template == null ) || ( template . htmlSubject == null ) || ( template . txtSubject == null ) || ( parent . certificates == null ) || ( parent . certificates . CommonName == null ) || ( parent . certificates . CommonName . indexOf ( '.' ) == - 1 ) ) return ; // If the server name is not set, invitation not possible.
2020-03-13 23:39:21 -04:00
2020-03-22 02:13:53 -04:00
// Set all the options.
var options = { email : email , servername : domain . title ? domain . title : 'MeshCentral' , token : token } ;
// Send the email
obj . pendingMails . push ( { to : email , from : parent . config . smtp . from , subject : mailReplacements ( template . htmlSubject , domain , options ) , text : mailReplacements ( template . txt , domain , options ) , html : mailReplacements ( template . html , domain , options ) } ) ;
sendNextMail ( ) ;
}
} ) ;
2020-03-13 23:39:21 -04:00
} ;
2019-07-22 19:00:43 -04:00
// Send account invitation mail
2020-04-04 22:29:20 -04:00
obj . sendAccountInviteMail = function ( domain , username , accountname , email , password , language ) {
2020-03-22 02:13:53 -04:00
obj . checkEmail ( email , function ( checked ) {
if ( checked ) {
parent . debug ( 'email' , "Sending account invitation to " + email ) ;
2019-07-22 19:00:43 -04:00
2020-04-04 22:29:20 -04:00
var template = getTemplate ( 'account-invite' , domain , language ) ;
2020-03-22 02:13:53 -04:00
if ( ( template == null ) || ( template . htmlSubject == null ) || ( template . txtSubject == null ) || ( parent . certificates == null ) || ( parent . certificates . CommonName == null ) || ( parent . certificates . CommonName . indexOf ( '.' ) == - 1 ) ) return ; // If the server name is not set, invitation not possible.
2019-07-22 19:00:43 -04:00
2020-03-22 02:13:53 -04:00
// Set all the options.
var options = { username : username , accountname : accountname , email : email , servername : domain . title ? domain . title : 'MeshCentral' , password : password } ;
// Send the email
obj . pendingMails . push ( { to : email , from : parent . config . smtp . from , subject : mailReplacements ( template . htmlSubject , domain , options ) , text : mailReplacements ( template . txt , domain , options ) , html : mailReplacements ( template . html , domain , options ) } ) ;
sendNextMail ( ) ;
}
} ) ;
2019-07-22 19:00:43 -04:00
} ;
2017-12-12 19:04:54 -05:00
// Send account check mail
2020-04-04 22:29:20 -04:00
obj . sendAccountCheckMail = function ( domain , username , email , language ) {
2020-03-22 02:13:53 -04:00
obj . checkEmail ( email , function ( checked ) {
if ( checked ) {
parent . debug ( 'email' , "Sending email verification to " + email ) ;
2018-09-06 18:58:34 -04:00
2020-04-04 22:29:20 -04:00
var template = getTemplate ( 'account-check' , domain , language ) ;
2020-03-22 02:13:53 -04:00
if ( ( template == null ) || ( template . htmlSubject == null ) || ( template . txtSubject == null ) || ( parent . certificates == null ) || ( parent . certificates . CommonName == null ) || ( parent . certificates . CommonName . indexOf ( '.' ) == - 1 ) ) return ; // If the server name is not set, no reset possible.
2018-09-06 18:58:34 -04:00
2020-03-22 02:13:53 -04:00
// Set all the options.
var options = { username : username , email : email , servername : domain . title ? domain . title : 'MeshCentral' } ;
options . cookie = obj . parent . encodeCookie ( { u : domain . id + '/' + username . toLowerCase ( ) , e : email , a : 1 } , obj . mailCookieEncryptionKey ) ;
// Send the email
obj . pendingMails . push ( { to : email , from : parent . config . smtp . from , subject : mailReplacements ( template . htmlSubject , domain , options ) , text : mailReplacements ( template . txt , domain , options ) , html : mailReplacements ( template . html , domain , options ) } ) ;
sendNextMail ( ) ;
}
} ) ;
2018-08-29 20:40:30 -04:00
} ;
2017-12-12 19:04:54 -05:00
// Send account reset mail
2020-04-04 22:29:20 -04:00
obj . sendAccountResetMail = function ( domain , username , email , language ) {
2020-03-22 02:13:53 -04:00
obj . checkEmail ( email , function ( checked ) {
if ( checked ) {
parent . debug ( 'email' , "Sending account password reset to " + email ) ;
2018-09-06 18:58:34 -04:00
2020-04-04 22:29:20 -04:00
var template = getTemplate ( 'account-reset' , domain , language ) ;
2020-03-22 02:13:53 -04:00
if ( ( template == null ) || ( template . htmlSubject == null ) || ( template . txtSubject == null ) || ( parent . certificates == null ) || ( parent . certificates . CommonName == null ) || ( parent . certificates . CommonName . indexOf ( '.' ) == - 1 ) ) return ; // If the server name is not set, don't validate the email address.
2018-09-06 18:58:34 -04:00
2020-03-22 02:13:53 -04:00
// Set all the options.
var options = { username : username , email : email , servername : domain . title ? domain . title : 'MeshCentral' } ;
options . cookie = obj . parent . encodeCookie ( { u : domain . id + '/' + username , e : email , a : 2 } , obj . mailCookieEncryptionKey ) ;
// Send the email
obj . pendingMails . push ( { to : email , from : parent . config . smtp . from , subject : mailReplacements ( template . htmlSubject , domain , options ) , text : mailReplacements ( template . txt , domain , options ) , html : mailReplacements ( template . html , domain , options ) } ) ;
sendNextMail ( ) ;
}
} ) ;
2018-08-29 20:40:30 -04:00
} ;
2018-01-23 17:15:59 -05:00
// Send agent invite mail
2020-04-04 22:29:20 -04:00
obj . sendAgentInviteMail = function ( domain , username , email , meshid , name , os , msg , flags , expirehours , language ) {
2020-03-22 02:13:53 -04:00
obj . checkEmail ( email , function ( checked ) {
if ( checked ) {
parent . debug ( 'email' , "Sending agent install invitation to " + email ) ;
2020-04-04 22:29:20 -04:00
var template = getTemplate ( 'mesh-invite' , domain , language ) ;
if ( ( template == null ) || ( template . htmlSubject == null ) || ( template . txtSubject == null ) || ( parent . certificates == null ) || ( parent . certificates . CommonName == null ) || ( parent . certificates . CommonName . indexOf ( '.' ) == - 1 ) ) {
parent . debug ( 'email' , "Failed to get email template for " + email ) ;
return ; // If the server name is not set, don't validate the email address.
}
2020-03-22 02:13:53 -04:00
// Set all the template replacement options and generate the final email text (both in txt and html formats).
var options = { username : username , name : name , email : email , installflags : flags , msg : msg , meshid : meshid , meshidhex : meshid . split ( '/' ) [ 2 ] , servername : domain . title ? domain . title : 'MeshCentral' } ;
options . windows = ( ( os == 0 ) || ( os == 1 ) ) ? 1 : 0 ;
options . linux = ( ( os == 0 ) || ( os == 2 ) ) ? 1 : 0 ;
options . osx = ( ( os == 0 ) || ( os == 3 ) ) ? 1 : 0 ;
options . link = ( os == 4 ) ? 1 : 0 ;
options . linkurl = createInviteLink ( domain , meshid , flags , expirehours ) ;
// Send the email
obj . pendingMails . push ( { to : email , from : parent . config . smtp . from , subject : mailReplacements ( template . htmlSubject , domain , options ) , text : mailReplacements ( template . txt , domain , options ) , html : mailReplacements ( template . html , domain , options ) } ) ;
sendNextMail ( ) ;
}
} ) ;
2018-08-29 20:40:30 -04:00
} ;
2017-12-12 19:04:54 -05:00
// Send out the next mail in the pending list
function sendNextMail ( ) {
if ( ( obj . sendingMail == true ) || ( obj . pendingMails . length == 0 ) ) { return ; }
var mailToSend = obj . pendingMails [ 0 ] ;
obj . sendingMail = true ;
//console.log('SMTP sending mail to ' + mailToSend.to + '.');
obj . smtpServer . sendMail ( mailToSend , function ( err , info ) {
//console.log(JSON.stringify(err), JSON.stringify(info));
obj . sendingMail = false ;
if ( err == null ) {
obj . pendingMails . shift ( ) ;
obj . retry = 0 ;
sendNextMail ( ) ; // Send the next mail
} else {
obj . retry ++ ;
2020-03-22 02:13:53 -04:00
parent . debug ( 'email' , 'SMTP server failed: ' + JSON . stringify ( err ) ) ;
2017-12-13 17:52:57 -05:00
console . log ( 'SMTP server failed: ' + JSON . stringify ( err ) ) ;
2017-12-12 21:23:26 -05:00
if ( obj . retry < 6 ) { setTimeout ( sendNextMail , 60000 ) ; } // Wait and try again
}
} ) ;
}
// Send out the next mail in the pending list
2018-08-29 20:40:30 -04:00
obj . verify = function ( ) {
2017-12-12 21:23:26 -05:00
obj . smtpServer . verify ( function ( err , info ) {
if ( err == null ) {
console . log ( 'SMTP mail server ' + parent . config . smtp . host + ' working as expected.' ) ;
} else {
2019-12-29 21:10:58 -05:00
// 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 ] ; } }
2020-03-22 02:13:53 -04:00
parent . debug ( 'email' , 'SMTP mail server ' + parent . config . smtp . host + ' failed: ' + JSON . stringify ( err2 ) ) ;
2019-12-29 21:10:58 -05:00
console . log ( 'SMTP mail server ' + parent . config . smtp . host + ' failed: ' + JSON . stringify ( err2 ) ) ;
2017-12-12 19:04:54 -05:00
}
} ) ;
2018-08-29 20:40:30 -04:00
} ;
2017-12-12 19:04:54 -05:00
2017-12-13 17:52:57 -05:00
// Load the cookie encryption key from the database
obj . parent . db . Get ( 'MailCookieEncryptionKey' , function ( err , docs ) {
2017-12-14 17:57:52 -05:00
if ( ( docs . length > 0 ) && ( docs [ 0 ] . key != null ) && ( obj . parent . mailtokengen == null ) ) {
2017-12-13 17:52:57 -05:00
// Key is present, use it.
obj . mailCookieEncryptionKey = Buffer . from ( docs [ 0 ] . key , 'hex' ) ;
} else {
// Key is not present, generate one.
obj . mailCookieEncryptionKey = obj . parent . generateCookieKey ( ) ;
obj . parent . db . Set ( { _id : 'MailCookieEncryptionKey' , key : obj . mailCookieEncryptionKey . toString ( 'hex' ) , time : Date . now ( ) } ) ;
}
} ) ;
2019-06-12 13:23:26 -04:00
// Create a agent invitation link
function createInviteLink ( domain , meshid , flags , expirehours ) {
return '/agentinvite?c=' + parent . encodeCookie ( { a : 4 , mid : meshid , f : flags , expire : expirehours * 60 } , parent . invitationLinkEncryptionKey ) ;
}
2020-03-22 02:13:53 -04:00
// Check the email domain DNS MX record.
obj . approvedEmailDomains = { } ;
obj . checkEmail = function ( email , func ) {
var emailSplit = email . split ( '@' ) ;
if ( emailSplit . length != 2 ) { func ( false ) ; return ; }
if ( obj . approvedEmailDomains [ emailSplit [ 1 ] ] === true ) { func ( true ) ; return ; }
require ( 'dns' ) . resolveMx ( emailSplit [ 1 ] , function ( err , addresses ) {
parent . debug ( 'email' , "checkEmail: " + email + ", " + ( err == null ) ) ;
if ( err == null ) { obj . approvedEmailDomains [ emailSplit [ 1 ] ] = true ; }
func ( err == null ) ;
} ) ;
}
2018-08-29 20:40:30 -04:00
return obj ;
} ;