Email rework (#195)

* Rework of Node-RED email nodes
This commit is contained in:
Neil Kolban
2016-04-20 13:47:23 -05:00
committed by Dave Conway-Jones
parent 8563624983
commit 3107080b19
6 changed files with 358 additions and 117 deletions

View File

@@ -14,12 +14,23 @@
* limitations under the License.
**/
/**
* POP3 protocol - RFC1939 - https://www.ietf.org/rfc/rfc1939.txt
*
* Dependencies:
* * poplib - https://www.npmjs.com/package/poplib
* * 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 nodemailer = require("nodemailer");
var Imap = require('imap');
//console.log(nodemailer.Transport.transports.SMTP.wellKnownHosts);
var Imap = require('imap');
var POP3Client = require("poplib");
var MailParser = require("mailparser").MailParser;
var util = require("util");
try {
var globalkeys = RED.settings.email || require(process.env.NODE_RED_HOME+"/../emailkeys.js");
@@ -122,14 +133,26 @@ module.exports = function(RED) {
global: { type:"boolean"}
}
});
//
// EmailInNode
//
// Setup the EmailInNode
function EmailInNode(n) {
var imap;
RED.nodes.createNode(this,n);
this.name = n.name;
this.repeat = n.repeat * 1000 || 300000;
this.inserver = n.server || (globalkeys && globalkeys.server) || "imap.gmail.com";
this.inport = n.port || (globalkeys && globalkeys.port) || "993";
this.box = n.box || "INBOX";
this.name = n.name;
this.repeat = n.repeat * 1000 || 300000;
this.inserver = n.server || (globalkeys && globalkeys.server) || "imap.gmail.com";
this.inport = n.port || (globalkeys && globalkeys.port) || "993";
this.box = n.box || "INBOX";
this.useSSL = n.useSSL;
this.protocol = n.protocol || "IMAP";
this.disposition = n.disposition || "None"; // "None", "Delete", "Read"
var flag = false;
if (this.credentials && this.credentials.hasOwnProperty("userid")) {
@@ -158,105 +181,242 @@ module.exports = function(RED) {
var node = this;
this.interval_id = null;
var oldmail = {};
// 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.
function processNewMessage(msg, mailMessage) {
msg = JSON.parse(JSON.stringify(msg)); // Clone the message
// Populate the msg fields from the content of the email message
// that we have just parsed.
msg.html = mailMessage.html;
msg.payload = mailMessage.text;
if (mailMessage.attachments) {
msg.attachments = mailMessage.attachments;
} else {
msg.attachments = [];
}
msg.topic = mailMessage.subject;
msg.header = mailMessage.headers;
msg.date = mailMessage.date;
if (mailMessage.from && mailMessage.from.length > 0) {
msg.from = mailMessage.from[0].address;
}
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.
function checkPOP3(msg) {
var currentMessage;
var maxMessage;
var imap = new Imap({
user: node.userid,
password: node.password,
host: node.inserver,
port: node.inport,
tls: true,
tlsOptions: { rejectUnauthorized: false },
connTimeout: node.repeat,
authTimeout: node.repeat
});
// Form a new connection to our email server using POP3.
var pop3Client = new POP3Client(
node.inport, node.inserver,
{enabletls: node.useSSL} // Should we use SSL to connect to our email server?
);
if (!isNaN(this.repeat) && this.repeat > 0) {
this.interval_id = setInterval( function() {
node.emit("input",{});
}, this.repeat );
}
// If we have a next message to retrieve, ask to retrieve it otherwise issue a
// quit request.
function nextMessage() {
if (currentMessage > maxMessage) {
pop3Client.quit();
return;
}
pop3Client.retr(currentMessage);
currentMessage++;
} // End of nextMessage
pop3Client.on("stat", function(status, data) {
// Data contains:
// {
// count: <Number of messages to be read>
// octect: <size of messages to be read>
// }
if (status) {
currentMessage = 1;
maxMessage = data.count;
nextMessage();
} else {
node.log(util.format("stat error: %s %j", status, data));
}
});
pop3Client.on("error", function(err) {
node.log("We caught an error: " + JSON.stringify(err));
});
pop3Client.on("connect", function() {
//node.log("We are now connected");
pop3Client.login("kolban@test.com", "password");
});
pop3Client.on("login", function(status, rawData) {
//node.log("login: " + status + ", " + rawData);
if (status) {
pop3Client.stat();
} else {
node.log(util.format("login error: %s %j", status, rawData));
pop3Client.quit();
}
});
pop3Client.on("retr", function(status, msgNumber, data, rawData) {
node.log(util.format("retr: status=%s, msgNumber=%d, data=%j", status, msgNumber, data));
if (status) {
// 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.
var mailparser = new MailParser();
mailparser.on("end", function(mailObject) {
//node.log(util.format("mailparser: on(end): %j", mailObject));
processNewMessage(msg, mailObject);
});
mailparser.write(data);
mailparser.end();
pop3Client.dele(msgNumber);
}
else {
node.log(util.format("retr error: %s %j", status, rawData));
pop3Client.quit();
}
});
pop3Client.on("invalid-state", function(cmd) {
node.log("Invalid state: " + cmd);
});
pop3Client.on("locked", function(cmd) {
node.log("We were locked: " + cmd);
});
// When we have deleted the last processed message, we can move on to
// processing the next message.
pop3Client.on("dele", function(status, msgNumber) {
nextMessage();
});
}; // End of checkPOP3
//
// checkIMAP
//
// Check the email sever using the IMAP protocol for new messages.
function checkIMAP(msg) {
node.log("Checkimg IMAP for new messages");
// We get back a 'ready' event once we have connected to imap
imap.once("ready", function() {
node.status({fill:"blue", shape:"dot", text:"email.status.fetching"});
console.log("> ready");
// Open the inbox folder
imap.openBox('INBOX', // Mailbox name
false, // Open readonly?
function(err, box) {
console.log("> Inbox open: %j", box);
imap.search([ 'UNSEEN' ], 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);
return;
}
console.log("> search - err=%j, results=%j", err, results);
if (results.length === 0) {
console.log(" [X] - Nothing to fetch");
return;
}
// We have the search results that contain the list of unseen messages and can now fetch those messages.
var fetch = imap.fetch(results, {
bodies : ["HEADER", "TEXT"],
markSeen : true
});
// For each fetched message returned ...
fetch.on('message', function(imapMessage, seqno) {
node.log(RED._("email.status.message",{number:seqno}));
var messageText = "";
console.log("> Fetch message - msg=%j, seqno=%d", imapMessage, seqno);
imapMessage.on('body', function(stream, info) {
console.log("> message - body - stream=?, info=%j", info);
// Info defined which part of the message this is ... for example
// 'TEXT' or 'HEADER'
stream.on('data', function(chunk) {
console.log("> stream - data - chunk=??");
messageText += chunk.toString('utf8');
});
}); // End of msg->body
// When the `end` event is raised on the message
imapMessage.once('end', function() {
console.log("> msg - end : %j", messageText);
var mailparser = new MailParser();
mailparser.on("end", function(mailMessage) {
//console.log("mailparser: on(end): %j", mailMessage);
processNewMessage(msg, mailMessage);
});
mailparser.write(messageText);
mailparser.end();
}); // End of msg->end
}); // End of fetch->message
// When we have fetched all the messages, we don't need the imap connection any more.
fetch.on('end', function() {
var cleanup = function() {
node.status({});
imap.end();
};
if (this.disposition == "Delete") {
imap.addFlags(results, "\Deleted", cleanup);
} else if (this.disposition == "Read") {
imap.addFlags(results, "\Answered", cleanup);
} else {
cleanup();
}
});
fetch.once('error', function(err) {
console.log('Fetch error: ' + err);
});
}); // End of imap->search
}); // End of imap->openInbox
}); // End of imap->ready
imap.connect();
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
}; // End of checkIMAP
// Perform a check of the email inboxes using either POP3 or IMAP
function checkEmail(msg) {
if (node.protocol === "POP3") {
checkPOP3(msg);
} else if (node.protocol === "IMAP") {
checkIMAP(msg);
}
}; // End of checkEmail
if (node.protocol === "IMAP") {
imap = new Imap({
user: node.userid,
password: node.password,
host: node.inserver,
port: node.inport,
tls: node.useSSL,
tlsOptions: { rejectUnauthorized: false },
connTimeout: node.repeat,
authTimeout: node.repeat
});
imap.on('error', function(err) {
node.log(err);
node.status({fill:"red",shape:"ring",text:"email.status.connecterror"});
});
};
this.on("input", function(msg) {
imap.once('ready', function() {
node.status({fill:"blue",shape:"dot",text:"email.status.fetching"});
var pay = {};
imap.openBox(node.box, false, function(err, box) {
if (err) {
node.status({fill:"red",shape:"ring",text:"email.status.foldererror"});
node.error(RED._("email.errors.fetchfail",{folder:node.box}),err);
}
else {
if (box.messages.total > 0) {
//var f = imap.seq.fetch(box.messages.total + ':*', { markSeen:true, bodies: ['HEADER.FIELDS (FROM SUBJECT DATE TO CC BCC)','TEXT'] });
var f = imap.seq.fetch(box.messages.total + ':*', { markSeen:true, bodies: ['HEADER','TEXT'] });
f.on('message', function(msg, seqno) {
node.log(RED._("email.status.message",{number:seqno}));
var prefix = '(#' + seqno + ') ';
msg.on('body', function(stream, info) {
var buffer = '';
stream.on('data', function(chunk) {
buffer += chunk.toString('utf8');
});
stream.on('end', function() {
if (info.which !== 'TEXT') {
var head = Imap.parseHeader(buffer);
if (head.hasOwnProperty("from")) { pay.from = head.from[0]; }
if (head.hasOwnProperty("subject")) { pay.topic = head.subject[0]; }
if (head.hasOwnProperty("date")) { pay.date = head.date[0]; }
pay.header = head;
} else {
var parts = buffer.split("Content-Type");
for (var p = 0; p < parts.length; p++) {
if (parts[p].indexOf("text/plain") >= 0) {
pay.payload = parts[p].split("\n").slice(1,-2).join("\n").trim();
}
else if (parts[p].indexOf("text/html") >= 0) {
pay.html = parts[p].split("\n").slice(1,-2).join("\n").trim();
} else {
pay.payload = parts[0];
}
}
//pay.body = buffer;
}
});
});
msg.on('end', function() {
//node.log('finished: '+prefix);
});
});
f.on('error', function(err) {
node.warn(RED._("email.errors.messageerror",{error:err}));
node.status({fill:"red",shape:"ring",text:"email.status.messageerror"});
});
f.on('end', function() {
delete(pay._msgid);
if (JSON.stringify(pay) !== oldmail) {
oldmail = JSON.stringify(pay);
node.send(pay);
node.log(RED._("email.status.newemail",{topic:pay.topic}));
}
else { node.log(RED._("email.status.duplicate",{topic:pay.topic})); }
//node.status({fill:"green",shape:"dot",text:"node-red:common.status.ok"});
node.status({});
});
}
else {
node.log(RED._("email.status.inboxzero"));
//node.status({fill:"green",shape:"dot",text:"node-red:common.status.ok"});
node.status({});
}
}
imap.end();
});
});
node.status({fill:"grey",shape:"dot",text:"node-red:common.status.connecting"});
imap.connect();
});
imap.on('error', function(err) {
node.log(err);
node.status({fill:"red",shape:"ring",text:"email.status.connecterror"});
checkEmail(msg);
});
this.on("close", function() {
@@ -266,13 +426,21 @@ module.exports = function(RED) {
if (imap) { imap.destroy(); }
});
// Set the repetition timer as needed
if (!isNaN(this.repeat) && this.repeat > 0) {
this.interval_id = setInterval( function() {
node.emit("input",{});
}, this.repeat );
}
node.emit("input",{});
}
RED.nodes.registerType("e-mail in",EmailInNode,{
credentials: {
userid: {type:"text"},
password: {type: "password"},
global: { type:"boolean"}
userid: { type:"text" },
password: { type: "password" },
global: { type:"boolean" }
}
});
};