/* eslint-disable indent */ const { domainToUnicode } = require("url"); /** * POP3 protocol - RFC1939 - https://www.ietf.org/rfc/rfc1939.txt * * Dependencies: * * node-pop3 - https://www.npmjs.com/package/node-pop3 * * nodemailer - https://www.npmjs.com/package/nodemailer * * imap - https://www.npmjs.com/package/imap * * mailparser - https://www.npmjs.com/package/mailparser */ module.exports = function(RED) { "use strict"; var util = require("util"); var Imap = require('imap'); var Pop3Command = require("node-pop3"); var nodemailer = require("nodemailer"); var simpleParser = require("mailparser").simpleParser; var SMTPServer = require("smtp-server").SMTPServer; //var microMTA = require("micromta").microMTA; var fs = require('fs'); if (parseInt(process.version.split("v")[1].split(".")[0]) < 8) { throw "Error : Requires nodejs version >= 8."; } function EmailNode(n) { RED.nodes.createNode(this,n); this.topic = n.topic; this.name = n.name; this.outserver = n.server; this.outport = n.port; this.secure = n.secure; this.tls = true; this.authtype = n.authtype || "BASIC"; if (this.authtype !== "BASIC") { this.inputs = 1; this.repeat = 0; } if (this.credentials && this.credentials.hasOwnProperty("userid")) { this.userid = this.credentials.userid; } else if (this.authtype !== "NONE") { this.error(RED._("email.errors.nouserid")); } if (this.authtype === "BASIC" ) { if (this.credentials && this.credentials.hasOwnProperty("password")) { this.password = this.credentials.password; } else { this.error(RED._("email.errors.nopassword")); } } else if (this.authtype === "XOAUTH2") { this.saslformat = n.saslformat; if (n.token !== "") { this.token = n.token; } else { this.error(RED._("email.errors.notoken")); } } if (n.tls === false) { this.tls = false; } var node = this; var smtpOptions = { host: node.outserver, port: node.outport, secure: node.secure, tls: {rejectUnauthorized: node.tls} } if (node.authtype === "BASIC" ) { smtpOptions.auth = { user: node.userid, pass: node.password }; } else if (node.authtype === "XOAUTH2") { var value = RED.util.getMessageProperty(msg,node.token); if (value !== undefined) { if (node.saslformat) { //Make base64 string for access - compatible with outlook365 and gmail saslxoauth2 = Buffer.from("user="+node.userid+"\x01auth=Bearer "+value+"\x01\x01").toString('base64'); } else { saslxoauth2 = value; } } smtpOptions.auth = { type: "OAuth2", user: node.userid, accessToken: saslxoauth2 }; } var smtpTransport = nodemailer.createTransport(smtpOptions); this.on("input", function(msg, send, done) { if (msg.hasOwnProperty("payload")) { send = send || function() { node.send.apply(node,arguments) }; if (smtpTransport) { node.status({fill:"blue",shape:"dot",text:"email.status.sending"}); if (msg.to && node.name && (msg.to !== node.name)) { node.warn(RED._("node-red:common.errors.nooverride")); } var sendopts = { from: ((msg.from) ? msg.from : node.userid) }; // sender address sendopts.to = node.name || msg.to; // comma separated list of addressees if (node.name === "") { sendopts.cc = msg.cc; sendopts.bcc = msg.bcc; sendopts.inReplyTo = msg.inReplyTo; sendopts.replyTo = msg.replyTo; sendopts.references = msg.references; sendopts.headers = msg.headers; sendopts.priority = msg.priority; } if (msg.hasOwnProperty("topic") && msg.topic === '') { sendopts.subject = ""; } else { sendopts.subject = msg.topic || msg.title || "Message from Node-RED"; } // subject line if (msg.hasOwnProperty("header") && msg.header.hasOwnProperty("message-id")) { sendopts.inReplyTo = msg.header["message-id"]; sendopts.subject = "Re: " + sendopts.subject; } if (msg.hasOwnProperty("envelope")) { sendopts.envelope = msg.envelope; } if (Buffer.isBuffer(msg.payload)) { // if it's a buffer in the payload then auto create an attachment instead if (!msg.filename) { var fe = "bin"; if ((msg.payload[0] === 0xFF)&&(msg.payload[1] === 0xD8)) { fe = "jpg"; } if ((msg.payload[0] === 0x47)&&(msg.payload[1] === 0x49)) { fe = "gif"; } //46 if ((msg.payload[0] === 0x42)&&(msg.payload[1] === 0x4D)) { fe = "bmp"; } if ((msg.payload[0] === 0x89)&&(msg.payload[1] === 0x50)) { fe = "png"; } //4E msg.filename = "attachment."+fe; } var fname = msg.filename.replace(/^.*[\\\/]/, '') || "attachment.bin"; sendopts.attachments = [ { content:msg.payload, filename:fname } ]; if (msg.hasOwnProperty("headers") && msg.headers.hasOwnProperty("content-type")) { sendopts.attachments[0].contentType = msg.headers["content-type"]; } // Create some body text.. if (msg.hasOwnProperty("description")) { sendopts.text = msg.description; } else { sendopts.text = RED._("email.default-message",{filename:fname}); } } else { var payload = RED.util.ensureString(msg.payload); sendopts.text = payload; // plaintext body if (/<[a-z][\s\S]*>/i.test(payload)) { sendopts.html = payload; // html body if (msg.hasOwnProperty("plaintext")) { var plaintext = RED.util.ensureString(msg.plaintext); sendopts.text = plaintext; // plaintext body - specific plaintext version } } if (msg.attachments) { if (!Array.isArray(msg.attachments)) { sendopts.attachments = [ msg.attachments ]; } else { sendopts.attachments = msg.attachments; } for (var a=0; a < sendopts.attachments.length; a++) { if (sendopts.attachments[a].hasOwnProperty("content")) { if (typeof sendopts.attachments[a].content !== "string" && !Buffer.isBuffer(sendopts.attachments[a].content)) { node.error(RED._("email.errors.invalidattachment"),msg); node.status({fill:"red",shape:"ring",text:"email.status.sendfail"}); return; } } } } } smtpTransport.sendMail(sendopts, function(error, info) { if (error) { node.error(error,msg); node.status({fill:"red",shape:"ring",text:"email.status.sendfail",response:error.response,msg:{to:msg.to,topic:msg.topic,id:msg._msgid}}); } else { node.log(RED._("email.status.messagesent",{response:info.response})); node.status({text:"",response:info.response,msg:{to:msg.to,topic:msg.topic,id:msg._msgid}}); if (done) { done(); } } }); } else { node.warn(RED._("email.errors.nosmtptransport")); } } else { node.warn(RED._("email.errors.nopayload")); } }); } RED.nodes.registerType("e-mail",EmailNode,{ credentials: { userid: {type:"text"}, password: {type: "password"}, global: { type:"boolean"} } }); // // EmailInNode // // Setup the EmailInNode function EmailInNode(n) { var imap; var pop3; RED.nodes.createNode(this,n); this.name = n.name; this.inputs = n.inputs; this.repeat = n.repeat * 1000 || 300000; if (this.repeat > 2147483647) { // setTimeout/Interval has a limit of 2**31-1 Milliseconds this.repeat = 2147483647; this.error(RED._("email.errors.refreshtoolarge")); } if (this.repeat < 1500) { this.repeat = 1500; } if (this.inputs === 1) { this.repeat = 0; } this.inserver = n.server || "imap.gmail.com"; this.inport = n.port || "993"; this.box = n.box || "INBOX"; this.useSSL= n.useSSL; this.autotls= n.autotls; this.protocol = n.protocol || "IMAP"; this.disposition = n.disposition || "None"; // "None", "Delete", "Read" this.criteria = n.criteria || "UNSEEN"; // "ALL", "ANSWERED", "FLAGGED", "SEEN", "UNANSWERED", "UNFLAGGED", "UNSEEN" this.authtype = n.authtype || "BASIC"; if (this.authtype !== "BASIC") { this.inputs = 1; this.repeat = 0; } if (this.credentials && this.credentials.hasOwnProperty("userid")) { this.userid = this.credentials.userid; } else if (this.authtype !== "NONE") { this.error(RED._("email.errors.nouserid")); } if (this.authtype === "BASIC" ) { if (this.credentials && this.credentials.hasOwnProperty("password")) { this.password = this.credentials.password; } else { this.error(RED._("email.errors.nopassword")); } } else if (this.authtype === "XOAUTH2") { this.saslformat = n.saslformat; if (n.token !== "") { this.token = n.token; } else { this.error(RED._("email.errors.notoken")); } } var node = this; node.interval_id = null; // Process a new email message by building a Node-RED message to be passed onwards // in the message flow. The parameter called `msg` is the template message we // start with while `mailMessage` is an object returned from `mailparser` that // will be used to populate the email. // DCJ NOTE: - heirachical multipart mime parsers seem to not exist - this one is barely functional. function processNewMessage(msg, mailMessage) { msg = RED.util.cloneMessage(msg); // Clone the message // Populate the msg fields from the content of the email message // that we have just parsed. msg.payload = mailMessage.text; msg.topic = mailMessage.subject; msg.date = mailMessage.date; msg.header = {}; mailMessage.headers.forEach((v, k) => {msg.header[k] = v;}); if (mailMessage.html) { msg.html = mailMessage.html; } if (mailMessage.to && mailMessage.to.length > 0) { msg.to = mailMessage.to; } if (mailMessage.cc && mailMessage.cc.length > 0) { msg.cc = mailMessage.cc; } if (mailMessage.bcc && mailMessage.bcc.length > 0) { msg.bcc = mailMessage.bcc; } if (mailMessage.from && mailMessage.from.value && mailMessage.from.value.length > 0) { msg.from = mailMessage.from.value[0].address; } if (mailMessage.attachments) { msg.attachments = mailMessage.attachments; } else { msg.attachments = []; } node.send(msg); // Propagate the message down the flow } // End of processNewMessage // Check the POP3 email mailbox for any new messages. For any that are found, // retrieve each message, call processNewMessage to process it and then delete // the messages from the server. async function checkPOP3(msg,send,done) { var tout = (node.repeat > 0) ? node.repeat - 500 : 15000; var saslxoauth2 = ""; var currentMessage = 1; var maxMessage = 0; var nextMessage; pop3 = new Pop3Command({ "host": node.inserver, "tls": node.useSSL, "timeout": tout, "port": node.inport }); try { node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"}); await pop3.connect(); if (node.authtype === "XOAUTH2") { var value = RED.util.getMessageProperty(msg,node.token); if (value !== undefined) { if (node.saslformat) { //Make base64 string for access - compatible with outlook365 and gmail saslxoauth2 = Buffer.from("user="+node.userid+"\x01auth=Bearer "+value+"\x01\x01").toString('base64'); } else { saslxoauth2 = value; } } await pop3.command('AUTH', "XOAUTH2"); await pop3.command(saslxoauth2); } else if (node.authtype === "BASIC") { await pop3.command('USER', node.userid); await pop3.command('PASS', node.password); } } catch(err) { node.error(err.message,err); node.status({fill:"red",shape:"ring",text:"email.status.connecterror"}); setInputRepeatTimeout(); done(); return; } maxMessage = (await pop3.STAT()).split(" ")[0]; if (maxMessage>0) { node.status({fill:"blue", shape:"dot", text:"email.status.fetching"}); while(currentMessage<=maxMessage) { try { nextMessage = await pop3.RETR(currentMessage); } catch(err) { node.error(RED._("email.errors.fetchfail", err.message),err); node.status({fill:"red",shape:"ring",text:"email.status.fetcherror"}); setInputRepeatTimeout(); done(); return; } try { // We have now received a new email message. Create an instance of a mail parser // and pass in the email message. The parser will signal when it has parsed the message. simpleParser(nextMessage, {}, function(err, parsed) { //node.log(util.format("simpleParser: on(end): %j", mailObject)); if (err) { node.status({fill:"red", shape:"ring", text:"email.status.parseerror"}); node.error(RED._("email.errors.parsefail", {folder:node.box}), err); } else { processNewMessage(msg, parsed); } }); //processNewMessage(msg, nextMessage); } catch(err) { node.error(RED._("email.errors.parsefail", {folder:node.box}), err); node.status({fill:"red",shape:"ring",text:"email.status.parseerror"}); setInputRepeatTimeout(); done(); return; } await pop3.DELE(currentMessage); currentMessage++; } await pop3.QUIT(); node.status({fill:"green",shape:"dot",text:"finished"}); setTimeout(status_clear, 5000); setInputRepeatTimeout(); done(); } } // End of checkPOP3 // // checkIMAP // // Check the email sever using the IMAP protocol for new messages. var s = false; var ss = false; function checkIMAP(msg,send,done) { var tout = (node.repeat > 0) ? node.repeat - 500 : 15000; var saslxoauth2 = ""; if (node.authtype === "XOAUTH2") { var value = RED.util.getMessageProperty(msg,node.token); if (value !== undefined) { if (node.saslformat) { //Make base64 string for access - compatible with outlook365 and gmail saslxoauth2 = Buffer.from("user="+node.userid+"\x01auth=Bearer "+value+"\x01\x01").toString('base64'); } else { saslxoauth2 = value; } } imap = new Imap({ xoauth2: saslxoauth2, host: node.inserver, port: node.inport, tls: node.useSSL, autotls: node.autotls, tlsOptions: { rejectUnauthorized: false }, connTimeout: tout, authTimeout: tout }); } else { imap = new Imap({ user: node.userid, password: node.password, host: node.inserver, port: node.inport, tls: node.useSSL, autotls: node.autotls, tlsOptions: { rejectUnauthorized: false }, connTimeout: tout, authTimeout: tout }); } imap.on('error', function(err) { if (err.errno !== "ECONNRESET") { s = false; node.error(err.message,err); node.status({fill:"red",shape:"ring",text:"email.status.connecterror"}); } setInputRepeatTimeout(); }); //console.log("Checking IMAP for new messages"); // We get back a 'ready' event once we have connected to imap s = true; imap.once("ready", function() { if (ss === true) { return; } ss = true; node.status({fill:"blue", shape:"dot", text:"email.status.fetching"}); //console.log("> ready"); // Open the folder imap.openBox(node.box, // Mailbox name false, // Open readonly? function(err, box) { //console.log("> Inbox err : %j", err); //console.log("> Inbox open: %j", box); if (err) { var boxs = []; imap.getBoxes(function(err,boxes) { if (err) { return; } for (var prop in boxes) { if (boxes.hasOwnProperty(prop)) { if (boxes[prop].children) { boxs.push(prop+"/{"+Object.keys(boxes[prop].children)+'}'); } else { boxs.push(prop); } } } node.error(RED._("email.errors.fetchfail", {folder:node.box+". Folders - "+boxs.join(', ')}),err); }); node.status({fill:"red", shape:"ring", text:"email.status.foldererror"}); imap.end(); s = false; setInputRepeatTimeout(); done(err); return; } else { var criteria = ((node.criteria === '_msg_')? (msg.criteria || ["UNSEEN"]): ([node.criteria])); if (Array.isArray(criteria)) { try { imap.search(criteria, function(err, results) { if (err) { node.status({fill:"red", shape:"ring", text:"email.status.foldererror"}); node.error(RED._("email.errors.fetchfail", {folder:node.box}),err); imap.end(); s = false; setInputRepeatTimeout(); done(err); return; } else { //console.log("> search - err=%j, results=%j", err, results); if (results.length === 0) { //console.log(" [X] - Nothing to fetch"); node.status({results:0}); imap.end(); s = false; setInputRepeatTimeout(); msg.payload = 0; done(); return; } var marks = false; if (node.disposition === "Read") { marks = true; } // We have the search results that contain the list of unseen messages and can now fetch those messages. var fetch = imap.fetch(results, { bodies: '', struct: true, markSeen: marks }); // For each fetched message returned ... fetch.on('message', function(imapMessage, seqno) { //node.log(RED._("email.status.message",{number:seqno})); //console.log("> Fetch message - msg=%j, seqno=%d", imapMessage, seqno); imapMessage.on('body', function(stream, info) { //console.log("> message - body - stream=?, info=%j", info); simpleParser(stream, {}, function(err, parsed) { if (err) { node.status({fill:"red", shape:"ring", text:"email.status.parseerror"}); node.error(RED._("email.errors.parsefail", {folder:node.box}),err); } else { processNewMessage(msg, parsed); } }); }); // End of msg->body }); // End of fetch->message // When we have fetched all the messages, we don't need the imap connection any more. fetch.on('end', function() { node.status({results:results.length}); var cleanup = function() { imap.end(); s = false; setInputRepeatTimeout(); msg.payload = results.length; done(); }; if (node.disposition === "Delete") { imap.addFlags(results, '\\Deleted', imap.expunge(cleanup) ); } else if (node.disposition === "Read") { imap.addFlags(results, '\\Seen', cleanup); } else { cleanup(); } }); fetch.once('error', function(err) { console.log('Fetch error: ' + err); imap.end(); s = false; setInputRepeatTimeout(); done(err); }); } }); // End of imap->search } catch(e) { node.status({fill:"red", shape:"ring", text:"email.status.bad_criteria"}); node.error(e.toString(),e); s = ss = false; imap.end(); done(e); return; } } else { node.status({fill:"red", shape:"ring", text:"email.status.bad_criteria"}); node.error(RED._("email.errors.bad_criteria"),msg); s = ss = false; imap.end(); done(); return; } } }); // End of imap->openInbox }); // End of imap->ready node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"}); imap.connect(); } // End of checkIMAP // Perform a check of the email inboxes using either POP3 or IMAP function checkEmail(msg,send,done) { if (node.protocol === "POP3") { checkPOP3(msg,send,done); } else if (node.protocol === "IMAP") { if (s === false && ss == false) { checkIMAP(msg,send,done); } } } // End of checkEmail node.on("input", function(msg, send, done) { send = send || function() { node.send.apply(node,arguments) }; checkEmail(msg,send,done); }); node.on("close", function() { if (this.interval_id != null) { clearTimeout(this.interval_id); } if (imap) { imap.end(); setTimeout(function() { imap.destroy(); },1000); node.status({}); } }); function status_clear() { node.status({}); } function setInputRepeatTimeout() { // Set the repetition timer as needed if (!isNaN(node.repeat) && node.repeat > 0) { node.interval_id = setTimeout( function() { node.emit("input",{}); }, node.repeat ); } ss = false; } if (this.inputs !== 1) { node.emit("input",{}); } } RED.nodes.registerType("e-mail in",EmailInNode,{ credentials: { userid: { type:"text" }, password: { type: "password" }, global: { type:"boolean" } } }); function EmailMtaNode(n) { RED.nodes.createNode(this, n); this.port = n.port; this.secure = n.secure; this.starttls = n.starttls; this.certFile = n.certFile; this.keyFile = n.keyFile; this.users = n.users; this.auth = n.auth; try { this.options = JSON.parse(n.expert); } catch (error) { this.options = {}; } var node = this; if (!Array.isArray(node.options.disabledCommands)) { node.options.disabledCommands = []; } node.options.secure = node.secure; if (node.certFile) { node.options.cert = fs.readFileSync(node.certFile); } if (node.keyFile) { node.options.key = fs.readFileSync(node.keyFile); } if (!node.starttls) { node.options.disabledCommands.push("STARTTLS"); } if (!node.auth) { node.options.disabledCommands.push("AUTH"); } node.options.onData = function (stream, session, callback) { simpleParser(stream, { skipTextToHtml:true, skipTextLinks:true }, (err, parsed) => { if (err) { node.error(RED._("email.errors.parsefail"),err); } else { node.status({fill:"green", shape:"dot", text:""}); var msg = {} msg.payload = parsed.text; msg.topic = parsed.subject; msg.date = parsed.date; msg.header = {}; parsed.headers.forEach((v, k) => {msg.header[k] = v;}); if (parsed.html) { msg.html = parsed.html; } if (parsed.to) { if (typeof(parsed.to) === "string" && parsed.to.length > 0) { msg.to = parsed.to; } else if (parsed.to.hasOwnProperty("text") && parsed.to.text.length > 0) { msg.to = parsed.to.text; } } if (parsed.cc) { if (typeof(parsed.cc) === "string" && parsed.cc.length > 0) { msg.cc = parsed.cc; } else if (parsed.cc.hasOwnProperty("text") && parsed.cc.text.length > 0) { msg.cc = parsed.cc.text; } } if (parsed.cc && parsed.cc.length > 0) { msg.cc = parsed.cc; } if (parsed.bcc && parsed.bcc.length > 0) { msg.bcc = parsed.bcc; } if (parsed.from && parsed.from.value && parsed.from.value.length > 0) { msg.from = parsed.from.value[0].address; } if (parsed.attachments) { msg.attachments = parsed.attachments; } else { msg.attachments = []; } node.send(msg); // Propagate the message down the flow setTimeout(function() { node.status({})}, 500); } callback(); }); } node.options.onAuth = function (auth, session, callback) { let id = node.users.findIndex(function (item) { return item.name === auth.username; }); if (id >= 0 && node.users[id].password === auth.password) { callback(null, { user: id + 1 }); } else { callback(new Error("Invalid username or password")); } } node.mta = new SMTPServer(node.options); node.mta.listen(node.port); node.mta.on("error", err => { node.error("Error: " + err.message, err); }); node.on("close", function() { node.mta.close(); }); } RED.nodes.registerType("e-mail mta",EmailMtaNode); };