diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 526464a6..9c8cea7f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -11,7 +11,7 @@ Put an `x` in the boxes that apply --> - [ ] Bugfix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) +- [x] New feature (non-breaking change which adds functionality) +Adds authentication option to the Email node (node-red-node-email) to use OAuth and XOAuth2 +********** This version: IMAP ONLY ********** + ## Checklist -- [ ] I have read the [contribution guidelines](https://github.com/node-red/node-red-nodes/blob/master/CONTRIBUTING.md) -- [ ] For non-bugfix PRs, I have discussed this change on the forum/slack team. -- [ ] I have run `grunt` to verify the unit tests pass -- [ ] I have added suitable unit tests to cover the new/changed functionality +- [x] I have read the [contribution guidelines](https://github.com/node-red/node-red-nodes/blob/master/CONTRIBUTING.md) +- [x] For non-bugfix PRs, I have discussed this change on the forum/slack team. +- [x] I have run `grunt` to verify the unit tests pass +- [x] I have added suitable unit tests to cover the new/changed functionality diff --git a/.gitignore b/.gitignore index 729dabf3..7d7e9f83 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ setenv.sh package-lock.json social/xmpp/92-xmpp.old *.tgz +.DS_Store diff --git a/function/random/package.json b/function/random/package.json index 13ac148e..ba190135 100644 --- a/function/random/package.json +++ b/function/random/package.json @@ -1,6 +1,6 @@ { "name" : "node-red-node-random", - "version" : "0.4.0", + "version" : "0.4.1", "description" : "A Node-RED node that when triggered generates a random number between two values.", "dependencies" : { }, diff --git a/function/random/random.js b/function/random/random.js index 681cfefe..1b9f0c50 100644 --- a/function/random/random.js +++ b/function/random/random.js @@ -18,9 +18,8 @@ module.exports = function(RED) { if (node.low) { // if the the node has a value use it tmp.low = Number(node.low); } else if ('from' in msg) { // else see if a 'from' is in the msg - if (Number(msg.from)) { // if it is, and is a number, use it - tmp.low = Number(msg.from); - } else { // otherwise setup NaN error + tmp.low = Number(msg.from); + if (isNaN(msg.from)) { // if it isn't a number setup NaN error tmp.low = NaN; tmp.low_e = " From: " + msg.from; // setup to show bad incoming msg.from } @@ -31,9 +30,8 @@ module.exports = function(RED) { if (node.high) { // if the the node has a value use it tmp.high = Number(node.high); } else if ('to' in msg) { // else see if a 'to' is in the msg - if (Number(msg.to)) { // if it is, and is a number, use it - tmp.high = Number(msg.to); - } else { // otherwise setup NaN error + tmp.high = Number(msg.to); + if (isNaN(msg.to)) { // if it isn't a number setup NaN error tmp.high = NaN tmp.high_e = " To: " + msg.to // setup to show bad incoming msg.to } diff --git a/hardware/PiGpio/36-rpi-gpio.js b/hardware/PiGpio/36-rpi-gpio.js index d93a8a40..ae234c49 100644 --- a/hardware/PiGpio/36-rpi-gpio.js +++ b/hardware/PiGpio/36-rpi-gpio.js @@ -45,46 +45,59 @@ module.exports = function(RED) { } } + var startPin = function() { + node.child = spawn(gpioCommand, ["in",node.pin,node.intype,node.debounce]); + node.running = true; + node.status({fill:"yellow",shape:"dot",text:"rpi-gpio.status.ok"}); + + node.child.stdout.on('data', function (data) { + var d = data.toString().trim().split("\n"); + for (var i = 0; i < d.length; i++) { + if (d[i] === '') { return; } + if (node.running && node.buttonState !== -1 && !isNaN(Number(d[i])) && node.buttonState !== d[i]) { + node.send({ topic:"gpio/"+node.pin, payload:Number(d[i]) }); + } + node.buttonState = d[i]; + node.status({fill:"green",shape:"dot",text:d[i]}); + if (RED.settings.verbose) { node.log("out: "+d[i]+" :"); } + } + }); + + node.child.stderr.on('data', function (data) { + if (RED.settings.verbose) { node.log("err: "+data+" :"); } + }); + + node.child.on('close', function (code) { + node.running = false; + node.child.removeAllListeners(); + delete node.child; + if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); } + if (!node.finished && code === 1) { + setTimeout(function() {startPin()}, 250); + } + else if (node.finished) { + node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); + node.finished(); + } + else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } + }); + + node.child.on('error', function (err) { + if (err.code === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")+err.path,err); } + else if (err.code === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")+err.path,err); } + else { node.error(RED._("rpi-gpio.errors.error",{error:err.code}),err) } + }); + + node.child.stdin.on('error', function (err) { + if (!node.finished) { + node.error(RED._("rpi-gpio.errors.error",{error:err.code}),err); + } + }); + } + if (allOK === true) { if (node.pin !== undefined) { - node.child = spawn(gpioCommand, ["in",node.pin,node.intype,node.debounce]); - node.running = true; - node.status({fill:"yellow",shape:"dot",text:"rpi-gpio.status.ok"}); - - node.child.stdout.on('data', function (data) { - var d = data.toString().trim().split("\n"); - for (var i = 0; i < d.length; i++) { - if (d[i] === '') { return; } - if (node.running && node.buttonState !== -1 && !isNaN(Number(d[i])) && node.buttonState !== d[i]) { - node.send({ topic:"gpio/"+node.pin, payload:Number(d[i]) }); - } - node.buttonState = d[i]; - node.status({fill:"green",shape:"dot",text:d[i]}); - if (RED.settings.verbose) { node.log("out: "+d[i]+" :"); } - } - }); - - node.child.stderr.on('data', function (data) { - if (RED.settings.verbose) { node.log("err: "+data+" :"); } - }); - - node.child.on('close', function (code) { - node.running = false; - node.child = null; - if (RED.settings.verbose) { node.log(RED._("rpi-gpio.status.closed")); } - if (node.finished) { - node.status({fill:"grey",shape:"ring",text:"rpi-gpio.status.closed"}); - node.finished(); - } - else { node.status({fill:"red",shape:"ring",text:"rpi-gpio.status.stopped"}); } - }); - - node.child.on('error', function (err) { - if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } - else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } - else { node.error(RED._("rpi-gpio.errors.error",{error:err.errno})) } - }); - + startPin(); } else { node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); @@ -108,10 +121,13 @@ module.exports = function(RED) { delete pinsInUse[node.pin]; if (node.child != null) { node.finished = done; - node.child.stdin.write("close "+node.pin); - node.child.kill('SIGKILL'); + node.child.stdin.write("close "+node.pin, () => { + if (node.child) { + node.child.kill('SIGKILL'); + } + }); } - else { done(); } + else { if (done) { done(); } } }); } RED.nodes.registerType("rpi-gpio in",GPIOInNode); @@ -188,11 +204,16 @@ module.exports = function(RED) { }); node.child.on('error', function (err) { - if (err.errno === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")); } - else if (err.errno === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")); } - else { node.error(RED._("rpi-gpio.errors.error")+': ' + err.errno); } + if (err.code === "ENOENT") { node.error(RED._("rpi-gpio.errors.commandnotfound")+err.path,err); } + else if (err.code === "EACCES") { node.error(RED._("rpi-gpio.errors.commandnotexecutable")+err.path,err); } + else { node.error(RED._("rpi-gpio.errors.error",{error:err.code}),err) } }); + node.child.stdin.on('error', function (err) { + if (!node.finished) { + node.error(RED._("rpi-gpio.errors.error",{error:err.code}),err); + } + }); } else { node.warn(RED._("rpi-gpio.errors.invalidpin")+": "+node.pin); @@ -210,10 +231,12 @@ module.exports = function(RED) { delete pinsInUse[node.pin]; if (node.child != null) { node.finished = done; - node.child.stdin.write("close "+node.pin); - node.child.kill('SIGKILL'); + node.child.stdin.write("close "+node.pin, () => { + node.child.kill('SIGKILL'); + setTimeout(function() { if (done) { done(); } }, 50); + }); } - else { done(); } + else { if (done) { done(); } } }); } diff --git a/hardware/PiGpio/locales/de/36-rpi-gpio.json b/hardware/PiGpio/locales/de/36-rpi-gpio.json index 9024f880..8f8c621e 100644 --- a/hardware/PiGpio/locales/de/36-rpi-gpio.json +++ b/hardware/PiGpio/locales/de/36-rpi-gpio.json @@ -66,8 +66,8 @@ "invalidinput": "Ungültige Eingabe", "needtobeexecutable": "__command__ muss ausführbar sein", "mustbeexecutable": "nrgpio muss ausführbar sein", - "commandnotfound": "nrgpio-Befehl nicht gefunden", - "commandnotexecutable": "nrgpio-Befehl nicht ausführbar", + "commandnotfound": "nrgpio-Befehl nicht gefunden ", + "commandnotexecutable": "nrgpio-Befehl nicht ausführbar ", "error": "Fehler: __error__", "pythoncommandnotfound": "nrgpio-Python-Befehl nicht aktiv" } diff --git a/hardware/PiGpio/locales/en-US/36-rpi-gpio.json b/hardware/PiGpio/locales/en-US/36-rpi-gpio.json index 8931f170..fdc1bcec 100644 --- a/hardware/PiGpio/locales/en-US/36-rpi-gpio.json +++ b/hardware/PiGpio/locales/en-US/36-rpi-gpio.json @@ -66,8 +66,8 @@ "invalidinput": "Invalid input", "needtobeexecutable": "__command__ needs to be executable", "mustbeexecutable": "nrgpio must to be executable", - "commandnotfound": "nrgpio command not found", - "commandnotexecutable": "nrgpio command not executable", + "commandnotfound": "nrgpio command not found ", + "commandnotexecutable": "nrgpio command not executable ", "error": "error: __error__", "pythoncommandnotfound": "nrgpio python command not running" } diff --git a/hardware/PiGpio/locales/ja/36-rpi-gpio.json b/hardware/PiGpio/locales/ja/36-rpi-gpio.json index 619eb3ef..644980a8 100644 --- a/hardware/PiGpio/locales/ja/36-rpi-gpio.json +++ b/hardware/PiGpio/locales/ja/36-rpi-gpio.json @@ -66,8 +66,8 @@ "invalidinput": "入力が不正です", "needtobeexecutable": "__command__ は実行可能である必要があります", "mustbeexecutable": "nrgpio は実行可能である必要があります", - "commandnotfound": "nrgpio コマンドが見つかりません", - "commandnotexecutable": "nrgpio コマンドが実行可能ではありません", + "commandnotfound": "nrgpio コマンドが見つかりません ", + "commandnotexecutable": "nrgpio コマンドが実行可能ではありません ", "error": "エラー: __error__", "pythoncommandnotfound": "nrgpio python コマンドが実行されていません" } diff --git a/hardware/PiGpio/locales/ko/36-rpi-gpio.json b/hardware/PiGpio/locales/ko/36-rpi-gpio.json index 323ae882..a5161ac9 100644 --- a/hardware/PiGpio/locales/ko/36-rpi-gpio.json +++ b/hardware/PiGpio/locales/ko/36-rpi-gpio.json @@ -65,8 +65,8 @@ "invalidinput": "입력이 올바르지 않습니다", "needtobeexecutable": "__command__ 은 실행가능상태일 필요가 있습니다 ", "mustbeexecutable": "nrgpio 은 실행가능상태일 필요가 있습니다 ", - "commandnotfound": "nrgpio 커맨드를 찾을수 없습니다", - "commandnotexecutable": "nrgpio 커맨드가 실행가능상태가 아닙니다", + "commandnotfound": "nrgpio 커맨드를 찾을수 없습니다 ", + "commandnotexecutable": "nrgpio 커맨드가 실행가능상태가 아닙니다 ", "error": "에러: __error__", "pythoncommandnotfound": "nrgpio python 커맨드가 실행되지 않았습니다" } diff --git a/hardware/PiGpio/package.json b/hardware/PiGpio/package.json index e445708c..7581e563 100644 --- a/hardware/PiGpio/package.json +++ b/hardware/PiGpio/package.json @@ -1,6 +1,6 @@ { "name": "node-red-node-pi-gpio", - "version": "2.0.3", + "version": "2.0.6", "description": "The basic Node-RED node for Pi GPIO", "dependencies" : { }, diff --git a/hardware/intel/mraa-gpio-ain.html b/hardware/intel/mraa-gpio-ain.html index 93d5a5ae..57b19627 100644 --- a/hardware/intel/mraa-gpio-ain.html +++ b/hardware/intel/mraa-gpio-ain.html @@ -1,7 +1,7 @@ diff --git a/hardware/intel/mraa-gpio-din.js b/hardware/intel/mraa-gpio-din.js index 5966e448..cfb79612 100644 --- a/hardware/intel/mraa-gpio-din.js +++ b/hardware/intel/mraa-gpio-din.js @@ -7,13 +7,16 @@ module.exports = function(RED) { RED.nodes.createNode(this,n); this.pin = n.pin; this.interrupt = n.interrupt; + this.mode = n.mode; + this.initialMsg = n.initial; this.x = new m.Gpio(parseInt(this.pin)); this.board = m.getPlatformName(); + this.defaultTimeout = 100; var node = this; - node.x.mode(m.PIN_GPIO); + node.x.mode(parseInt(this.mode)); node.x.dir(m.DIR_IN); - node.x.isr(m.EDGE_BOTH, function() { - var g = node.x.read(); + + var eventHandler = function(g) { var msg = { payload:g, topic:node.board+"/D"+node.pin }; switch (g) { case 0: { @@ -34,8 +37,15 @@ module.exports = function(RED) { node.status({fill:"grey",shape:"ring",text:"unknown"}); } } - }); - switch (node.x.read()) { + } + + var isrCallback = function() { + eventHandler(node.x.read()); + } + + node.x.isr(m.EDGE_BOTH, isrCallback); + var initialState = node.x.read(); + switch (initialState) { case 0: { node.status({fill:"green",shape:"ring",text:"low"}); break; @@ -48,8 +58,17 @@ module.exports = function(RED) { node.status({}); } } + + if (this.initialMsg) { + setTimeout(() => { + node.send( { payload: node.x.read(), topic:node.board+"/D"+node.pin } ); + }, this.defaultTimeout); + } + this.on('close', function() { node.x.isr(m.EDGE_BOTH, null); + node.x.isrExit(); + node.x.close(); }); } RED.nodes.registerType("mraa-gpio-din", gpioDin); diff --git a/hardware/intel/mraa-gpio-dout.html b/hardware/intel/mraa-gpio-dout.html index ed0289ac..b9808617 100644 --- a/hardware/intel/mraa-gpio-dout.html +++ b/hardware/intel/mraa-gpio-dout.html @@ -1,7 +1,7 @@ diff --git a/hardware/intel/mraa-gpio-dout.js b/hardware/intel/mraa-gpio-dout.js index 020db558..6bb833d3 100644 --- a/hardware/intel/mraa-gpio-dout.js +++ b/hardware/intel/mraa-gpio-dout.js @@ -29,6 +29,7 @@ module.exports = function(RED) { }); this.on('close', function() { + node.p.close(); }); } RED.nodes.registerType("mraa-gpio-dout", gpioDout); diff --git a/hardware/intel/mraa-gpio-led.html b/hardware/intel/mraa-gpio-led.html new file mode 100644 index 00000000..985774fa --- /dev/null +++ b/hardware/intel/mraa-gpio-led.html @@ -0,0 +1,85 @@ + + + + + + diff --git a/hardware/intel/mraa-gpio-led.js b/hardware/intel/mraa-gpio-led.js new file mode 100644 index 00000000..d9a4ce08 --- /dev/null +++ b/hardware/intel/mraa-gpio-led.js @@ -0,0 +1,86 @@ +module.exports = function(RED) { + var m = require('mraa'); + function LEDNode(n) { + RED.nodes.createNode(this, n); + this.pin = Number(n.pin); + this.color = Number(n.color); + + if (this.pin == 0) { + this.user1_green = new m.Led(0); /*user-led1-green*/ + this.user1_red = new m.Led(1); /*user-led1-red*/ + } if(this.pin == 1) { + this.user2_green = new m.Led(2); /*user-led2-green*/ + this.user2_red = new m.Led(3); /*user-led2-red*/ + } + + function set_led_green(led_green, led_red) { + led_green.setBrightness(1); + led_red.setBrightness(0); + } + + function set_led_red(led_green, led_red) { + led_green.setBrightness(0); + led_red.setBrightness(1); + } + + function set_led_orange(led_green, led_red) { + led_green.setBrightness(1); + led_red.setBrightness(1); + } + + function turn_off_led(led_green, led_red) { + led_green.setBrightness(0); + led_red.setBrightness(0); + } + + this.on("input", function(msg) { + if (this.pin == 0) { + this.led_green = this.user1_green; + this.led_red = this.user1_red; + } + else if (this.pin == 1) { + this.led_green = this.user2_green; + this.led_red = this.user2_red; + } + + if (msg.payload == "1") { + switch(this.color) { + case 0: + set_led_green(this.led_green, this.led_red); + break; + case 1: + set_led_red(this.led_green, this.led_red); + break; + case 2: + set_led_orange(this.led_green, this.led_red); + break; + default: + console.log("unexpected"); + break; + } + } + else { + turn_off_led(this.led_green, this.led_red); + } + }); + + this.on('close', function() { + if (this.pin == 0) { + this.user1_green.close(); + this.user1_red.close(); + } if(this.pin == 1) { + this.user2_green.close(); + this.user2_red.close(); + } + }); + } + RED.nodes.registerType("mraa-gpio-led", LEDNode); + + RED.httpAdmin.get('/mraa-gpio/:id', RED.auth.needsPermission('mraa-gpio.read'), function(req,res) { + res.json(m.getPlatformType()); + }); + + RED.httpAdmin.get('/mraa-version/:id', RED.auth.needsPermission('mraa-version.read'), function(req,res) { + res.json(m.getVersion()); + }); +} diff --git a/hardware/intel/mraa-gpio-pwm.html b/hardware/intel/mraa-gpio-pwm.html index 2dfb95e6..c1d078bc 100644 --- a/hardware/intel/mraa-gpio-pwm.html +++ b/hardware/intel/mraa-gpio-pwm.html @@ -1,7 +1,7 @@ diff --git a/hardware/intel/mraa-gpio-pwm.js b/hardware/intel/mraa-gpio-pwm.js index 22b02cb4..6a2469f2 100644 --- a/hardware/intel/mraa-gpio-pwm.js +++ b/hardware/intel/mraa-gpio-pwm.js @@ -21,6 +21,7 @@ module.exports = function(RED) { this.on('close', function() { node.p.enable(false); + node.p.close(); }); } RED.nodes.registerType("mraa-gpio-pwm", gpioPWM); diff --git a/hardware/intel/package.json b/hardware/intel/package.json index d360b4e5..6238514b 100644 --- a/hardware/intel/package.json +++ b/hardware/intel/package.json @@ -1,7 +1,7 @@ { "name" : "node-red-node-intel-gpio", - "version" : "0.0.6", - "description" : "A Node-RED node to talk to an Intel Galileo or Edison using mraa", + "version" : "0.3.0", + "description" : "A Node-RED node to talk to an Intel Galileo, Edison or Siemens IOT2050 board using mraa", "dependencies" : { }, "repository" : { @@ -10,18 +10,27 @@ "directory" : "tree/master/hardware/intel" }, "license": "Apache-2.0", - "keywords": [ "node-red", "intel", "galileo", "edison" ], + "keywords": [ "node-red", "intel", "galileo", "edison", "siemens", "iot2050" ], "node-red" : { "nodes" : { "mraa-gpio-ain": "mraa-gpio-ain.js", "mraa-gpio-din": "mraa-gpio-din.js", "mraa-gpio-dout": "mraa-gpio-dout.js", - "mraa-gpio-pwm": "mraa-gpio-pwm.js" + "mraa-gpio-pwm": "mraa-gpio-pwm.js", + "mraa-gpio-led": "mraa-gpio-led.js" } }, "author": { "name": "Dave Conway-Jones", "email": "ceejay@vnet.ibm.com", "url": "http://nodered.org" - } + }, + "contributors": [ + { + "name": "@fr0st61te" + }, + { + "name": "@jan-kiszka" + } + ] } diff --git a/hardware/mcp3008/.DS_Store b/hardware/mcp3008/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/hardware/mcp3008/.DS_Store and /dev/null differ diff --git a/hardware/sensehat/package.json b/hardware/sensehat/package.json index 23060bbc..c9845b99 100644 --- a/hardware/sensehat/package.json +++ b/hardware/sensehat/package.json @@ -1,6 +1,6 @@ { "name" : "node-red-node-pi-sense-hat", - "version" : "0.1.2", + "version" : "0.1.4", "description" : "A Node-RED node to interact with a Raspberry Pi Sense HAT", "repository" : { "type":"git", diff --git a/hardware/sensehat/sensehat.js b/hardware/sensehat/sensehat.js index 67179233..f41d4360 100644 --- a/hardware/sensehat/sensehat.js +++ b/hardware/sensehat/sensehat.js @@ -7,7 +7,7 @@ module.exports = function(RED) { var hatCommand = __dirname+'/sensehat'; - if (!fs.existsSync('/usr/lib/python2.7/dist-packages/sense_hat')) { + if (!fs.existsSync('/usr/lib/python2.7/dist-packages/sense_hat') && !fs.existsSync('/usr/lib/python3/dist-packages/sense_hat')) { throw "Error: Can't find Sense HAT python libraries. Run sudo apt-get install sense-hat"; } @@ -107,7 +107,13 @@ module.exports = function(RED) { // Any data on stderr means a bad thing has happened. // Best to kill it and let it reconnect. if (RED.settings.verbose) { RED.log.error("err: "+data+" :"); } - hat.kill('SIGKILL'); + if (data.indexOf("WARNING") === 0) { + if (data.indexOf("sensor not present") !== -1) { return; } + else { RED.log.warn(data); } + } + else { + hat.kill('SIGKILL'); + } }); hat.stderr.on('error', function(err) { }); hat.stdin.on('error', function(err) { }); diff --git a/hardware/unicorn/package.json b/hardware/unicorn/package.json index 78af1a10..43ff515a 100644 --- a/hardware/unicorn/package.json +++ b/hardware/unicorn/package.json @@ -1,9 +1,9 @@ { "name" : "node-red-node-pi-unicorn-hat", - "version" : "0.1.1", + "version" : "0.1.2", "description" : "A Node-RED node to output to a Raspberry Pi Unicorn HAT from Pimorini.", "dependencies" : { - "pngjs": "2.2.*" + "pngjs": "2.3.1" }, "repository" : { "type":"git", diff --git a/hardware/wemo/README.md b/hardware/wemo/README.md index d5a6dbd7..03995a84 100644 --- a/hardware/wemo/README.md +++ b/hardware/wemo/README.md @@ -16,7 +16,7 @@ Run the following command in your Node-RED user directory - typically `~/.node-r The output node switches a socket, a light or group of lights on or off -This should be backward compatible with the pervious version of this node but will benefit +This should be backward compatible with the previous version of this node but will benefit from opening the config dialog and selecting the node you want. The node accepts the following `msg.payload` as input diff --git a/hardware/wemo/package.json b/hardware/wemo/package.json index 339b6ced..8dc74a6d 100644 --- a/hardware/wemo/package.json +++ b/hardware/wemo/package.json @@ -28,14 +28,14 @@ ], "license": "Apache-2.0", "dependencies": { - "node-ssdp": "~3.2.5", - "request": "~2.74.0", + "node-ssdp": "~3.3.0", + "request": "~2.88.2", "xml2js": "~0.4.13", - "util": "~0.10.3", + "util": "~0.12.4", "url": "~0.11.0", - "ip": "~1.0.1", - "body-parser": "~1.14.1", - "q": "~1.4.1" + "ip": "~1.1.5", + "body-parser": "~1.20.0", + "q": "~1.5.1" }, "node-red": { "nodes": { diff --git a/io/ping/88-ping.js b/io/ping/88-ping.js index c7119ba1..505f9380 100644 --- a/io/ping/88-ping.js +++ b/io/ping/88-ping.js @@ -4,7 +4,7 @@ module.exports = function(RED) { var spawn = require("child_process").spawn; var plat = require("os").platform(); - function doPing(node, host, arrayMode) { + function doPing(node, host, msg, arrayMode) { const defTimeout = 5000; var ex, ex6, hostOptions, commandLineOptions; if (typeof host === "string") { @@ -20,7 +20,8 @@ module.exports = function(RED) { hostOptions.timeout = hostOptions.timeout < 1000 ? 1000 : hostOptions.timeout; hostOptions.timeout = hostOptions.timeout > 30000 ? 30000 : hostOptions.timeout; var timeoutS = Math.round(hostOptions.timeout / 1000); //whole numbers only - var msg = { payload:false, topic:hostOptions.host }; + msg.payload = false; + msg.topic = hostOptions.host; //only include the extra msg object if operating in advance/array mode. if (arrayMode) { msg.ping = hostOptions @@ -221,7 +222,7 @@ module.exports = function(RED) { let pingables = generatePingList(node.host); for (let index = 0; index < pingables.length; index++) { const element = pingables[index]; - if (element) { doPing(node, element, false); } + if (element) { doPing(node, element, {}, false); } } }, node.timer); } @@ -234,12 +235,12 @@ module.exports = function(RED) { let pingables = generatePingList(payload) for (let index = 0; index < pingables.length; index++) { const element = pingables[index]; - if (element) { doPing(node, element, false); } + if (element) { doPing(node, element, RED.util.cloneMessage(msg), false); } } } else if (Array.isArray(payload) ) { for (let index = 0; index < payload.length; index++) { const element = payload[index]; - if (element) { doPing(node, element, true); } + if (element) { doPing(node, element, RED.util.cloneMessage(msg), true); } } } }); diff --git a/io/ping/package.json b/io/ping/package.json index 8f5e22be..c56918ad 100644 --- a/io/ping/package.json +++ b/io/ping/package.json @@ -1,6 +1,6 @@ { "name" : "node-red-node-ping", - "version" : "0.3.1", + "version" : "0.3.3", "description" : "A Node-RED node to ping a remote server, for use as a keep-alive check.", "dependencies" : { }, diff --git a/io/serialport/25-serial.html b/io/serialport/25-serial.html index dfc73f5e..30469ddd 100644 --- a/io/serialport/25-serial.html +++ b/io/serialport/25-serial.html @@ -117,22 +117,22 @@   - + - - - @@ -149,22 +149,22 @@   - - - - @@ -177,23 +177,23 @@
- +
- - +
- @@ -204,7 +204,7 @@
- +
diff --git a/io/serialport/25-serial.js b/io/serialport/25-serial.js index 6bd45fab..64093ceb 100644 --- a/io/serialport/25-serial.js +++ b/io/serialport/25-serial.js @@ -271,7 +271,7 @@ module.exports = function(RED) { if (addchar !== "") { payload += addchar; } } else if (addchar !== "") { - payload = Buffer.concat([payload,addchar]); + payload = Buffer.concat([payload,Buffer.from(addchar)]); } return payload; }, diff --git a/io/serialport/package.json b/io/serialport/package.json index 22d65481..32ebcdc5 100644 --- a/io/serialport/package.json +++ b/io/serialport/package.json @@ -1,9 +1,9 @@ { "name" : "node-red-node-serialport", - "version" : "1.0.1", + "version" : "1.0.3", "description" : "Node-RED nodes to talk to serial ports", "dependencies" : { - "serialport" : "^10.3.0" + "serialport" : "^10.5.0" }, "repository" : { "type":"git", @@ -21,7 +21,7 @@ "engines" : { "node" : ">=12.0.0" }, "author": { "name": "Dave Conway-Jones", - "email": "ceejay@vnet.ibm.com", + "email": "dceejay@gmail.com", "url": "http://nodered.org" } } diff --git a/io/snmp/README.md b/io/snmp/README.md index 5067b11b..7dd5062d 100644 --- a/io/snmp/README.md +++ b/io/snmp/README.md @@ -1,8 +1,16 @@ node-red-node-snmp ================== -A pair of Node-RED nodes that -fetch either individual oids, or a table oid from a SNMP enabled host. +A set of Node-RED nodes that +fetch values from SNMP enabled hosts. Supports v1, v2c and v3. +* SNMP get - Simple SNMP oid or oid list fetcher +* SNMP set - Simple snmp Set node. +* SNMP subtree - Simple sub tree fetcher +* SNMP table - Simple SNMP oid table fetcher +* SNMP walker - Simple SNMP oid walker fetcher + +## v2 Breaking Change +v2 has a breaking change in that the single snmp node no longer tries to stringify an octet string type (04). This makes it consistent with the other nodes in this bundle. this means the user now has to convert to a string if required but has better control of how they wish to do that. Install ------- @@ -18,9 +26,15 @@ Usage SNMP oids fetcher. Can fetch a single or comma separated list of oids. Triggered by any input. -`msg.host` may contain the host. +`msg.host` may contain the host including the port. -`msg.community` may contain the community. +`msg.community` may contain the community. (v1 and v2c only) + +`msg.username` may contain the username. (v3 only) + +`msg.authkey` may contain the digest security key. (v3 only) + +`msg.privkey` may contain the encryption security key. (v3 only) `msg.oid` may contain a comma separated list of oids to search for. (no spaces) @@ -28,6 +42,12 @@ The host configured in the edit config will override `msg.host`. Leave blank if The community configured in the edit config will override `msg.community`. Leave blank if you want to use `msg.community` to provide input. +The username configured in the edit config will override `msg.username`. Leave blank if you want to use `msg.username` to provide input. + +The digest security key configured in the edit config will override `msg.authkey`. Leave blank if you want to use `msg.authkey` to provide input. + +The encryption security key configured in the edit config will override `msg.privkey`. Leave blank if you want to use `msg.privkey` to provide input. + The oids configured in the edit config will override `msg.oid`. Leave blank if you want to use `msg.oid` to provide input. @@ -38,9 +58,15 @@ Values depends on the oids being requested. SNMP sets the value of one or more OIDs. -`msg.host` may contain the host. +`msg.host` may contain the host including the port. -`msg.community` may contain the community. +`msg.community` may contain the community. (v1 and v2c only) + +`msg.username` may contain the username. (v3 only) + +`msg.authkey` may contain the digest security key. (v3 only) + +`msg.privkey` may contain the encryption security key. (v3 only) `msg.varbinds` may contain an array of varbind JSON objects e.g.: ``` @@ -81,6 +107,12 @@ The host configured in the edit config will override `msg.host`. Leave blank if The community configured in the edit config will override `msg.community`. Leave blank if you want to use `msg.community` to provide input. +The username configured in the edit config will override `msg.username`. Leave blank if you want to use `msg.username` to provide input. + +The digest security key configured in the edit config will override `msg.authkey`. Leave blank if you want to use `msg.authkey` to provide input. + +The encryption security key configured in the edit config will override `msg.privkey`. Leave blank if you want to use `msg.privkey` to provide input. + The varbinds configured in the edit config will override `msg.varbinds`. Leave blank if you want to use `msg.varbinds` to provide input. @@ -89,16 +121,28 @@ The varbinds configured in the edit config will override `msg.varbinds`. Leave b Simple SNMP table oid fetcher. Triggered by any input. -`msg.host` may contain the host. +`msg.host` may contain the host including the port. -`msg.community` may contain the community. +`msg.community` may contain the community. (v1 and v2c only) -`msg.oid` may contain the oid of a single table to search for. +`msg.username` may contain the username. (v3 only) + +`msg.authkey` may contain the digest security key. (v3 only) + +`msg.privkey` may contain the encryption security key. (v3 only) + +`msg.oid` may contain a comma separated list of oids to search for. (no spaces) The host configured in the edit config will override `msg.host`. Leave blank if you want to use `msg.host` to provide input. The community configured in the edit config will override `msg.community`. Leave blank if you want to use `msg.community` to provide input. +The username configured in the edit config will override `msg.username`. Leave blank if you want to use `msg.username` to provide input. + +The digest security key configured in the edit config will override `msg.authkey`. Leave blank if you want to use `msg.authkey` to provide input. + +The encryption security key configured in the edit config will override `msg.privkey`. Leave blank if you want to use `msg.privkey` to provide input. + The oid configured in the edit config will override `msg.oid`. Leave blank if you want to use `msg.oid` to provide input. @@ -109,9 +153,15 @@ Values depends on the oids being requested. Simple SNMP oid subtree fetcher. Triggered by any input. Reads from OID specified and any below it. -`msg.host` may contain the host. +`msg.host` may contain the host including the port. -`msg.community` may contain the community. +`msg.community` may contain the community. (v1 and v2c only) + +`msg.username` may contain the username. (v3 only) + +`msg.authkey` may contain the digest security key. (v3 only) + +`msg.privkey` may contain the encryption security key. (v3 only) `msg.oid` may contain the oid of a single table to search for. @@ -119,6 +169,12 @@ The host configured in the edit config will override `msg.host`. Leave blank if The community configured in the edit config will override `msg.community`. Leave blank if you want to use `msg.community` to provide input. +The username configured in the edit config will override `msg.username`. Leave blank if you want to use `msg.username` to provide input. + +The digest security key configured in the edit config will override `msg.authkey`. Leave blank if you want to use `msg.authkey` to provide input. + +The encryption security key configured in the edit config will override `msg.privkey`. Leave blank if you want to use `msg.privkey` to provide input. + The oid configured in the edit config will override `msg.oid`. Leave blank if you want to use `msg.oid` to provide input. @@ -129,9 +185,15 @@ Values depends on the oids being requested. Simple SNMP oid walker fetcher. Triggered by any input. Reads from OID specified to the end of the table. -`msg.host` may contain the host. +`msg.host` may contain the host including the port. -`msg.community` may contain the community. +`msg.community` may contain the community. (v1 and v2c only) + +`msg.username` may contain the username. (v3 only) + +`msg.authkey` may contain the digest security key. (v3 only) + +`msg.privkey` may contain the encryption security key. (v3 only) `msg.oid` may contain the oid of a single table to search for. @@ -139,6 +201,12 @@ The host configured in the edit config will override `msg.host`. Leave blank if The community configured in the edit config will override `msg.community`. Leave blank if you want to use `msg.community` to provide input. +The username configured in the edit config will override `msg.username`. Leave blank if you want to use `msg.username` to provide input. + +The digest security key configured in the edit config will override `msg.authkey`. Leave blank if you want to use `msg.authkey` to provide input. + +The encryption security key configured in the edit config will override `msg.privkey`. Leave blank if you want to use `msg.privkey` to provide input. + The oid configured in the edit config will override `msg.oid`. Leave blank if you want to use `msg.oid` to provide input. diff --git a/io/snmp/package.json b/io/snmp/package.json index 3f1393c1..e64f3ec0 100644 --- a/io/snmp/package.json +++ b/io/snmp/package.json @@ -1,9 +1,9 @@ { "name" : "node-red-node-snmp", - "version" : "0.0.25", - "description" : "A Node-RED node that looks for SNMP oids.", + "version" : "2.0.0", + "description" : "A Node-RED node that gets and sets SNMP oid values. Supports v1, v2c and v3", "dependencies" : { - "net-snmp" : "1.2.4" + "net-snmp" : "^3.9.0" }, "repository" : { "type":"git", @@ -11,7 +11,7 @@ "directory" : "tree/master/io/snmp" }, "license": "Apache-2.0", - "keywords": [ "node-red", "snmp", "oid" ], + "keywords": [ "node-red", "snmp", "oid", "snmpv3" ], "node-red" : { "nodes" : { "snmp": "snmp.js" @@ -24,6 +24,9 @@ }, "contributors": [ { "name": "Mika Karaila" }, - { "name": "Bryan Malyn" } + { "name": "Bryan Malyn" }, + { "name": "Steve-Mcl" }, + { "name": "Andres" }, + { "name": "@echobops" } ] } diff --git a/io/snmp/snmp.html b/io/snmp/snmp.html index 4cd13ef8..3e3c029b 100644 --- a/io/snmp/snmp.html +++ b/io/snmp/snmp.html @@ -1,21 +1,112 @@ + + @@ -65,22 +170,61 @@
-
- - -
Timeout -  S +  S
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
-
@@ -94,15 +238,18 @@

Simple snmp Set node. Trigger by any input

msg.host may contain the host.

msg.community may contain the community.

+

msg.username may contain the username. (V3 only)

+

msg.authkey may contain the digest security key. (V3 only)

+

msg.privkey may contain the encryption security key. (V3 only)

msg.varbinds may contain varbinds as an array of json objects containing multiple oids, types and values. -

[
-    {
-        "oid":   "1.3.6.1.2.1.1.5.0",
-        "type":  "OctetString",
-        "value": "host1"
-    },
-    {   "oid":   ... }
-]
+
[
+  {
+    "oid": "1.3.6.1.2.1.1.5.0",
+    "type": "OctetString",
+    "value": "host1"
+  },
+  { "oid": ... }
+]

Any numeric inputs must be numbers, not strings, e.g. 1 not "1".

OIDs must be numeric. iso. is the same a 1.

@@ -114,12 +261,27 @@ color: "YellowGreen", defaults: { host: { value: "127.0.0.1" }, - community: { value: "public" }, version: { value: "1", required: true }, - varbinds: { value: "" }, timeout: { value: 5 }, + community: { value: "public" }, + auth: { value: "noAuthNoPriv", required: true }, + authprot: { value: "MD5", required: true }, + privprot: { value: "DES", required: true }, + oids: { value: "" }, + varbinds: { value: "", validate:function(v) { + try { + return !v || !!JSON.parse(v); + } catch(e) { + return false; + } + }}, name: { value: "" } }, + credentials: { + username: { type: "text" }, + authkey: { type: "password" }, + privkey: { type: "password" } + }, inputs: 1, outputs: 0, icon: "snmp.png", @@ -128,6 +290,9 @@ }, labelStyle: function () { return this.name ? "node_label_italic" : ""; + }, + oneditprepare: function () { + node_snmp_common.oneditprepare(this); } }); @@ -137,19 +302,58 @@
-
- - -
Timeout -  S +  S
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -165,6 +369,9 @@

Simple SNMP oid table fetcher. Triggered by any input.

msg.host may contain the host.

msg.community may contain the community.

+

msg.username may contain the username. (V3 only)

+

msg.authkey may contain the digest security key. (V3 only)

+

msg.privkey may contain the encryption security key. (V3 only)

msg.oid may contain the oid of a table to request.

OID must be numeric. iso. is the same a 1.

The node will output msg.payload and msg.oid.

@@ -176,12 +383,20 @@ color: "YellowGreen", defaults: { host: { value: "127.0.0.1" }, - community: { value: "public" }, version: { value: "1", required: true }, - oids: { value: "" }, timeout: { value: 5 }, + community: { value: "public" }, + auth: { value: "noAuthNoPriv", required: true }, + authprot: { value: "MD5", required: true }, + privprot: { value: "DES", required: true }, + oids: { value: "" }, name: { value: "" } }, + credentials: { + username: { type: "text" }, + authkey: { type: "password" }, + privkey: { type: "password" } + }, inputs: 1, outputs: 1, icon: "snmp.png", @@ -190,6 +405,9 @@ }, labelStyle: function () { return this.name ? "node_label_italic" : ""; + }, + oneditprepare: function () { + node_snmp_common.oneditprepare(this); } }); @@ -199,19 +417,58 @@
-
- - -
Timeout -  S +  S
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -227,6 +484,9 @@

Simple SNMP oid subtree fetcher. Triggered by any input. Reads all OIDS at and below the current base OID.

msg.host may contain the host.

msg.community may contain the community.

+

msg.username may contain the username. (V3 only)

+

msg.authkey may contain the digest security key. (V3 only)

+

msg.privkey may contain the encryption security key. (V3 only)

msg.oid may contain the oid of a table to request.

OID must be numeric. iso. is the same a 1.

The node will output msg.payload and msg.oid.

@@ -238,12 +498,20 @@ color: "YellowGreen", defaults: { host: { value: "127.0.0.1" }, - community: { value: "public" }, version: { value: "1", required: true }, - oids: { value: "" }, timeout: { value: 5 }, + community: { value: "public" }, + auth: { value: "noAuthNoPriv", required: true }, + authprot: { value: "MD5", required: true }, + privprot: { value: "DES", required: true }, + oids: { value: "" }, name: { value: "" } }, + credentials: { + username: { type: "text" }, + authkey: { type: "password" }, + privkey: { type: "password" } + }, inputs: 1, outputs: 1, icon: "snmp.png", @@ -252,6 +520,9 @@ }, labelStyle: function () { return this.name ? "node_label_italic" : ""; + }, + oneditprepare: function () { + node_snmp_common.oneditprepare(this); } }); @@ -262,19 +533,58 @@
-
- - -
Timeout -  S +  S
+
+ + +
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
@@ -287,14 +597,17 @@ @@ -304,12 +617,20 @@ color: "YellowGreen", defaults: { host: { value: "127.0.0.1" }, - community: { value: "public" }, version: { value: "1", required: true }, - oids: { value: "" }, timeout: { value: 5 }, + community: { value: "public" }, + auth: { value: "noAuthNoPriv", required: true }, + authprot: { value: "MD5", required: true }, + privprot: { value: "DES", required: true }, + oids: { value: "" }, name: { value: "" } }, + credentials: { + username: { type: "text" }, + authkey: { type: "password" }, + privkey: { type: "password" } + }, inputs: 1, outputs: 1, icon: "snmp.png", @@ -318,6 +639,9 @@ }, labelStyle: function () { return this.name ? "node_label_italic" : ""; + }, + oneditprepare: function () { + node_snmp_common.oneditprepare(this); } }); diff --git a/io/snmp/snmp.js b/io/snmp/snmp.js index bac37c6e..3853e5fa 100644 --- a/io/snmp/snmp.js +++ b/io/snmp/snmp.js @@ -1,262 +1,480 @@ module.exports = function (RED) { "use strict"; - var snmp = require("net-snmp"); - - var sessions = {}; - - function getSession(host, community, version, timeout) { - var sessionKey = host + ":" + community + ":" + version; - var port = 161; - if (host.indexOf(":") !== -1) { - port = host.split(":")[1]; - host = host.split(":")[0]; + const SNMP = require("net-snmp"); + const sessions = {}; + function generateUUID() { + let d = Date.now(); + let d2 = ((typeof performance !== 'undefined') && performance.now && (performance.now() * 1000)) || (Date.now() * Math.random() * 100000);//Time in microseconds since load + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + let r = Math.random() * 16;//random number between 0 and 16 + if (d > 0) {//Use timestamp until depleted + r = (d + r) % 16 | 0; + d = Math.floor(d / 16); + } else {//Use microseconds since page-load if supported + r = (d2 + r) % 16 | 0; + d2 = Math.floor(d2 / 16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + } + function openSession(sessionid, host, user, options) { + // SNMPv3 call + if (options.version === SNMP.Version3) { + sessions[sessionid] = SNMP.createV3Session(host, user, options); } - if (!(sessionKey in sessions)) { - sessions[sessionKey] = snmp.createSession(host, community, { port:port, version:version, timeout:(timeout || 5000) }); + // SNMPv1 or SNMPv2c call + else { + sessions[sessionid] = SNMP.createSession(host, user.community, options); } - return sessions[sessionKey]; + return sessions[sessionid]; } - function SnmpNode(n) { - RED.nodes.createNode(this, n); - this.community = n.community; - this.host = n.host; - this.version = (n.version === "2c") ? snmp.Version2c : snmp.Version1; - this.oids = n.oids.replace(/\s/g, ""); - this.timeout = Number(n.timeout || 5) * 1000; - var node = this; + // Any session needs to be closed after completion + function closeSession(sessionid) { + try { + sessions[sessionid].removeAllListeners(); + } catch (e) { } + try { + sessions[sessionid].close(); + } catch (e) { } + delete sessions[sessionid]; + } - this.on("input", function (msg) { - var host = node.host || msg.host; - var community = node.community || msg.community; - var oids = node.oids || msg.oid; + function initSnmpNode(node, config) { + node.community = config.community; + node.host = config.host; + node.version = config.version; + node.auth = config.auth; + node.authprot = config.authprot; + node.privprot = config.privprot; + if (node.credentials) { + node.username = node.credentials.username; + node.authkey = node.credentials.authkey; + node.privkey = node.credentials.privkey; + } + node.timeout = Number(config.timeout || 5) * 1000; + } + + function prepareSnmpOptions(node, msg) { + let host = node.host || msg.host; + const sessionid = generateUUID(); + const user = {} + const options = {}; + const compat = { "v1": "1", "v2": "2c", "v2c": "2c", "v3": "3" }; + if(compat[node.version]) { + node.version = compat[node.version]; + } else if(["1","2c","3"].indexOf(node.version) < 0) { + node.version = "1"; + } + options.version = node.version; + if (node.version === "1") { + options.version = SNMP.Version1; + user.community = node.community || msg.community; + } else if (node.version === "2c") { + options.version = SNMP.Version2c; + user.community = node.community || msg.community; + } else if (node.version === "3") { + user.name = node.username || msg.username || ""; + user.level = SNMP.SecurityLevel.noAuthNoPriv; + user.authProtocol = SNMP.AuthProtocols.none; + user.authKey = ""; + user.privProtocol = SNMP.PrivProtocols.none; + user.privKey = ""; + options.version = SNMP.Version3; + if (node.auth === "authNoPriv" || node.auth === "authPriv") { + user.level = SNMP.SecurityLevel.authNoPriv; + user.authProtocol = (node.authprot === "SHA") ? SNMP.AuthProtocols.sha : SNMP.AuthProtocols.md5; + user.authKey = node.authkey || msg.authkey || ""; + if (node.auth === "authPriv") { + user.level = SNMP.SecurityLevel.authPriv; + if (node.privprot === "DES" || node.privprot === "AES") { + user.privProtocol = (node.privprot === "AES") ? SNMP.PrivProtocols.aes : SNMP.PrivProtocols.des; + user.privKey = node.privkey || msg.privkey || ""; + } + } + } + } + + options.timeout = node.timeout; + options.debug = msg.debug || undefined; + options.port = options.port || 161; + options.retries = options.retries || 1; + + if (msg.engineID) { + options.engineID = msg.engineID;//The engineID used for SNMPv3 communications, given as a hex string - defaults to a system-generated engineID containing elements of random + } + if (msg.backoff) { + options.backoff = msg.backoff;//The factor by which to increase the timeout for every retry, defaults to 1 for no increase + } + if (msg.backwardsGetNexts) { + options.backwardsGetNexts = msg.backwardsGetNexts;//boolean to allow GetNext operations to retrieve lexicographically preceding OIDs + } + if (msg.idBitsSize === 16 || msg.idBitsSize === 32) { + options.idBitsSize = msg.idBitsSize;//Either 16 or 32, defaults to 32. Used to reduce the size of the generated id for compatibility with some older devices. + } + const ipv = parseIP(host); + if (ipv.version === 4) { + host = ipv.ip; + options.port = ipv.port || options.port; + options.transport = 'udp4'; + } else if (ipv.version === 6) { + host = ipv.ip; + options.port = ipv.port || options.port; + options.transport = 'udp6'; + } else { + //probably a host name + if (host.indexOf(":") > 0) { + host = host.split(":")[0]; + options.port = host.split(":")[1]; + } + } + return { + host: host, + sessionid: sessionid, + user: user, + options: options, + } + } + function parseIP(ip) { + const IPV4_PAT = /^(\d+)\.(\d+)\.(\d+)\.(\d+)(?::(\d+)){0,1}$/g; + const IPV6_DOUBLE_COL_PAT = /^\[{0,1}([0-9a-f:]*)::([0-9a-f:]*)(?:\]:(\d+)){0,1}$/g; + const ipv4Matcher = IPV4_PAT.exec(ip); + let hex = ""; + let port; + let ipOnly = []; + try { + + if (ipv4Matcher && ipv4Matcher.length) { + for (let i = 1; i <= 4; i++) { + ipOnly.push(ipv4Matcher[i]); + hex += toHex4(ipv4Matcher[i]); + } + if (ipv4Matcher[5]) { + port = parseInt(ipv4Matcher[5]); + } + return { ip: ipOnly.join("."), hex, port, version: 4 }; + } + + // IPV6 Must be colons format (a:b:c:d:e:A.B.C.D not currently supported) + let ipv6Pattern = "^\\[{0,1}"; + for (let i = 1; i <= 7; i++) { + ipv6Pattern += "([0-9a-f]+):"; + } + ipv6Pattern += "([0-9a-f]+)(?:\\]:(\\d+)){0,1}$"; + const IPV6_PAT = new RegExp(ipv6Pattern); + + + // IPV6, double colon + const ipv6DoubleColonMatcher = IPV6_DOUBLE_COL_PAT.exec(ip); + if (ipv6DoubleColonMatcher && ipv6DoubleColonMatcher.length) { + let p1 = ipv6DoubleColonMatcher[1]; + if (!p1) { + p1 = "0"; + } + let p2 = ipv6DoubleColonMatcher[2]; + if (!p2) { + p2 = "0"; + } + p1 = p1.padStart(4, "0"); + p2 = p2.padStart(4, "0"); + ip = p1 + getZeros(8 - numCount(p1) - numCount(p2)) + p2; + if (ipv6DoubleColonMatcher[3]) { + ip = "[" + ip + "]:" + ipv6DoubleColonMatcher[3]; + } + } + + // IPV6 + const ipv6Matcher = IPV6_PAT.exec(ip); + if (ipv6Matcher && ipv6Matcher.length) { + for (let i = 1; i <= 8; i++) { + const p = toHex6(ipv6Matcher[i]).padStart(4, "0"); + ipOnly.push(p); + hex += p; + } + if (ipv6Matcher[9]) { + port = parseInt(ipv6Matcher[9]); + } + return { ip: ipOnly.join(":"), hex, port, version: 6 }; + } + + throw new Error("Unknown address: " + ip); + } catch (error) { + return { ip, hex, port, version: null, error: error }; + } + + function numCount(/** @type {string} */s) { + return s.split(":").length; + } + function getZeros(/** @type {number} */ count) { + const sb = [":"]; + while (count > 0) { + sb.push("0000:"); + count--; + } + return sb.join(""); + } + function toHex4(/** @type {string} */ s) { + const val = parseInt(s); + if (val < 0 || val > 255) { + throw new Error("Invalid value : " + s); + } + return val.toString(16).padStart(2, "0"); + } + function toHex6(/** @type {string} */ s) { + const val = parseInt(s, 16); + if (val < 0 || val > 65536) { + throw new Error("Invalid hex value : " + s); + } + return s; + } + } + function SnmpNode(n) { + const node = this; + RED.nodes.createNode(node, n); + initSnmpNode(node, n); + node.oids = n.oids ? n.oids.replace(/\s/g, "") : ""; + + node.on("input", function (msg) { + const oids = node.oids || msg.oid; + const { host, sessionid, user, options } = prepareSnmpOptions(node, msg); if (oids) { - getSession(host, community, node.version, node.timeout).get(oids.split(","), function (error, varbinds) { + let sess = openSession(sessionid, host, user, options); + sess.on("error", function (err) { + node.error(err, msg); + }) + sess.get(oids.split(","), function (error, varbinds) { if (error) { node.error(error.toString(), msg); - } - else { - for (var i = 0; i < varbinds.length; i++) { - if (snmp.isVarbindError(varbinds[i])) { - node.error(snmp.varbindError(varbinds[i]), msg); - } - else { - if (varbinds[i].type == 4) { varbinds[i].value = varbinds[i].value.toString(); } - varbinds[i].tstr = snmp.ObjectType[varbinds[i].type]; - //node.log(varbinds[i].oid + "|" + varbinds[i].tstr + "|" + varbinds[i].value); + } else { + for (let i = 0; i < varbinds.length; i++) { + let vb = varbinds[i]; + if (SNMP.isVarbindError(vb)) { + node.error(SNMP.varbindError(vb), msg); + vb._error = SNMP.varbindError(vb); //add _error to msg so users can determine the varbind is not valid } + // else { + // if (vb.type == 4) { vb.value = vb.value.toString(); } + // } + vb.tstr = SNMP.ObjectType[vb.type]; } - msg.oid = oids; msg.payload = varbinds; + msg.oid = oids; node.send(msg); } + closeSession(sessionid); // Needed to close the session else a bad or good read could affect future readings }); - } - else { + } else { node.warn("No oid(s) to search for"); } }); } - RED.nodes.registerType("snmp", SnmpNode); + RED.nodes.registerType("snmp", SnmpNode, { + credentials: { + username: { type: "text" }, + authkey: { type: "password" }, + privkey: { type: "password" } + } + }); function SnmpSNode(n) { - RED.nodes.createNode(this, n); - this.community = n.community; - this.host = n.host; - this.version = (n.version === "2c") ? snmp.Version2c : snmp.Version1; - this.varbinds = n.varbinds; - this.timeout = Number(n.timeout || 5) * 1000; - if (this.varbinds && this.varbinds.trim().length === 0) { delete this.varbinds; } - var node = this; - this.on("input", function (msg) { - var host = node.host || msg.host; - var community = node.community || msg.community; - var varbinds = (node.varbinds) ? JSON.parse(node.varbinds) : msg.varbinds; + const node = this; + RED.nodes.createNode(node, n); + initSnmpNode(node, n); + node.varbinds = n.varbinds; + if (node.varbinds && node.varbinds.trim().length === 0) { delete node.varbinds; } + node.on("input", function (msg) { + const { host, sessionid, user, options } = prepareSnmpOptions(node, msg); + const varbinds = (node.varbinds) ? JSON.parse(node.varbinds) : msg.varbinds; if (varbinds) { - for (var i = 0; i < varbinds.length; i++) { - varbinds[i].type = snmp.ObjectType[varbinds[i].type]; + for (let i = 0; i < varbinds.length; i++) { + varbinds[i].type = SNMP.ObjectType[varbinds[i].type]; } - getSession(host, community, node.version, node.timeout).set(varbinds, function (error, varbinds) { + let sess = openSession(sessionid, host, user, options); + sess.on("error", function (err) { + node.error(err, msg); + }) + sess.set(varbinds, function (error, varbinds) { if (error) { node.error(error.toString(), msg); - } - else { - for (var i = 0; i < varbinds.length; i++) { + } else { + for (let i = 0; i < varbinds.length; i++) { // for version 2c we must check each OID for an error condition - if (snmp.isVarbindError(varbinds[i])) { - node.error(snmp.varbindError(varbinds[i]), msg); + if (SNMP.isVarbindError(varbinds[i])) { + node.error(SNMP.varbindError(varbinds[i]), msg); } } } + closeSession(sessionid); }); - } - else { + } else { node.warn("No varbinds to set"); } }); } - RED.nodes.registerType("snmp set", SnmpSNode); + RED.nodes.registerType("snmp set", SnmpSNode, { + credentials: { + username: { type: "text" }, + authkey: { type: "password" }, + privkey: { type: "password" } + } + }); function SnmpTNode(n) { - RED.nodes.createNode(this, n); - this.community = n.community; - this.host = n.host; - this.version = (n.version === "2c") ? snmp.Version2c : snmp.Version1; - this.oids = n.oids.replace(/\s/g, ""); - this.timeout = Number(n.timeout || 5) * 1000; - var node = this; - var maxRepetitions = 20; + const node = this; + RED.nodes.createNode(node, n); + initSnmpNode(node, n); + node.oids = n.oids ? n.oids.replace(/\s/g, "") : "" + const maxRepetitions = 20; function sortInt(a, b) { if (a > b) { return 1; } - else if (b > a) { return -1; } - else { return 0; } + else if (b > a) { return -1; } else { return 0; } } - this.on("input", function (msg) { - var host = node.host || msg.host; - var community = node.community || msg.community; - var oids = node.oids || msg.oid; + node.on("input", function (msg) { + const oids = node.oids || msg.oid; + const { host, sessionid, user, options } = prepareSnmpOptions(node, msg); if (oids) { msg.oid = oids; - getSession(host, community, node.version, node.timeout).table(oids, maxRepetitions, function (error, table) { + let sess = openSession(sessionid, host, user, options); + sess.on("error", function (err) { + node.error(err, msg); + }) + sess.table(oids, maxRepetitions, function (error, table) { if (error) { node.error(error.toString(), msg); - } - else { - var indexes = []; - for (var index in table) { + } else { + const indexes = []; + for (let index in table) { if (table.hasOwnProperty(index)) { indexes.push(parseInt(index)); } } indexes.sort(sortInt); - for (var i = 0; i < indexes.length; i++) { - var columns = []; - for (var column in table[indexes[i]]) { + for (let i = 0; i < indexes.length; i++) { + const columns = []; + for (let column in table[indexes[i]]) { if (table[indexes[i]].hasOwnProperty(column)) { columns.push(parseInt(column)); } } columns.sort(sortInt); - // console.log("row index = " + indexes[i]); - // for (var j = 0; j < columns.length; j++) { - // console.log(" column " + columns[j] + " = " + table[indexes[i]][columns[j]]); - // } } msg.payload = table; node.send(msg); } + closeSession(sessionid); }); - } - else { + } else { node.warn("No oid to search for"); } }); } - RED.nodes.registerType("snmp table", SnmpTNode); + RED.nodes.registerType("snmp table", SnmpTNode, { + credentials: { + username: { type: "text" }, + authkey: { type: "password" }, + privkey: { type: "password" } + } + }); function SnmpSubtreeNode(n) { - RED.nodes.createNode(this, n); - this.community = n.community; - this.host = n.host; - this.version = (n.version === "2c") ? snmp.Version2c : snmp.Version1; - this.oids = n.oids.replace(/\s/g, ""); - this.timeout = Number(n.timeout || 5) * 1000; - var node = this; - var maxRepetitions = 20; - var response = []; + const node = this; + RED.nodes.createNode(node, n); + initSnmpNode(node, n); + node.oids = n.oids ? n.oids.replace(/\s/g, "") : "" + const maxRepetitions = 20; - function feedCb(varbinds) { - for (var i = 0; i < varbinds.length; i++) { - if (snmp.isVarbindError(varbinds[i])) { - node.error(snmp.varbindError(varbinds[i]), msg); - } - else { - //console.log(varbinds[i].oid + "|" + varbinds[i].value); - response.push({ oid: varbinds[i].oid, value: varbinds[i].value }); + node.on("input", function (msg) { + const oids = node.oids || msg.oid; + const { host, sessionid, user, options } = prepareSnmpOptions(node, msg); + const response = []; + function feedCb(varbinds) { + for (let i = 0; i < varbinds.length; i++) { + if (SNMP.isVarbindError(varbinds[i])) { + node.error(SNMP.varbindError(varbinds[i]), msg); + } else { + response.push({ oid: varbinds[i].oid, value: varbinds[i].value }); + } } } - } - - this.on("input", function (msg) { - var host = node.host || msg.host; - var community = node.community || msg.community; - var oids = node.oids || msg.oid; if (oids) { msg.oid = oids; - getSession(host, community, node.version, node.timeout).subtree(msg.oid, maxRepetitions, feedCb, function (error) { + let sess = openSession(sessionid, host, user, options); + sess.on("error", function (err) { + node.error(err, msg); + }) + sess.subtree(msg.oid, maxRepetitions, feedCb, function (error) { if (error) { node.error(error.toString(), msg); - } - else { - // Clone the array - msg.payload = response.slice(0); + } else { + msg.payload = response; node.send(msg); - //Clears response - response.length = 0; } + closeSession(sessionid); }); - } - else { + } else { node.warn("No oid to search for"); } }); } - RED.nodes.registerType("snmp subtree", SnmpSubtreeNode); - + RED.nodes.registerType("snmp subtree", SnmpSubtreeNode, { + credentials: { + username: { type: "text" }, + authkey: { type: "password" }, + privkey: { type: "password" } + } + }); function SnmpWalkerNode(n) { - RED.nodes.createNode(this, n); - this.community = n.community; - this.host = n.host; - this.version = (n.version === "2c") ? snmp.Version2c : snmp.Version1; - this.oids = n.oids.replace(/\s/g, ""); - this.timeout = Number(n.timeout || 5) * 1000; - var node = this; - var maxRepetitions = 20; - var response = []; + const node = this; + RED.nodes.createNode(node, n); + initSnmpNode(node, n); + node.oids = n.oids ? n.oids.replace(/\s/g, "") : "" + const maxRepetitions = 20; - function feedCb(varbinds) { - for (var i = 0; i < varbinds.length; i++) { - if (snmp.isVarbindError(varbinds[i])) { - node.error(snmp.varbindError(varbinds[i]), msg); - } - else { - //console.log(varbinds[i].oid + "|" + varbinds[i].value); - response.push({ oid: varbinds[i].oid, value: varbinds[i].value }); + node.on("input", function (msg) { + const oids = node.oids || msg.oid; + const { host, sessionid, user, options } = prepareSnmpOptions(node, msg); + const response = []; + function feedCb(varbinds) { + for (let i = 0; i < varbinds.length; i++) { + if (SNMP.isVarbindError(varbinds[i])) { + node.error(SNMP.varbindError(varbinds[i]), msg); + } else { + response.push({ oid: varbinds[i].oid, value: varbinds[i].value }); + } } } - } - - this.on("input", function (msg) { - node.msg = msg; - var oids = node.oids || msg.oid; - var host = node.host || msg.host; - var community = node.community || msg.community; if (oids) { msg.oid = oids; - getSession(host, community, node.version, node.timeout).walk(msg.oid, maxRepetitions, feedCb, function (error) { + let sess = openSession(sessionid, host, user, options); + sess.on("error", function (err) { + node.error(err, msg); + }) + sess.walk(msg.oid, maxRepetitions, feedCb, function (error) { if (error) { node.error(error.toString(), msg); - } - else { - // Clone the array - msg.payload = response.slice(0); + } else { + msg.payload = response; node.send(msg); - //Clears response - response.length = 0; } + closeSession(sessionid); }); - } - else { + } else { node.warn("No oid to search for"); } }); } - RED.nodes.registerType("snmp walker", SnmpWalkerNode); + RED.nodes.registerType("snmp walker", SnmpWalkerNode, { + credentials: { + username: { type: "text" }, + authkey: { type: "password" }, + privkey: { type: "password" } + } + }); }; diff --git a/io/stomp/18-stomp.js b/io/stomp/18-stomp.js index 8689a1e0..a35da1a7 100644 --- a/io/stomp/18-stomp.js +++ b/io/stomp/18-stomp.js @@ -57,6 +57,10 @@ module.exports = function(RED) { node.warn("reconnecting"); }); + node.client.on("reconnect", function() { + node.status({fill:"green",shape:"dot",text:"connected"}); + }); + node.client.on("error", function(error) { node.status({fill:"grey",shape:"dot",text:"error"}); node.warn(error); @@ -124,6 +128,10 @@ module.exports = function(RED) { node.warn("reconnecting"); }); + node.client.on("reconnect", function() { + node.status({fill:"green",shape:"dot",text:"connected"}); + }); + node.client.on("error", function(error) { node.status({fill:"grey",shape:"dot",text:"error"}); node.warn(error); diff --git a/io/stomp/package.json b/io/stomp/package.json index ccec0f5f..1f1d1329 100644 --- a/io/stomp/package.json +++ b/io/stomp/package.json @@ -1,6 +1,6 @@ { "name" : "node-red-node-stomp", - "version" : "0.0.12", + "version" : "0.0.14", "description" : "A Node-RED node to publish and subscribe to/from a Stomp server", "dependencies" : { "stomp-client" : "^0.9.0" @@ -19,7 +19,7 @@ }, "author": { "name": "Dave Conway-Jones", - "email": "ceejay@vnet.ibm.com", + "email": "dceejay@gmail.com", "url": "http://nodered.org" } } diff --git a/package.json b/package.json index 9c59c78c..f4cff818 100644 --- a/package.json +++ b/package.json @@ -33,29 +33,29 @@ "devDependencies": { "exif": "^0.6.0", "feedparser": "^2.2.10", - "grunt": "^1.4.1", + "grunt": "^1.6.1", "grunt-cli": "^1.4.3", "grunt-contrib-jshint": "^2.1.0", "grunt-jscs": "^3.0.1", "grunt-lint-inline": "^1.0.0", "grunt-simple-mocha": "^0.4.1", "imap": "^0.8.19", - "mailparser": "^3.4.0", - "markdown-it": "^12.3.0", + "mailparser": "^3.6.4", + "markdown-it": "^12.3.2", "mocha": "~6.2.3", "msgpack-lite": "^0.1.26", "multilang-sentiment": "^1.2.0", "ngeohash": "^0.6.3", - "node-red": "^2.1.4", - "node-red-node-test-helper": "^0.2.7", - "nodemailer": "^6.7.2", - "poplib": "^0.1.7", + "node-red": "^3.0.2", + "node-red-node-test-helper": "^0.3.0", + "nodemailer": "^6.9.1", + "node-pop3": "^0.8.0", "proxyquire": "^2.1.3", "pushbullet": "^2.4.0", "sentiment": "^2.1.0", "should": "^13.2.3", "sinon": "~7.5.0", - "smtp-server": "^3.10.0", + "smtp-server": "^3.11.0", "supertest": "^4.0.2", "when": "^3.7.8" }, diff --git a/parsers/cbor/70-cbor.html b/parsers/cbor/70-cbor.html new file mode 100644 index 00000000..8367e80e --- /dev/null +++ b/parsers/cbor/70-cbor.html @@ -0,0 +1,44 @@ + + + + + + diff --git a/parsers/cbor/70-cbor.js b/parsers/cbor/70-cbor.js new file mode 100644 index 00000000..abed6dbf --- /dev/null +++ b/parsers/cbor/70-cbor.js @@ -0,0 +1,38 @@ + +module.exports = function(RED) { + "use strict"; + var cbor = require('cbor-x'); + + function CborNode(n) { + RED.nodes.createNode(this,n); + this.property = n.property||"payload"; + var node = this; + this.on("input", function(msg) { + var value = RED.util.getMessageProperty(msg,node.property); + if (value !== undefined) { + if (Buffer.isBuffer(value)) { + var l = value.length; + try { + value = cbor.decode(value); + RED.util.setMessageProperty(msg,node.property,value); + node.send(msg); + node.status({text:l +" b->o "+ JSON.stringify(value).length}); + } + catch (e) { + node.error("Bad decode",msg); + node.status({text:"not a cbor buffer"}); + } + } + else { + var le = JSON.stringify(value).length; + value = cbor.encode(value); + RED.util.setMessageProperty(msg,node.property,value); + node.send(msg); + node.status({text:le +" o->b "+ value.length}); + } + } + else { node.warn("No payload found to process"); } + }); + } + RED.nodes.registerType("cbor",CborNode); +} diff --git a/parsers/cbor/LICENSE b/parsers/cbor/LICENSE new file mode 100644 index 00000000..f5b60114 --- /dev/null +++ b/parsers/cbor/LICENSE @@ -0,0 +1,14 @@ +Copyright 2016 JS Foundation and other contributors, https://js.foundation/ +Copyright 2013-2016 IBM Corp. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/parsers/cbor/README.md b/parsers/cbor/README.md new file mode 100644 index 00000000..d0c7fbf9 --- /dev/null +++ b/parsers/cbor/README.md @@ -0,0 +1,27 @@ +node-red-node-cbor +================== + +A Node-RED node to pack and unpack objects to cbor format buffers. + +Install +------- + +Run the following command in your Node-RED user directory - typically `~/.node-red` + + npm install node-red-node-cbor + +Changes +------- + +Version 1.0.0 - move to cbor-x library (more supported and faster). +Usage +----- + +Uses the cbor-x npm to pack and unpack msg.payload objects to cbor format buffers. + +**Note**: this node does not currently encode raw buffer types. +It will automatically try to *decode* any buffer received, and may not cause an error. + +If the input is NOT a buffer it converts it into a msgpack buffer. + +If the input is a msgpack buffer it converts it back to the original type. diff --git a/parsers/cbor/icons/parser-cbor.png b/parsers/cbor/icons/parser-cbor.png new file mode 100644 index 00000000..02b5ddc0 Binary files /dev/null and b/parsers/cbor/icons/parser-cbor.png differ diff --git a/parsers/cbor/package.json b/parsers/cbor/package.json new file mode 100644 index 00000000..ca9cfafd --- /dev/null +++ b/parsers/cbor/package.json @@ -0,0 +1,32 @@ +{ + "name" : "node-red-node-cbor", + "version" : "1.0.1", + "description" : "A Node-RED node to pack and unpack objects to cbor format", + "dependencies" : { + "cbor-x" : "^1.3.2" + }, + "bundledDependencies": [ + "cbor-x" + ], + "repository" : { + "type":"git", + "url":"https://github.com/node-red/node-red-nodes.git", + "directory": "tree/master/parsers/cbor" + }, + "license": "Apache-2.0", + "keywords": [ "node-red", "cbor" ], + "node-red" : { + "version": ">=1.0.0", + "nodes" : { + "cbor": "70-cbor.js" + } + }, + "author": { + "name": "Dave Conway-Jones", + "email": "dceejay@gmail.com", + "url": "http://nodered.org" + }, + "engines": { + "node": ">=14" + } +} diff --git a/parsers/markdown/package.json b/parsers/markdown/package.json index 7dd44f2c..c0320a10 100644 --- a/parsers/markdown/package.json +++ b/parsers/markdown/package.json @@ -1,9 +1,9 @@ { "name": "node-red-node-markdown", - "version": "0.3.0", + "version": "0.4.0", "description": "A Node-RED node to convert a markdown string to html.", "dependencies": { - "markdown-it": "^12.3.2" + "markdown-it": "^13.0.1" }, "repository": { "type": "git", @@ -16,6 +16,7 @@ "markdown" ], "node-red": { + "version": ">=1.0.0", "nodes": { "markdown": "70-markdown.js" } @@ -24,5 +25,8 @@ "name": "Dave Conway-Jones", "email": "ceejay@vnet.ibm.com", "url": "http://nodered.org" + }, + "engines": { + "node": ">=14" } } diff --git a/social/email/61-email.html b/social/email/61-email.html index b6885ccc..d67fb52b 100644 --- a/social/email/61-email.html +++ b/social/email/61-email.html @@ -40,15 +40,32 @@
+ + +
+
-
+
+ + +
-
+
@@ -68,6 +85,9 @@ defaults: { server: {value:"smtp.gmail.com",required:true}, port: {value:"465",required:true}, + authtype: {value: "BASIC"}, + saslformat: {value: true}, + token: {value: "oauth2Response.access_token"}, secure: {value: true}, tls: {value: true}, name: {value:""}, @@ -92,11 +112,43 @@ return (this.dname)?"node_label_italic":""; }, oneditprepare: function() { + if (this.authtype === undefined) { + this.authtype = "BASIC"; + $("#node-input-authtype").val('BASIC'); + } + $("#node-input-authtype").change(function() { + var protocol = $("#node-input-authtype").val(); + if (protocol === "BASIC") { + $(".node-input-userid").show(); + $(".node-input-password").show(); + $(".node-input-saslformat").hide(); + $(".node-input-token").hide(); + $(".node-input-useTLS").show(); + } else if (protocol === "NONE") { + $(".node-input-userid").hide(); + $(".node-input-password").hide(); + $(".node-input-saslformat").hide(); + $(".node-input-token").hide(); + $(".node-input-useTLS").hide(); + } else { + $(".node-input-userid").show(); + $(".node-input-password").hide(); + $(".node-input-saslformat").show(); + $(".node-input-token").show(); + $("#node-input-fetch").val("trigger"); + $("#node-input-fetch").change(); + $(".node-input-useTLS").show(); + } + }); if (this.credentials.global) { $('#node-tip').show(); } else { $('#node-tip').hide(); } + $("#node-input-token").typedInput({ + type:'msg', + types:['msg'] + }); } }); })(); @@ -143,13 +195,29 @@
+ + +
+
-
+
+ +
@@ -222,6 +290,29 @@ } checkPorts(); }); + + $("#node-input-authtype").change(function() { + var protocol = $("#node-input-authtype").val(); + if (protocol === "BASIC") { + $(".node-input-userid").show(); + $(".node-input-password").show(); + $(".node-input-saslformat").hide(); + $(".node-input-token").hide(); + } else if (protocol === "NONE") { + $(".node-input-userid").hide(); + $(".node-input-password").hide(); + $(".node-input-saslformat").hide(); + $(".node-input-token").hide(); + } + else { + $(".node-input-userid").show(); + $(".node-input-password").hide(); + $(".node-input-saslformat").show(); + $(".node-input-token").show(); + $("#node-input-fetch").val("trigger"); + $("#node-input-fetch").change(); + } + }); @@ -237,6 +328,9 @@ useSSL: {value: true}, autotls: {value: "never"}, port: {value:"993",required:true}, + authtype: {value: "BASIC"}, + saslformat: {value: true}, + token: {value: "oauth2Response.access_token"}, box: {value:"INBOX"}, // For IMAP, The mailbox to process disposition: { value: "Read" }, // For IMAP, the disposition of the read email criteria: {value: "UNSEEN"}, @@ -263,6 +357,10 @@ }, oneditprepare: function() { var that = this; + if (this.authtype === undefined) { + this.authtype = "BASIC"; + $("#node-input-authtype").val('BASIC'); + } if (this.credentials.global) { $('#node-tip').show(); } else { @@ -289,6 +387,8 @@ else { $('#node-repeatTime').show(); that.inputs = 0; + $("#node-input-authtype").val("BASIC"); + $("#node-input-authtype").change(); } }); $("#node-input-criteria").change(function() { @@ -297,6 +397,10 @@ $("#node-input-fetch").change(); } }); + $("#node-input-token").typedInput({ + type:'msg', + types:['msg'] + }); } }); })(); @@ -306,7 +410,46 @@ + \ No newline at end of file diff --git a/social/email/61-email.js b/social/email/61-email.js index f1ac8a89..82361d23 100644 --- a/social/email/61-email.js +++ b/social/email/61-email.js @@ -1,10 +1,12 @@ /* eslint-disable indent */ +const { domainToUnicode } = require("url"); + /** * POP3 protocol - RFC1939 - https://www.ietf.org/rfc/rfc1939.txt * * Dependencies: - * * poplib - https://www.npmjs.com/package/poplib + * * 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 @@ -14,22 +16,17 @@ module.exports = function(RED) { "use strict"; var util = require("util"); var Imap = require('imap'); - var POP3Client = require("./poplib.js"); + 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."; } - try { - var globalkeys = RED.settings.email || require(process.env.NODE_RED_HOME+"/../emailkeys.js"); - } - catch(err) { - } - function EmailNode(n) { RED.nodes.createNode(this,n); this.topic = n.topic; @@ -38,26 +35,33 @@ module.exports = function(RED) { this.outport = n.port; this.secure = n.secure; this.tls = true; - var flag = false; + 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 (globalkeys) { - this.userid = globalkeys.user; - flag = true; + } + 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")); } } - if (this.credentials && this.credentials.hasOwnProperty("password")) { - this.password = this.credentials.password; - } else { - if (globalkeys) { - this.password = globalkeys.pass; - flag = true; + else if (this.authtype === "XOAUTH2") { + this.saslformat = n.saslformat; + if (n.token !== "") { + this.token = n.token; + } else { + this.error(RED._("email.errors.notoken")); } } - if (flag) { - RED.nodes.addCredentials(n.id,{userid:this.userid, password:this.password, global:true}); - } if (n.tls === false) { this.tls = false; } var node = this; @@ -68,12 +72,28 @@ module.exports = function(RED) { tls: {rejectUnauthorized: node.tls} } - if (this.userid && this.password) { + 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) { @@ -95,7 +115,8 @@ module.exports = function(RED) { sendopts.headers = msg.headers; sendopts.priority = msg.priority; } - sendopts.subject = msg.topic || msg.title || "Message from Node-RED"; // subject line + 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; @@ -116,7 +137,8 @@ module.exports = function(RED) { sendopts.attachments[0].contentType = msg.headers["content-type"]; } // Create some body text.. - sendopts.text = RED._("email.default-message",{filename:fname, description:(msg.description||"")}); + 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); @@ -173,6 +195,7 @@ module.exports = function(RED) { // Setup the EmailInNode function EmailInNode(n) { var imap; + var pop3; RED.nodes.createNode(this,n); this.name = n.name; @@ -187,43 +210,45 @@ module.exports = function(RED) { this.repeat = 1500; } if (this.inputs === 1) { this.repeat = 0; } - this.inserver = n.server || (globalkeys && globalkeys.server) || "imap.gmail.com"; - this.inport = n.port || (globalkeys && globalkeys.port) || "993"; + 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" - - var flag = false; + 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 (globalkeys) { - this.userid = globalkeys.user; - flag = true; - } else { - this.error(RED._("email.errors.nouserid")); - } } - if (this.credentials && this.credentials.hasOwnProperty("password")) { - this.password = this.credentials.password; - } else { - if (globalkeys) { - this.password = globalkeys.pass; - flag = true; - } else { + 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")); } } - if (flag) { - RED.nodes.addCredentials(n.id,{userid:this.userid, password:this.password, global:true}); + 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; - this.interval_id = null; + 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 @@ -252,103 +277,90 @@ module.exports = function(RED) { // 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; - //node.log("Checking POP3 for new messages"); + 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; - // 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 we have a next message to retrieve, ask to retrieve it otherwise issue a - // quit request. - function nextMessage() { - if (currentMessage > maxMessage) { - pop3Client.quit(); - setInputRepeatTimeout(); - return; - } - pop3Client.retr(currentMessage); - currentMessage++; - } // End of nextMessage - - pop3Client.on("stat", function(status, data) { - // Data contains: - // { - // count: - // octect: - // } - if (status) { - currentMessage = 1; - maxMessage = data.count; - nextMessage(); - } else { - node.log(util.format("stat error: %s %j", status, data)); - } + 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); - pop3Client.on("error", function(err) { + } 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(); - node.log("error: " + JSON.stringify(err)); - }); - - pop3Client.on("connect", function() { - //node.log("We are now connected"); - pop3Client.login(node.userid, node.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(); - 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(); + } - 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. - simpleParser(data, {}, 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); - } - }); - pop3Client.dele(msgNumber); - } - else { - node.log(util.format("retr error: %s %j", status, rawData)); - pop3Client.quit(); - setInputRepeatTimeout(); - } - }); - - 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 @@ -358,7 +370,50 @@ module.exports = function(RED) { // Check the email sever using the IMAP protocol for new messages. var s = false; var ss = false; - function checkIMAP(msg) { + 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; @@ -391,6 +446,7 @@ module.exports = function(RED) { imap.end(); s = false; setInputRepeatTimeout(); + done(err); return; } else { @@ -406,6 +462,7 @@ module.exports = function(RED) { imap.end(); s = false; setInputRepeatTimeout(); + done(err); return; } else { @@ -416,6 +473,8 @@ module.exports = function(RED) { imap.end(); s = false; setInputRepeatTimeout(); + msg.payload = 0; + done(); return; } @@ -453,11 +512,13 @@ module.exports = function(RED) { imap.end(); s = false; setInputRepeatTimeout(); + msg.payload = results.length; + done(); }; if (node.disposition === "Delete") { - imap.addFlags(results, "\Deleted", cleanup); + imap.addFlags(results, '\\Deleted', imap.expunge(cleanup) ); } else if (node.disposition === "Read") { - imap.addFlags(results, "\Seen", cleanup); + imap.addFlags(results, '\\Seen', cleanup); } else { cleanup(); } @@ -468,6 +529,7 @@ module.exports = function(RED) { imap.end(); s = false; setInputRepeatTimeout(); + done(err); }); } }); // End of imap->search @@ -477,6 +539,7 @@ module.exports = function(RED) { node.error(e.toString(),e); s = ss = false; imap.end(); + done(e); return; } } @@ -485,6 +548,7 @@ module.exports = function(RED) { node.error(RED._("email.errors.bad_criteria"),msg); s = ss = false; imap.end(); + done(); return; } } @@ -496,42 +560,20 @@ module.exports = function(RED) { // Perform a check of the email inboxes using either POP3 or IMAP - function checkEmail(msg) { + function checkEmail(msg,send,done) { if (node.protocol === "POP3") { - checkPOP3(msg); + checkPOP3(msg,send,done); } else if (node.protocol === "IMAP") { - if (s === false && ss == false) { checkIMAP(msg); } + if (s === false && ss == false) { checkIMAP(msg,send,done); } } } // End of checkEmail - if (node.protocol === "IMAP") { - var tout = (node.repeat > 0) ? node.repeat - 500 : 15000; - 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") { - node.log(err); - s = false; - node.status({fill:"red",shape:"ring",text:"email.status.connecterror"}); - } - setInputRepeatTimeout(); - }); - } - - this.on("input", function(msg) { - checkEmail(msg); + node.on("input", function(msg, send, done) { + send = send || function() { node.send.apply(node,arguments) }; + checkEmail(msg,send,done); }); - this.on("close", function() { + node.on("close", function() { if (this.interval_id != null) { clearTimeout(this.interval_id); } @@ -542,6 +584,10 @@ module.exports = function(RED) { } }); + function status_clear() { + node.status({}); + } + function setInputRepeatTimeout() { // Set the repetition timer as needed if (!isNaN(node.repeat) && node.repeat > 0) { @@ -565,47 +611,81 @@ module.exports = function(RED) { function EmailMtaNode(n) { - RED.nodes.createNode(this,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.mta = new SMTPServer({ - secure: false, - logger: false, - disabledCommands: ['AUTH', 'STARTTLS'], - - 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); + 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; } } - callback(); - }); + 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); diff --git a/social/email/README.md b/social/email/README.md index 58192c4c..bac85577 100644 --- a/social/email/README.md +++ b/social/email/README.md @@ -9,7 +9,11 @@ Pre-requisite You will need valid email credentials for your email server. For GMail this may mean getting an application password if you have two-factor authentication enabled. -**Note :** Version 1.x of this node requires **Node.js v8** or newer. +For Exchange and Outlook 365 you must use OAuth2.0. + +**Notes **: + Version 2.x of this node required **Node.js v12** or newer. + Version 1.x of this node requires **Node.js v8** or newer. Install ------- @@ -26,6 +30,12 @@ GMail users If you are accessing GMail you may need to either enable an application password, or enable less secure access via your Google account settings.

+Office 365 users +---------------- + +If you are accessing Exchnage you will need to register an application through their platform and use OAuth2.0. +Details on how to do this can be found here. + Usage ----- @@ -42,6 +52,9 @@ If there is text/html then that is returned in `msg.html`. `msg.from` and Additionally `msg.header` contains the complete header object including **to**, **cc** and other potentially useful properties. +Modern authentication through OAuth2.0 is supported, but must be triggered by an incoming access token and +can only be automatically triggered upstream. + ### Output node Sends the `msg.payload` as an email, with a subject of `msg.topic`. diff --git a/social/email/locales/de/61-email.json b/social/email/locales/de/61-email.json index 545f1aa2..704dfb9e 100644 --- a/social/email/locales/de/61-email.json +++ b/social/email/locales/de/61-email.json @@ -36,7 +36,7 @@ "always": "immer", "rejectUnauthorised": "Überprüfen sie, ob das serverzertifikat gültig ist" }, - "default-message": "__description__\n\nDatei von Node-RED ist angehängt: __filename__", + "default-message": "\nDatei von Node-RED ist angehängt: __filename__", "tip": { "cred": "Hinweis: Berechtigungen von globaler emailkeys.js-Datei kopiert", "recent": "Tipp: Es wird nur die letzte E-Mail abgerufen", diff --git a/social/email/locales/en-US/61-email.html b/social/email/locales/en-US/61-email.html index aca1ea6b..393573e3 100644 --- a/social/email/locales/en-US/61-email.html +++ b/social/email/locales/en-US/61-email.html @@ -7,8 +7,17 @@

You may optionally set msg.from in the payload which will override the userid default value.

Gmail users

-

If you are accessing Gmail you may need to either enable an application password, - or enable less secure access via your Google account settings.

+

If you are accessing Gmail you may need to either enable an application password.

+

Authentication

+

When connecting to a SMTP server, two authentication types are available: Basic and XOAuth2.

+
    +
  • Basic: requires a username and password to be entered
  • +
  • XOAuth2: requires a username and a msg property to extract the access token
  • +
+

SASL Formatting:

+

SASL XOAuth2 tokens are created by combining the username and token, encoding it in base64, and passing it to the mail server in the following format:

+
base64("user=" + userName + "^Aauth=Bearer " + accessToken + "^A^A")
+

If the checkbox is unticked, flow creators can format the token themselves before passing it to the node.

Details

The payload can be html format. You may supply a separate plaintext version using msg.plaintext. If you don't and msg.payload contains html, it will also be used for the plaintext. @@ -26,43 +35,78 @@ diff --git a/social/email/locales/en-US/61-email.json b/social/email/locales/en-US/61-email.json index 9d876117..d825cb44 100644 --- a/social/email/locales/en-US/61-email.json +++ b/social/email/locales/en-US/61-email.json @@ -31,12 +31,24 @@ "unflagged": "Unflagged", "unseen": "Unseen", "autotls": "Start TLS?", + "authtype": "Auth type", + "saslformat": "Format to SASL", + "token": "Token", "never": "never", "required": "if required", "always": "always", - "rejectUnauthorised": "Check server certificate is valid" + "rejectUnauthorised": "Check server certificate is valid", + "enableSecure": "Secure connection", + "enableStarttls": "Start TLS", + "starttlsUpgrade": "Upgrade cleartext connection with STARTTLS", + "certFile": "Certificate", + "keyFile":"Private key", + "users": "Users", + "auth": "Authenticate users", + "addButton": "Add", + "expert": "Expert" }, - "default-message": "__description__\n\nFile from Node-RED is attached: __filename__", + "default-message": "\nFile from Node-RED is attached: __filename__", "tip": { "cred": "Note: Copied credentials from global emailkeys.js file.", "recent": "Tip: Only retrieves the single most recent email.", @@ -60,6 +72,7 @@ "errors": { "nouserid": "No e-mail userid set", "nopassword": "No e-mail password set", + "notoken": "No token property set", "nocredentials": "No Email credentials found. See info panel.", "nosmtptransport": "No SMTP transport. See info panel.", "nopayload": "No payload to send", diff --git a/social/email/locales/ja/61-email.json b/social/email/locales/ja/61-email.json index c4dbc1c4..82c7bcc7 100644 --- a/social/email/locales/ja/61-email.json +++ b/social/email/locales/ja/61-email.json @@ -36,7 +36,7 @@ "always": "常時", "rejectUnauthorised": "チェックサーバ証明書は有効です" }, - "default-message": "__description__\n\nNode-REDからファイルが添付されました: __filename__", + "default-message": "\nNode-REDからファイルが添付されました: __filename__", "tip": { "cred": "注釈: emailkeys.jsファイルから認証情報をコピーしました。", "recent": "注釈: 最新のメールを1件のみ取得します。", diff --git a/social/email/package.json b/social/email/package.json index 62046064..2d98b4c6 100644 --- a/social/email/package.json +++ b/social/email/package.json @@ -1,15 +1,17 @@ { "name": "node-red-node-email", - "version": "1.15.1", + "version": "2.0.0", "description": "Node-RED nodes to send and receive simple emails.", "dependencies": { "imap": "^0.8.19", - "mailparser": "^3.4.0", - "nodemailer": "^6.7.3", - "smtp-server": "^3.10.0" + "node-pop3": "^0.8.0", + "mailparser": "^3.6.4", + "nodemailer": "^6.9.1", + "smtp-server": "^3.11.0" }, "bundledDependencies": [ "imap", + "node-pop3", "mailparser", "nodemailer", "smtp-server" @@ -26,9 +28,12 @@ "gmail", "imap", "pop", + "smtp", + "smtp-server", "mta" ], "node-red": { + "version": ">=1.0.0", "nodes": { "email": "61-email.js" } diff --git a/social/email/poplib.js b/social/email/poplib.js deleted file mode 100644 index 9fcaa80f..00000000 --- a/social/email/poplib.js +++ /dev/null @@ -1,859 +0,0 @@ -/* - - Node.js POP3 client library - - Copyright (C) 2011-2013 by Ditesh Shashikant Gathani - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -*/ - -var net = require("net"), - tls = require("tls"), - util = require("util"), - crypto = require("crypto"), - events = require("events"); - -// Constructor -function POP3Client(port, host, options) { - - if (options === undefined) { options = {}; } - - // Optional constructor arguments - var enabletls = options.enabletls !== undefined ? options.enabletls: false; - var ignoretlserrs = options.ignoretlserrs !== undefined ? options.ignoretlserrs: false; - var debug = options.debug || false; - - var tlsDirectOpts = options.tlsopts !== undefined ? options.tlsopts: {}; - - // Private variables follow - var self = this; - var response = null; - var checkResp = true; - var bufferedData = ""; - var state = 0; - var locked = false; - var multiline = false; - var socket = null; - var tlssock = null; - var callback = function(resp, data) { - - if (resp === false) { - - locked = false; - callback = function() {}; - self.emit("connect", false, data); - - } else { - - // Checking for APOP support - var banner = data.trim(); - var bannerComponents = banner.split(" "); - - for(var i=0; i < bannerComponents.length; i++) { - - if (bannerComponents[i].indexOf("@") > 0) { - - self.data["apop"] = true; - self.data["apop-timestamp"] = bannerComponents[i]; - break; - - } - } - - state = 1; - self.data["banner"] = banner; - self.emit("connect", true, data); - - } - }; - - // Public variables follow - this.data = { - - host: host, - port: port, - banner: "", - stls: false, - apop: false, - username: "", - tls: enabletls, - ignoretlserrs: ignoretlserrs - - }; - - // Privileged methods follow - this.setCallback = function(cb) { callback = cb; }; - this.getCallback = function() { return callback }; - this.setState = function(val) { state = val; }; - this.getState = function() { return state; }; - this.setLocked = function(val) { locked = val; }; - this.getLocked = function() { return locked; }; - this.setMultiline = function(val) { multiline = val; }; - this.getMultiline = function() { return multiline; }; - - // Writes to remote server socket - this.write = function(command, argument) { - - var text = command; - - if (argument !== undefined) { text = text + " " + argument + "\r\n"; } - else { text = text + "\r\n"; } - - if (debug) { console.log("Client: " + util.inspect(text)); } - - socket.write(text); - - }; - - // Kills the socket connection - this.end = function() { - socket.end(); - }; - - // Upgrades a standard unencrypted TCP connection to use TLS - // Liberally copied and modified from https://gist.github.com/848444 - // starttls() should be a private function, but I can't figure out - // how to get a public prototypal method (stls) to talk to private method (starttls) - // which references private variables without going through a privileged method - this.starttls = function(options) { - - var s = socket; - s.removeAllListeners("end"); - s.removeAllListeners("data"); - s.removeAllListeners("error"); - socket=null; - - var sslcontext = require('crypto').createCredentials(options); - var pair = tls.createSecurePair(sslcontext, false); - var cleartext = pipe(pair); - - pair.on('secure', function() { - - var verifyError = pair.ssl.verifyError(); - cleartext.authorized = true; - - if (verifyError) { - - cleartext.authorized = false; - cleartext.authorizationError = verifyError; - - } - - cleartext.on("data", onData); - cleartext.on("error", onError); - cleartext.on("end", onEnd); - socket=cleartext; - (self.getCallback())(cleartext.authorized, cleartext.authorizationError); - - }); - - cleartext._controlReleased = true; - - function pipe(pair) { - - pair.encrypted.pipe(s); - s.pipe(pair.encrypted); - - pair.fd = s.fd; - var cleartext = pair.cleartext; - cleartext.socket = s; - cleartext.encrypted = pair.encrypted; - cleartext.authorized = false; - - function onerror(e) { - if (cleartext._controlReleased) { cleartext.emit('error', e); } - } - - function onclose() { - s.removeListener('error', onerror); - s.removeListener('close', onclose); - } - - s.on('error', onerror); - s.on('close', onclose); - return cleartext; - - } - }; - - // Private methods follow - // Event handlers follow - function onData(data) { - - data = data.toString("ascii"); - bufferedData += data; - - if (debug) { console.log("Server: " + util.inspect(data)); } - - if (checkResp === true) { - - if (bufferedData.substr(0, 3) === "+OK") { - - checkResp = false; - response = true; - - } else if (bufferedData.substr(0, 4) === "-ERR") { - - checkResp = false; - response = false; - - // The following is only used for SASL - } else if (multiline === false) { - - checkResp = false; - response = true; - - } - } - - if (checkResp === false) { - - if (multiline === true && (response === false || bufferedData.substr(bufferedData.length-5) === "\r\n.\r\n")) { - - // Make a copy to avoid race conditions - var responseCopy = response; - var bufferedDataCopy = bufferedData; - - response = null; - checkResp = true; - multiline = false; - bufferedData = ""; - - callback(responseCopy, bufferedDataCopy); - - } else if (multiline === false) { - - // Make a copy to avoid race conditions - var responseCopy = response; - var bufferedDataCopy = bufferedData; - - response = null; - checkResp = true; - multiline = false; - bufferedData = ""; - - callback(responseCopy, bufferedDataCopy); - - } - } - } - - function onError(err) { - if (err.errno === 111) { self.emit("connect", false, err); } - else { self.emit("error", err); } - } - - function onEnd(data) { - self.setState(0); - socket = null; - } - - function onClose() { - self.emit("close"); - } - - // Constructor code follows - // Set up EventEmitter constructor function - events.EventEmitter.call(this); - - // Remote end socket - if (enabletls === true) { - - tlssock = tls.connect({ - host: host, - port: port, - rejectUnauthorized: !self.data.ignoretlserrs - }, function() { - - if (tlssock.authorized === false && self.data["ignoretlserrs"] === false) { - self.emit("tls-error", tlssock.authorizationError); - } - - } - ); - - socket = tlssock; - - } else { socket = new net.createConnection(port, host); } - - // Set up event handlers - socket.on("data", onData); - socket.on("error", onError); - socket.on("end", onEnd); - socket.on("close", onClose); - -} - -util.inherits(POP3Client, events.EventEmitter); - -POP3Client.prototype.login = function (username, password) { - - var self = this; - - if (self.getState() !== 1) { self.emit("invalid-state", "login"); } - else if (self.getLocked() === true) { self.emit("locked", "login"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - if (resp === false) { - - self.setLocked(false); - self.setCallback(function() {}); - self.emit("login", false, data); - - } else { - - self.setCallback(function(resp, data) { - - self.setLocked(false); - self.setCallback(function() {}); - - if (resp !== false) { self.setState(2); } - self.emit("login", resp, data); - - }); - - self.setMultiline(false); - self.write("PASS", password); - - } - }); - - self.setMultiline(false); - self.write("USER", username); - - } -}; - -// SASL AUTH implementation -// Currently supports SASL PLAIN and CRAM-MD5 -POP3Client.prototype.auth = function (type, username, password) { - - type = type.toUpperCase(); - var self = this; - var types = {"PLAIN": 1, "CRAM-MD5": 1}; - var initialresp = ""; - - if (self.getState() !== 1) { self.emit("invalid-state", "auth"); } - else if (self.getLocked() === true) { self.emit("locked", "auth"); } - - if ((type in types) === false) { - - self.emit("auth", false, "Invalid auth type", null); - return; - - } - - function tlsok() { - - if (type === "PLAIN") { - - initialresp = " " + new Buffer(username + "\u0000" + username + "\u0000" + password).toString("base64") + "="; - self.setCallback(function(resp, data) { - - if (resp !== false) { self.setState(2); } - self.emit("auth", resp, data, data); - - }); - - } else if (type === "CRAM-MD5") { - - self.setCallback(function(resp, data) { - - if (resp === false) { self.emit("auth", resp, "Server responded -ERR to AUTH CRAM-MD5", data); } - else { - - var challenge = new Buffer(data.trim().substr(2), "base64").toString(); - var hmac = crypto.createHmac("md5", password); - var response = new Buffer(username + " " + hmac.update(challenge).digest("hex")).toString("base64"); - - self.setCallback(function(resp, data) { - - var errmsg = null; - - if (resp !== false) { self.setState(2); } - else {errmsg = "Server responded -ERR to response"; } - - self.emit("auth", resp, null, data); - - }); - - self.write(response); - - } - }); - } - - self.write("AUTH " + type + initialresp); - - } - - if (self.data["tls"] === false && self.data["stls"] === false) { - - // Remove all existing STLS listeners - self.removeAllListeners("stls"); - - self.on("stls", function(resp, rawdata) { - - if (resp === false) { - - // We (optionally) ignore self signed cert errors, - // in blatant violation of RFC 2595, Section 2.4 - if (self.data["ignoretlserrs"] === true && rawdata === "DEPTH_ZERO_SELF_SIGNED_CERT"){ tlsok(); } - else { self.emit("auth", false, "Unable to upgrade connection to STLS", rawdata); } - - } else { tlsok(); } - - }); - - self.stls(); - - } else { tlsok(); } -}; - -POP3Client.prototype.apop = function (username, password) { - - var self = this; - - if (self.getState() !== 1) { self.emit("invalid-state", "apop"); } - else if (self.getLocked() === true) { self.emit("locked", "apop"); } - else if (self.data["apop"] === false) { self.emit("apop", false, "APOP support not detected on remote server"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - self.setLocked(false); - self.setCallback(function() {}); - - if (resp === true) { self.setState(2); } - self.emit("apop", resp, data); - - }); - - self.setMultiline(false); - self.write("APOP", username + " " + crypto.createHash("md5").update(self.data["apop-timestamp"] + password).digest("hex")); - - } -}; - -POP3Client.prototype.stls = function() { - - var self = this; - - if (self.getState() !== 1) { self.emit("invalid-state", "stls"); } - else if (self.getLocked() === true) { self.emit("locked", "stls"); } - else if (self.data["tls"] === true) { self.emit("stls", false, "Unable to execute STLS as TLS connection already established"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - self.setLocked(false); - self.setCallback(function() {}); - - if (resp === true) { - - self.setCallback(function(resp, data) { - - if (resp === false && self.data["ignoretlserrs"] === true && data === "DEPTH_ZERO_SELF_SIGNED_CERT") {resp = true; } - self.data["stls"] = true; - self.emit("stls", resp, data); - - }); - - self.starttls(); - - } else { self.emit("stls", false, data); } - }); - - self.setMultiline(false); - self.write("STLS"); - - } -}; - - -POP3Client.prototype.top = function(msgnumber, lines) { - - var self = this; - - if (self.getState() !== 2) { self.emit("invalid-state", "top"); } - else if (self.getLocked() === true) { self.emit("locked", "top"); } - else { - - self.setCallback(function(resp, data) { - - var returnValue = null; - self.setLocked(false); - self.setCallback(function() {}); - - if (resp !== false) { - - returnValue = ""; - var startOffset = data.indexOf("\r\n", 0) + 2; - var endOffset = data.indexOf("\r\n.\r\n", 0) + 2; - - if (endOffset > startOffset) {returnValue = data.substr(startOffset, endOffset-startOffset); } - - } - - self.emit("top", resp, msgnumber, returnValue, data); - - }); - - self.setMultiline(true); - self.write("TOP", msgnumber + " " + lines); - - } -}; - -POP3Client.prototype.list = function(msgnumber) { - - var self = this; - - if (self.getState() !== 2) { self.emit("invalid-state", "list"); } - else if (self.getLocked() === true) { self.emit("locked", "list"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - var returnValue = null; - var msgcount = 0; - self.setLocked(false); - self.setCallback(function() {}); - - if (resp !== false) { - - returnValue = []; - var listitem = ""; - if (msgnumber !== undefined) { - - msgcount = 1 - listitem = data.split(" "); - returnValue[listitem[1]] = listitem[2]; - - } else { - - var offset = 0; - var newoffset = 0; - var returnValue = []; - var startOffset = data.indexOf("\r\n", 0) + 2; - var endOffset = data.indexOf("\r\n.\r\n", 0) + 2; - - if (endOffset > startOffset) { - - data = data.substr(startOffset, endOffset-startOffset); - - while(true) { - - if (offset > endOffset) { break; } - - newoffset = data.indexOf("\r\n", offset); - - if (newoffset < 0) { break; } - - msgcount++; - listitem = data.substr(offset, newoffset-offset); - listitem = listitem.split(" "); - returnValue[listitem[0]] = listitem[1]; - offset = newoffset + 2; - - } - } - } - } - - self.emit("list", resp, msgcount, msgnumber, returnValue, data); - - }); - - if (msgnumber !== undefined) { self.setMultiline(false); } - else { self.setMultiline(true); } - - self.write("LIST", msgnumber); - - } -}; - -POP3Client.prototype.stat = function() { - - var self = this; - - if (self.getState() !== 2) { self.emit("invalid-state", "stat"); } - else if (self.getLocked() === true) { self.emit("locked", "stat"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - var returnValue = null; - self.setLocked(false); - self.setCallback(function() {}); - - if (resp !== false) { - - var listitem = data.split(" "); - returnValue = { - - "count": listitem[1].trim(), - "octets": listitem[2].trim(), - - }; - } - - self.emit("stat", resp, returnValue, data); - - }); - - self.setMultiline(false); - self.write("STAT", undefined); - - } -}; - -POP3Client.prototype.uidl = function(msgnumber) { - - var self = this; - - if (self.getState() !== 2) { self.emit("invalid-state", "uidl"); } - else if (self.getLocked() === true) { self.emit("locked", "uidl"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - var returnValue = null; - self.setLocked(false); - self.setCallback(function() {}); - - if (resp !== false) { - - returnValue = []; - var listitem = ""; - if (msgnumber !== undefined) { - - listitem = data.split(" "); - returnValue[listitem[1]] = listitem[2].trim(); - - } else { - - var offset = 0; - - var newoffset = 0; - var returnValue = []; - var startOffset = data.indexOf("\r\n", 0) + 2; - var endOffset = data.indexOf("\r\n.\r\n", 0) + 2; - - if (endOffset > startOffset) { - - data = data.substr(startOffset, endOffset-startOffset); - endOffset -= startOffset; - - while (offset < endOffset) { - - newoffset = data.indexOf("\r\n", offset); - listitem = data.substr(offset, newoffset-offset); - listitem = listitem.split(" "); - returnValue[listitem[0]] = listitem[1]; - offset = newoffset + 2; - - } - } - } - } - - self.emit("uidl", resp, msgnumber, returnValue, data); - - }); - - if (msgnumber !== undefined) { self.setMultiline(false); } - else { self.setMultiline(true); } - - self.write("UIDL", msgnumber); - - } -}; - -POP3Client.prototype.retr = function(msgnumber) { - - var self = this; - - if (self.getState() !== 2) { self.emit("invalid-state", "retr"); } - else if (self.getLocked() === true) { self.emit("locked", "retr"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - var returnValue = null; - self.setLocked(false); - self.setCallback(function() {}); - - if (resp !== false) { - - var startOffset = data.indexOf("\r\n", 0) + 2; - var endOffset = data.indexOf("\r\n.\r\n", 0); - returnValue = data.substr(startOffset, endOffset-startOffset); - - } - - self.emit("retr", resp, msgnumber, returnValue, data); - - }); - - self.setMultiline(true); - self.write("RETR", msgnumber); - - } -}; - -POP3Client.prototype.dele = function(msgnumber) { - - var self = this; - - if (self.getState() !== 2) { self.emit("invalid-state", "dele"); } - else if (self.getLocked() === true) { self.emit("locked", "dele"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - self.setLocked(false); - self.setCallback(function() {}); - self.emit("dele", resp, msgnumber, data); - - }); - - self.setMultiline(false); - self.write("DELE", msgnumber); - - } -}; - -POP3Client.prototype.noop = function() { - - var self = this; - - if (self.getState() !== 2) { self.emit("invalid-state", "noop"); } - else if (self.getLocked() === true) { self.emit("locked", "noop"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - self.setLocked(false); - self.setCallback(function() {}); - self.emit("noop", resp, data); - - }); - - self.setMultiline(false); - self.write("NOOP", undefined); - - } -}; - -POP3Client.prototype.rset = function() { - - var self = this; - - if (self.getState() !== 2) { self.emit("invalid-state", "rset"); } - else if (self.getLocked() === true) { self.emit("locked", "rset"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - self.setLocked(false); - self.setCallback(function() {}); - self.emit("rset", resp, data); - - }); - - self.setMultiline(false); - self.write("RSET", undefined); - - } -}; - -POP3Client.prototype.capa = function() { - - var self = this; - - if (self.getState() === 0) { self.emit("invalid-state", "quit"); } - else if (self.getLocked() === true) { self.emit("locked", "capa"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - var returnValue = null; - self.setLocked(false); - self.setCallback(function() {}); - - if (resp === true) { - - var startOffset = data.indexOf("\r\n", 0) + 2; - var endOffset = data.indexOf("\r\n.\r\n", 0); - returnValue = data.substr(startOffset, endOffset-startOffset); - returnValue = returnValue.split("\r\n"); - - } - - self.emit("capa", resp, returnValue, data); - - }); - - self.setMultiline(true); - self.write("CAPA", undefined); - - } -}; - -POP3Client.prototype.quit = function() { - - var self = this; - - if (self.getState() === 0) { self.emit("invalid-state", "quit"); } - else if (self.getLocked() === true) { self.emit("locked", "quit"); } - else { - - self.setLocked(true); - self.setCallback(function(resp, data) { - - self.setLocked(false); - self.setCallback(function() {}); - - self.end(); - self.emit("quit", resp, data); - - }); - - self.setMultiline(false); - self.write("QUIT", undefined); - - } -}; - -module.exports = POP3Client; \ No newline at end of file diff --git a/social/feedparser/32-feedparse.html b/social/feedparser/32-feedparse.html index 8f61c04f..1a9c4770 100644 --- a/social/feedparser/32-feedparse.html +++ b/social/feedparser/32-feedparse.html @@ -7,6 +7,11 @@

+
+ + + +
@@ -20,7 +25,8 @@ defaults: { name: {value:""}, url: {value:"", required:true}, - interval: { value:15, required:true, validate:function(v) {return (!isNaN(parseInt(v)) && (parseInt(v) <= 35790))} } + interval: { value:15, required:true, validate:function(v) {return (!isNaN(parseInt(v)) && (parseInt(v) <= 35790))} }, + ignorefirst: { value:false } }, inputs:0, outputs:1, diff --git a/social/feedparser/32-feedparse.js b/social/feedparser/32-feedparse.js index aea5fef0..9feeeba8 100644 --- a/social/feedparser/32-feedparse.js +++ b/social/feedparser/32-feedparse.js @@ -11,11 +11,13 @@ module.exports = function(RED) { if (n.interval > 35790) { this.warn(RED._("feedparse.errors.invalidinterval")) } this.interval = (parseInt(n.interval)||15) * 60000; this.interval_id = null; + this.ignorefirst = n.ignorefirst || false; this.seen = {}; + this.donefirst = false; var node = this; var parsedUrl = url.parse(this.url); if (!(parsedUrl.host || (parsedUrl.hostname && parsedUrl.port)) && !parsedUrl.isUnix) { - node.error(RED._("feedparse.errors.invalidurl")); + node.error(RED._("feedparse.errors.invalidurl"),RED._("feedparse.errors.invalidurl")); } else { var getFeed = function() { @@ -33,19 +35,24 @@ module.exports = function(RED) { else { res.pipe(feedparser); } }); - feedparser.on('error', function(error) { node.error(error); }); + feedparser.on('error', function(error) { node.error(error,error); }); feedparser.on('readable', function () { var stream = this, article; while (article = stream.read()) { // jshint ignore:line if (!(article.guid in node.seen) || ( node.seen[article.guid] !== 0 && node.seen[article.guid] != article.date.getTime())) { - node.seen[article.guid] = article.date?article.date.getTime():0; + node.seen[article.guid] = article.date ? article.date.getTime() : 0; var msg = { topic: article.origlink || article.link, payload: article.description, article: article }; - node.send(msg); + if (node.ignorefirst === true && node.donefirst === false) { + // do nothing + } + else { + node.send(msg); + } } } }); @@ -53,7 +60,7 @@ module.exports = function(RED) { feedparser.on('meta', function (meta) {}); feedparser.on('end', function () {}); }; - node.interval_id = setInterval(function() { getFeed(); }, node.interval); + node.interval_id = setInterval(function() { node.donefirst = true; getFeed(); }, node.interval); getFeed(); } diff --git a/social/feedparser/locales/en-US/32-feedparse.json b/social/feedparser/locales/en-US/32-feedparse.json index 2a5fd943..c2be15ee 100644 --- a/social/feedparser/locales/en-US/32-feedparse.json +++ b/social/feedparser/locales/en-US/32-feedparse.json @@ -4,7 +4,8 @@ "label": { "feedurl": "Feed url", "refresh": "Refresh", - "minutes": "minutes" + "minutes": "minutes", + "ignorefirst": "Ignore any stories older than restart" }, "errors": { "badstatuscode": "error - Bad status code", diff --git a/social/feedparser/package.json b/social/feedparser/package.json index ca3d75cc..c729aa53 100644 --- a/social/feedparser/package.json +++ b/social/feedparser/package.json @@ -1,6 +1,6 @@ { "name": "node-red-node-feedparser", - "version": "0.2.2", + "version": "0.3.0", "description": "A Node-RED node to get RSS Atom feeds.", "dependencies": { "feedparser": "^2.2.10", @@ -15,7 +15,8 @@ "keywords": [ "node-red", "atom", - "rss" + "rss", + "feed" ], "node-red": { "nodes": { diff --git a/social/notify/package.json b/social/notify/package.json index 5344fb9c..7b332caa 100644 --- a/social/notify/package.json +++ b/social/notify/package.json @@ -1,9 +1,9 @@ { "name": "node-red-node-notify", - "version": "0.2.0", + "version": "0.3.0", "description": "A Node-RED node to send local popup Notify alerts", "dependencies": { - "node-notifier": "^5.4.5" + "node-notifier": "^10.0.1" }, "repository": { "type": "git", diff --git a/social/pushbullet/57-pushbullet.js b/social/pushbullet/57-pushbullet.js index 7e6f0ba1..b2068edb 100644 --- a/social/pushbullet/57-pushbullet.js +++ b/social/pushbullet/57-pushbullet.js @@ -221,8 +221,14 @@ module.exports = function(RED) { msg.payload = incoming.iden; } else if (incoming.type === 'sms_changed') { - msg.topic = "SMS: "+ incoming.notifications[0].title; - msg.payload = incoming.notifications[0].body; + if (incoming.notifications && incoming.notifications.length > 0) { + msg.topic = "SMS: "+ incoming.notifications[0].title; + msg.payload = incoming.notifications[0].body; + } + else { + msg.topic = "SMS: "; + msg.payload = ""; + } msg.message = incoming; } else { @@ -244,12 +250,11 @@ module.exports = function(RED) { function PushbulletOut(n) { RED.nodes.createNode(this, n); - var self = this; - this.title = n.title; this.chan = n.chan; this.pushtype = n.pushtype; this.pusher = null; + var self = this; var configNode; diff --git a/social/pushbullet/package.json b/social/pushbullet/package.json index 6054b93d..a7f2f337 100644 --- a/social/pushbullet/package.json +++ b/social/pushbullet/package.json @@ -1,6 +1,6 @@ { "name" : "node-red-node-pushbullet", - "version" : "0.0.17", + "version" : "0.0.19", "description" : "A Node-RED node to send alerts via Pushbullet", "dependencies" : { "pushbullet": "^2.4.0", diff --git a/social/pusher/icons/pusher.png b/social/pusher/icons/pusher.png index 04bc1635..9ec546d6 100644 Binary files a/social/pusher/icons/pusher.png and b/social/pusher/icons/pusher.png differ diff --git a/social/pusher/package.json b/social/pusher/package.json index 44d5d958..c09f007c 100644 --- a/social/pusher/package.json +++ b/social/pusher/package.json @@ -1,10 +1,10 @@ { "name": "node-red-node-pusher", - "version": "0.1.0", + "version": "1.0.0", "description": "A Node-RED node to send and receive messages using Pusher.com", "dependencies": { - "pusher": "^1.5.1", - "pusher-client": "^1.1.0" + "pusher": "^5.1.2", + "pusher-js": "^8.0.1" }, "repository": { "type": "git", @@ -25,5 +25,11 @@ "name": "Dave Conway-Jones", "email": "ceejay@vnet.ibm.com", "url": "http://nodered.org" - } + }, + "contributors": [ + { + "name": "Shaqaruden", + "email": "shaqaruden@gmail.com" + } + ] } diff --git a/social/pusher/pusher.html b/social/pusher/pusher.html index c34e41cf..967dfe9e 100644 --- a/social/pusher/pusher.html +++ b/social/pusher/pusher.html @@ -2,11 +2,11 @@ diff --git a/time/suncalc/locales/ru/79-suncalc.json b/time/suncalc/locales/ru/79-suncalc.json new file mode 100644 index 00000000..55368da6 --- /dev/null +++ b/time/suncalc/locales/ru/79-suncalc.json @@ -0,0 +1,33 @@ +{ + "sunrise": { + "label": { + "latitude": " Широта", + "longitude": " Долгота", + "start": " Начало", + "end": " Окончание", + "offset": " Смещение", + "name": " Имя" + }, + "nightEnd": "Утренние астрономические сумерки", + "nauticalDawn": "Утренние навигационные сумерки", + "dawn": "Рассвет, утренние гражданские сумерки", + "sunrise": "Восход", + "sunriseEnd": "Восход закончился", + "goldenHourEnd": "Окончание утреннего золотого часа", + "goldenHour": "Начало вечернего золотого часа", + "sunsetStart": "Начало заката", + "sunset": "Закат, начинаются вечерние гражданские сумерки", + "dusk": "Смеркается, начинаются вечерние астрономические сумерки", + "nauticalDusk": "Начинаются вечерние морские сумерки", + "night": "Достаточно темно для астрономии", + "start": " начало", + "mins": " мин", + "end": " конец", + "dayState": "день", + "nightState": "ночь", + "onePerMin": "раз в минуту", + "onse": "по событию", + "sunName": "Рассвет/закат" + + } +} diff --git a/time/suncalc/package.json b/time/suncalc/package.json index 826abe3b..ec5b574b 100644 --- a/time/suncalc/package.json +++ b/time/suncalc/package.json @@ -1,6 +1,6 @@ { "name" : "node-red-node-suncalc", - "version" : "1.0.1", + "version" : "1.1.0", "description" : "A Node-RED node to provide a signal at sunrise and sunset", "dependencies" : { "suncalc" : "^1.8.0" diff --git a/time/timeswitch/README.md b/time/timeswitch/README.md index 2f0558ef..c4d6a517 100644 --- a/time/timeswitch/README.md +++ b/time/timeswitch/README.md @@ -7,9 +7,11 @@ simple timeswitch node to schedule daily on/off events. Install ------- -Run the following command in your Node-RED user directory - typically `~/.node-red` +You can install by using the `Menu - Manage Palette` option, or running the following command in your +Node-RED user directory - typically `~/.node-red` - npm install node-red-node-timeswitch + cd ~/.node-red + npm i node-red-node-timeswitch Usage ----- @@ -24,9 +26,11 @@ or dusk, and negatively (-ve) for minutes before dawn or dusk.. The output emits a `msg.payload` of *1* or *0* every minute depending on whether the current time is during the selected on time or off time. -If you just need the transitions from 0->1 or 1->0 then follow this node with an RBE node. +If you just need the transitions from 0->1 or 1->0 then follow this node with a `filter (RBE)` node. You may also optionally specify a `msg.topic` if required. -**Note**: For a more complex version with more built-in options see Pete Scargill's -[node-red-contrib-bigtimer](http://flows.nodered.org/node/node-red-contrib-bigtimer) node. +**Note**: For a more complex version with more built-in options see +[node-red-contrib-bigtimer](http://flows.nodered.org/node/node-red-contrib-bigtimer) node, or +for multiple schedules and a nice visual interface to cron then use Steve's +[node-red-contrib-cron-plus](https://flows.nodered.org/node/node-red-contrib-cron-plus) node. diff --git a/time/timeswitch/locales/en-US/timeswitch.html b/time/timeswitch/locales/en-US/timeswitch.html index 4609435c..036a87f6 100644 --- a/time/timeswitch/locales/en-US/timeswitch.html +++ b/time/timeswitch/locales/en-US/timeswitch.html @@ -7,6 +7,6 @@ or negatively (-ve) for minutes earlier.

The output emits a msg.payload of 1 or 0 every minute depending on whether the current time is during the selected on time or off time.

-

If you just need the transitions from 0->1 or 1->0 then follow this node with an RBE node.

+

If you just need the transitions from 0->1 or 1->0 then follow this node with a filter (RBE) node.

You may also optionally specify a msg.topic if required.

diff --git a/time/timeswitch/package.json b/time/timeswitch/package.json index 0f9c9a51..71917d79 100644 --- a/time/timeswitch/package.json +++ b/time/timeswitch/package.json @@ -1,9 +1,9 @@ { "name" : "node-red-node-timeswitch", - "version" : "0.1.0", + "version" : "1.0.0", "description" : "A Node-RED node to provide a simple timeswitch to schedule daily on/off events.", "dependencies" : { - "spacetime": "^6.12.5", + "spacetime": "^7.4.0", "suncalc": "^1.8.0" }, "repository" : { @@ -25,7 +25,19 @@ }, "contributors": [ { - "name": "@pmacostapdi" + "name": "@dceejay" + }, + { + "name": "@pmacostapdi" + }, + { + "name": "@heikokue" + }, + { + "name": "@sammachin" + }, + { + "name": "@jdmallen" } ] } diff --git a/time/timeswitch/timeswitch.js b/time/timeswitch/timeswitch.js index 87802a8e..fc5e3c87 100644 --- a/time/timeswitch/timeswitch.js +++ b/time/timeswitch/timeswitch.js @@ -1,19 +1,18 @@ - -module.exports = function(RED) { +module.exports = function (RED) { "use strict"; var SunCalc = require('suncalc'); const spacetime = require("spacetime") + const SUNRISE_KEY = "sunrise"; + const SUNSET_KEY = "sunset"; function TimeswitchNode(n) { RED.nodes.createNode(this, n); this.lat = n.lat; this.lon = n.lon; - this.start = n.start || "sunrise"; - this.end = n.end || "sunset"; this.startt = n.starttime; this.endt = n.endtime; - this.duskoff = n.duskoff; - this.dawnoff = n.dawnoff; + this.sunriseOffset = n.dawnoff; + this.sunsetOffset = n.duskoff; this.mytopic = n.mytopic; this.timezone = n.timezone || "UTC"; @@ -24,6 +23,7 @@ module.exports = function(RED) { this.thu = n.thu; this.fri = n.fri; this.sat = n.sat; + this.jan = n.jan; this.feb = n.feb; this.mar = n.mar; @@ -38,118 +38,195 @@ module.exports = function(RED) { this.dec = n.dec; var node = this; - var ison = 0; - var newendtime = 0; - this.on("input", function(msg2) { - if (msg2.payload === "reset") { ison = 0; } + this.on("input", function () { + // current global time + const now = spacetime.now(); + const nowNative = now.toNativeDate(); - var timeOffset = spacetime(Date.now()).goto(this.timezone.toLowerCase()).timezone().current.offset * 60 * 60 * 1000; - var now = new Date(Date.now() + timeOffset); - var nowMillis = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), now.getUTCHours(), now.getUTCMinutes(), 0); - var midnightMillis = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 0, 0); - var today = Math.round((nowMillis - midnightMillis) / 60000) % 1440; - var starttime = Number(node.startt); - var endtime = Number(node.endt); + // all sun events for the given lat/long + const sunEvents = SunCalc.getTimes(nowNative, node.lat, node.lon); + let sunriseDateTime = spacetime(sunEvents[SUNRISE_KEY]).nearest("minute"); + let sunsetDateTime = spacetime(sunEvents[SUNSET_KEY]).nearest("minute"); - if ((starttime >= 5000) || (endtime == 5000) || (endtime == 6000)) { - var times = SunCalc.getTimes(now, node.lat, node.lon); - var startMillis = Date.UTC(times[node.start].getUTCFullYear(), times[node.start].getUTCMonth(), times[node.start].getUTCDate(), times[node.start].getUTCHours(), times[node.start].getUTCMinutes()); - var endMillis = Date.UTC(times[node.end].getUTCFullYear(), times[node.end].getUTCMonth(), times[node.end].getUTCDate(), times[node.end].getUTCHours(), times[node.end].getUTCMinutes()); - var dawn = ((startMillis - midnightMillis) / 60000) + Number(node.dawnoff); - var dusk = ((endMillis - midnightMillis) / 60000) + Number(node.duskoff); - if (starttime == 5000) { starttime = dawn; } - if (starttime == 6000) { starttime = dusk; } - if (endtime == 5000) { endtime = dawn; } - if (endtime == 6000) { endtime = dusk; } - if (RED.settings.verbose) { node.log("Dawn " + parseInt(dawn / 60) + ":" + dawn % 60 + " - Dusk " + parseInt(dusk / 60) + ":" + dusk % 60); } + // add optional sun event offset, if specified + sunriseDateTime = sunriseDateTime.add(Number(node.sunriseOffset), "minutes"); + sunsetDateTime = sunsetDateTime.add(Number(node.sunsetOffset), "minutes"); + + // check if sun event has already occurred today + if (now.isAfter(sunriseDateTime)) { + // get tomorrow's sunrise, since it'll be different + sunriseDateTime = spacetime(SunCalc.getTimes(now.add(1, "day").toNativeDate(), node.lat, node.lon)[SUNRISE_KEY]).nearest("minute"); + // add optional sun event offset, if specified (again) + sunriseDateTime = sunriseDateTime.add(Number(node.sunriseOffset), "minutes"); + } + if (now.isAfter(sunsetDateTime)) { + // get tomorrow's sunset, since it'll be different + sunsetDateTime = spacetime(SunCalc.getTimes(now.add(1, "day").toNativeDate(), node.lat, node.lon)[SUNSET_KEY]).nearest("minute"); + // add optional sun event offset, if specified (again) + sunsetDateTime = sunsetDateTime.add(Number(node.sunsetOffset), "minutes"); } - var proceed = 0; - switch (now.getDay()) { - case 0 : { if (node.sun) { proceed++; } break; } - case 1 : { if (node.mon) { proceed++; } break; } - case 2 : { if (node.tue) { proceed++; } break; } - case 3 : { if (node.wed) { proceed++; } break; } - case 4 : { if (node.thu) { proceed++; } break; } - case 5 : { if (node.fri) { proceed++; } break; } - case 6 : { if (node.sat) { proceed++; } break; } + // log sun events + if (RED.settings.verbose) { + node.log(`Sunrise ${sunriseDateTime.format("time")} - Sunset ${sunsetDateTime.format("time")} `); } - if (proceed) { - switch (now.getMonth()) { - case 0 : { if (node.jan) { proceed++; } break; } - case 1 : { if (node.feb) { proceed++; } break; } - case 2 : { if (node.mar) { proceed++; } break; } - case 3 : { if (node.apr) { proceed++; } break; } - case 4 : { if (node.may) { proceed++; } break; } - case 5 : { if (node.jun) { proceed++; } break; } - case 6 : { if (node.jul) { proceed++; } break; } - case 7 : { if (node.aug) { proceed++; } break; } - case 8 : { if (node.sep) { proceed++; } break; } - case 9 : { if (node.oct) { proceed++; } break; } - case 10: { if (node.nov) { proceed++; } break; } - case 11: { if (node.dec) { proceed++; } break; } + // apply selected timezone to selected times (not to sunrise/sunset-- those are based on lat/long) + const currentTimeZone = now.timezone(); + const selectedTimeZone = spacetime(now.epoch, this.timezone.toLowerCase()).timezone(); + + // handler function to convert minute strings (from