mirror of
https://github.com/node-red/node-red-nodes.git
synced 2023-10-10 13:36:58 +02:00
XOAuth2 for Email-In node (#975)
* Update PULL_REQUEST_TEMPLATE.md * Add new UI elements to Email In node Locale for en-US Added Auth type and Token field to Email IN Dynamically appear based on selection * XOAUTH2 IMAP Minor UI changes. Exposing only XOAuth2. Picks up raw access token from input message specified. Only works for IMAP Token formatted by node for Exchange and GMail, won't work on other providers. Only works on trigger, not timer TODO: Add POP XOAUTH2 capability Add SMTP XOAUTH2 capability Add option to pass SASL XAOUTH2 token rather than raw OAUTH2 token * SASL Format Added checkbox to turn off SASL formatting if the user wants to do this themselves * XOAuth2 forces input Using XOauth2 forces triggered node, and automatic trigger sets auth to basic; XOAuth2 needs token from flow * Error reporting Password missing error only occurs if set to basic authentication. Token missing only occurs if set to XOAuth2. * Unit tests Make sure basic authentication is selected by default, and that an additional input is created and timed triggers are turned off for XOauth2 * Cleanup and README Remove old code, update readme * XOauth2 IMAP Release Prevent XOAuth2 being used for POP. Update PR Template. Updated help file. Bumped version to 1.19-beta * Update POP3 dependency Removed dependency to poplib.js, moved to node-pop3. Re-wrote checkPOP3 function asynchronously using the new library. Added some node.status changes to mimic IMAP behaviour. * XOAUTH2 POP3 Added checking for authentication type to allow XOauth2 tokens to be sent to POP server. Turned off UI restrictions for this functionality. * XOAUTH2 POP3 Release Updated help docs and version to reflect changes. * Add new UI elements to Email Out node Add option for XAouth2 for SMTP node * XOAUTH2 SMTP Exposing functionality for OAuth2 through Nodemailer. Added some error reporting if credentials are missing to match the Email-In node. * XOAUTH2 SMTP Release Updated help file to reflect changes. * Unit Tests for Email Out Modified tests to allow these unit tests to pass, but does not address the fault caused by the Node Test Helper - credentials only loaded after the flow has been loaded. --------- Co-authored-by: Dave Conway-Jones <dceejay@users.noreply.github.com>
This commit is contained in:
parent
497270ba74
commit
9a57958a1e
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
13
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -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)
|
||||
|
||||
<!--
|
||||
If you want to raise a pull-request with a new feature, or a refactoring
|
||||
@ -25,10 +25,13 @@ the [forum](https://discourse.nodered.org) or
|
||||
|
||||
<!-- Describe the nature of this change. What problem does it address? -->
|
||||
|
||||
Adds authentication option to the Email node (node-red-node-email) to use OAuth and XOAuth2
|
||||
********** This version: IMAP ONLY **********
|
||||
|
||||
## Checklist
|
||||
<!-- Put an `x` in the boxes that apply -->
|
||||
|
||||
- [ ] 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
|
||||
|
@ -49,7 +49,7 @@
|
||||
"node-red": "^2.1.4",
|
||||
"node-red-node-test-helper": "^0.2.7",
|
||||
"nodemailer": "^6.7.2",
|
||||
"poplib": "^0.1.7",
|
||||
"node-pop3": "^0.8.0",
|
||||
"proxyquire": "^2.1.3",
|
||||
"pushbullet": "^2.4.0",
|
||||
"sentiment": "^2.1.0",
|
||||
|
@ -40,13 +40,29 @@
|
||||
<span data-i18n="email.label.useSecureConnection"></span>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-authtype"><i class="fa fa-tasks"></i> <span data-i18n="email.label.authtype"></span></label>
|
||||
<select type="text" id="node-input-authtype">
|
||||
<option value="BASIC">Basic</option>
|
||||
<option value="XOAUTH2">XOAuth2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row node-input-userid">
|
||||
<label for="node-input-userid"><i class="fa fa-user"></i> <span data-i18n="email.label.userid"></span></label>
|
||||
<input type="text" id="node-input-userid">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-row node-input-password">
|
||||
<label for="node-input-password"><i class="fa fa-lock"></i> <span data-i18n="email.label.password"></span></label>
|
||||
<input type="password" id="node-input-password">
|
||||
</div>
|
||||
|
||||
<div class="form-row node-input-saslformat" style="display: none;">
|
||||
<label for="node-input-saslformat"><i class="fa fa-code"></i> <span data-i18n="email.label.saslformat"></span></label>
|
||||
<input type="checkbox" id="node-input-saslformat" style="width: auto;">
|
||||
</div>
|
||||
<div class="form-row node-input-token" style="display: none;">
|
||||
<label for="node-input-token"><i class="fa fa-lock"></i> <span data-i18n="email.label.token"></span></label>
|
||||
<input type="text" id="node-input-token" placeholder="oauth2Response.access_token">
|
||||
</div>
|
||||
<br/>
|
||||
<div class="form-row">
|
||||
<label for="node-input-useTLS"><i class="fa fa-lock"></i> <span data-i18n="email.label.useTLS"></label>
|
||||
@ -58,6 +74,22 @@
|
||||
<input type="text" id="node-input-dname" data-i18n="[placeholder]node-red:common.label.name">
|
||||
</div>
|
||||
<div class="form-tips" id="node-tip"><span data-i18n="[html]email.tip.cred"></span></div>
|
||||
<script>
|
||||
$("#node-input-authtype").change(function() {
|
||||
var protocol = $("#node-input-authtype").val();
|
||||
if (protocol === "BASIC") {
|
||||
$(".node-input-password").show();
|
||||
$(".node-input-saslformat").hide();
|
||||
$(".node-input-token").hide();
|
||||
} else {
|
||||
$(".node-input-password").hide();
|
||||
$(".node-input-saslformat").show();
|
||||
$(".node-input-token").show();
|
||||
$("#node-input-fetch").val("trigger");
|
||||
$("#node-input-fetch").change();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<script type="text/javascript">
|
||||
@ -68,6 +100,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:""},
|
||||
@ -97,6 +132,10 @@
|
||||
} else {
|
||||
$('#node-tip').hide();
|
||||
}
|
||||
$("#node-input-token").typedInput({
|
||||
type:'msg',
|
||||
types:['msg']
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
@ -143,13 +182,28 @@
|
||||
<input type="text" id="node-input-port" placeholder="993">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-authtype"><i class="fa fa-tasks"></i> <span data-i18n="email.label.authtype"></span></label>
|
||||
<select type="text" id="node-input-authtype">
|
||||
<option value="BASIC">Basic</option>
|
||||
<option value="XOAUTH2">XOAuth2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row node-input-userid">
|
||||
<label for="node-input-userid"><i class="fa fa-user"></i> <span data-i18n="email.label.userid"></span></label>
|
||||
<input type="text" id="node-input-userid">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-row node-input-password">
|
||||
<label for="node-input-password"><i class="fa fa-lock"></i> <span data-i18n="email.label.password"></span></label>
|
||||
<input type="password" id="node-input-password">
|
||||
</div>
|
||||
<div class="form-row node-input-saslformat" style="display: none;">
|
||||
<label for="node-input-saslformat"><i class="fa fa-code"></i> <span data-i18n="email.label.saslformat"></span></label>
|
||||
<input type="checkbox" id="node-input-saslformat" style="width: auto;">
|
||||
</div>
|
||||
<div class="form-row node-input-token" style="display: none;">
|
||||
<label for="node-input-token"><i class="fa fa-lock"></i> <span data-i18n="email.label.token"></span></label>
|
||||
<input type="text" id="node-input-token" placeholder="oauth2Response.access_token">
|
||||
</div>
|
||||
<div class="form-row node-input-box">
|
||||
<label for="node-input-box"><i class="fa fa-inbox"></i> <span data-i18n="email.label.folder"></span></label>
|
||||
<input type="text" id="node-input-box">
|
||||
@ -222,6 +276,21 @@
|
||||
}
|
||||
checkPorts();
|
||||
});
|
||||
|
||||
$("#node-input-authtype").change(function() {
|
||||
var protocol = $("#node-input-authtype").val();
|
||||
if (protocol === "BASIC") {
|
||||
$(".node-input-password").show();
|
||||
$(".node-input-saslformat").hide();
|
||||
$(".node-input-token").hide();
|
||||
} else {
|
||||
$(".node-input-password").hide();
|
||||
$(".node-input-saslformat").show();
|
||||
$(".node-input-token").show();
|
||||
$("#node-input-fetch").val("trigger");
|
||||
$("#node-input-fetch").change();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -237,6 +306,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"},
|
||||
@ -289,6 +361,8 @@
|
||||
else {
|
||||
$('#node-repeatTime').show();
|
||||
that.inputs = 0;
|
||||
$("#node-input-authtype").val("BASIC");
|
||||
$("#node-input-authtype").change();
|
||||
}
|
||||
});
|
||||
$("#node-input-criteria").change(function() {
|
||||
@ -297,6 +371,10 @@
|
||||
$("#node-input-fetch").change();
|
||||
}
|
||||
});
|
||||
$("#node-input-token").typedInput({
|
||||
type:'msg',
|
||||
types:['msg']
|
||||
});
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
@ -6,7 +6,7 @@ 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
|
||||
@ -16,7 +16,7 @@ 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;
|
||||
@ -42,20 +42,38 @@ module.exports = function(RED) {
|
||||
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 {
|
||||
this.error(RED._("email.errors.nouserid"));
|
||||
}
|
||||
}
|
||||
if(this.authtype === "BASIC" ) {
|
||||
if (this.credentials && this.credentials.hasOwnProperty("password")) {
|
||||
this.password = this.credentials.password;
|
||||
} else {
|
||||
if (globalkeys) {
|
||||
this.password = globalkeys.pass;
|
||||
flag = true;
|
||||
} else {
|
||||
this.error(RED._("email.errors.nopassword"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.saslformat = n.saslformat;
|
||||
if(n.token!=="") {
|
||||
this.token = n.token;
|
||||
} else {
|
||||
this.error(RED._("email.errors.notoken"));
|
||||
}
|
||||
}
|
||||
if (flag) {
|
||||
@ -71,11 +89,26 @@ 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);
|
||||
|
||||
@ -178,6 +211,7 @@ module.exports = function(RED) {
|
||||
// Setup the EmailInNode
|
||||
function EmailInNode(n) {
|
||||
var imap;
|
||||
var pop3;
|
||||
|
||||
RED.nodes.createNode(this,n);
|
||||
this.name = n.name;
|
||||
@ -200,6 +234,11 @@ module.exports = function(RED) {
|
||||
this.protocol = n.protocol || "IMAP";
|
||||
this.disposition = n.disposition || "None"; // "None", "Delete", "Read"
|
||||
this.criteria = n.criteria || "UNSEEN"; // "ALL", "ANSWERED", "FLAGGED", "SEEN", "UNANSWERED", "UNFLAGGED", "UNSEEN"
|
||||
this.authtype = n.authtype || "BASIC";
|
||||
if (this.authtype !== "BASIC") {
|
||||
this.inputs = 1;
|
||||
this.repeat = 0;
|
||||
}
|
||||
|
||||
var flag = false;
|
||||
|
||||
@ -213,6 +252,7 @@ module.exports = function(RED) {
|
||||
this.error(RED._("email.errors.nouserid"));
|
||||
}
|
||||
}
|
||||
if(this.authtype === "BASIC" ) {
|
||||
if (this.credentials && this.credentials.hasOwnProperty("password")) {
|
||||
this.password = this.credentials.password;
|
||||
} else {
|
||||
@ -223,6 +263,14 @@ module.exports = function(RED) {
|
||||
this.error(RED._("email.errors.nopassword"));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
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});
|
||||
}
|
||||
@ -257,74 +305,63 @@ 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,send,done) {
|
||||
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?
|
||||
);
|
||||
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);
|
||||
|
||||
// If we have a next message to retrieve, ask to retrieve it otherwise issue a
|
||||
// quit request.
|
||||
function nextMessage() {
|
||||
if (currentMessage > maxMessage) {
|
||||
pop3Client.quit();
|
||||
} else if(node.authtype == "BASIC") {
|
||||
await pop3.command('USER', node.userid);
|
||||
await pop3.command('PASS', node.password);
|
||||
}
|
||||
} catch(err) {
|
||||
node.error(err.message,err);
|
||||
node.status({fill:"red",shape:"ring",text:"email.status.connecterror"});
|
||||
setInputRepeatTimeout();
|
||||
done();
|
||||
return;
|
||||
}
|
||||
pop3Client.retr(currentMessage);
|
||||
currentMessage++;
|
||||
} // End of nextMessage
|
||||
|
||||
pop3Client.on("stat", function(status, data) {
|
||||
// Data contains:
|
||||
// {
|
||||
// count: <Number of messages to be read>
|
||||
// octect: <size of messages to be read>
|
||||
// }
|
||||
if (status) {
|
||||
currentMessage = 1;
|
||||
maxMessage = data.count;
|
||||
nextMessage();
|
||||
} else {
|
||||
node.log(util.format("stat error: %s %j", status, data));
|
||||
}
|
||||
});
|
||||
|
||||
pop3Client.on("error", function(err) {
|
||||
setInputRepeatTimeout();
|
||||
node.log("error: " + JSON.stringify(err));
|
||||
done();
|
||||
});
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
pop3Client.on("retr", function(status, msgNumber, data, rawData) {
|
||||
// node.log(util.format("retr: status=%s, msgNumber=%d, data=%j", status, msgNumber, data));
|
||||
if (status) {
|
||||
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(data, {}, function(err, parsed) {
|
||||
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"});
|
||||
@ -334,29 +371,24 @@ module.exports = function(RED) {
|
||||
processNewMessage(msg, parsed);
|
||||
}
|
||||
});
|
||||
pop3Client.dele(msgNumber);
|
||||
//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;
|
||||
}
|
||||
else {
|
||||
node.log(util.format("retr error: %s %j", status, rawData));
|
||||
pop3Client.quit();
|
||||
await pop3.DELE(currentMessage);
|
||||
currentMessage++;
|
||||
}
|
||||
await pop3.QUIT();
|
||||
node.status({fill:"green",shape:"dot",text:"finished"});
|
||||
setTimeout(status_clear, 5000);
|
||||
setInputRepeatTimeout();
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -367,6 +399,49 @@ module.exports = function(RED) {
|
||||
var s = false;
|
||||
var ss = false;
|
||||
function checkIMAP(msg,send,done) {
|
||||
var tout = (node.repeat > 0) ? node.repeat - 500 : 15000;
|
||||
var saslxoauth2 = "";
|
||||
if(node.authtype == "XOAUTH2") {
|
||||
var value = RED.util.getMessageProperty(msg,node.token);
|
||||
if (value !== undefined) {
|
||||
if(node.saslformat) {
|
||||
//Make base64 string for access - compatible with outlook365 and gmail
|
||||
saslxoauth2 = Buffer.from("user="+node.userid+"\x01auth=Bearer "+value+"\x01\x01").toString('base64');
|
||||
} else {
|
||||
saslxoauth2 = value;
|
||||
}
|
||||
}
|
||||
imap = new Imap({
|
||||
xoauth2: saslxoauth2,
|
||||
host: node.inserver,
|
||||
port: node.inport,
|
||||
tls: node.useSSL,
|
||||
autotls: node.autotls,
|
||||
tlsOptions: { rejectUnauthorized: false },
|
||||
connTimeout: tout,
|
||||
authTimeout: tout
|
||||
});
|
||||
} else {
|
||||
imap = new Imap({
|
||||
user: node.userid,
|
||||
password: node.password,
|
||||
host: node.inserver,
|
||||
port: node.inport,
|
||||
tls: node.useSSL,
|
||||
autotls: node.autotls,
|
||||
tlsOptions: { rejectUnauthorized: false },
|
||||
connTimeout: tout,
|
||||
authTimeout: tout
|
||||
});
|
||||
}
|
||||
imap.on('error', function(err) {
|
||||
if (err.errno !== "ECONNRESET") {
|
||||
s = false;
|
||||
node.error(err.message,err);
|
||||
node.status({fill:"red",shape:"ring",text:"email.status.connecterror"});
|
||||
}
|
||||
setInputRepeatTimeout();
|
||||
});
|
||||
//console.log("Checking IMAP for new messages");
|
||||
// We get back a 'ready' event once we have connected to imap
|
||||
s = true;
|
||||
@ -521,29 +596,6 @@ module.exports = function(RED) {
|
||||
}
|
||||
} // 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") {
|
||||
s = false;
|
||||
node.error(err.message,err);
|
||||
node.status({fill:"red",shape:"ring",text:"email.status.connecterror"});
|
||||
}
|
||||
setInputRepeatTimeout();
|
||||
});
|
||||
}
|
||||
|
||||
node.on("input", function(msg, send, done) {
|
||||
send = send || function() { node.send.apply(node,arguments) };
|
||||
checkEmail(msg,send,done);
|
||||
@ -560,6 +612,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) {
|
||||
|
@ -9,6 +9,8 @@ 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.
|
||||
|
||||
For Exchange and Outlook 365 you must use OAuth2.0.
|
||||
|
||||
**Note :** Version 1.x of this node requires **Node.js v8** or newer.
|
||||
|
||||
Install
|
||||
@ -26,6 +28,13 @@ GMail users
|
||||
If you are accessing GMail you may need to either enable <a target="_new" href="https://support.google.com/mail/answer/185833?hl=en">an application password</a>,
|
||||
or enable <a target="_new" href="https://support.google.com/accounts/answer/6010255?hl=en">less secure access</a> via your Google account settings.</p>
|
||||
|
||||
Office 365 users
|
||||
-----------
|
||||
|
||||
If you are accessing Exchnage you will need to register an application through their platform and use OAuth2.0.
|
||||
<a target="_new" href="https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#get-an-access-token">Details on how to do this can be found here.</a>
|
||||
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
@ -42,6 +51,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`.
|
||||
|
@ -8,6 +8,16 @@
|
||||
default value.</p>
|
||||
<h3>Gmail users</h3>
|
||||
<p>If you are accessing Gmail you may need to either enable <a target="_new" href="https://support.google.com/mail/answer/185833?hl=en">an application password</a>.</p>
|
||||
<h3>Authentication</h3>
|
||||
<p>When connecting to a SMTP server, two authentication types are available: Basic and XOAuth2.</p>
|
||||
<ul>
|
||||
<li><b>Basic:</b> requires a username and password to be entered</li>
|
||||
<li><b>XOAuth2:</b> requires a username and a <code>msg</code> property to extract the access token</li>
|
||||
</ul>
|
||||
<h3>SASL Formatting:</h3>
|
||||
<p>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:</p>
|
||||
<pre>base64("user=" + userName + "^Aauth=Bearer " + accessToken + "^A^A")</pre>
|
||||
<p>If the checkbox is unticked, flow creators can format the token themselves before passing it to the node.</p>
|
||||
<h3>Details</h3>
|
||||
<p>The payload can be html format. You may supply a separate plaintext version using <code>msg.plaintext</code>.
|
||||
If you don't and <code>msg.payload</code> contains html, it will also be used for the plaintext.
|
||||
@ -25,21 +35,32 @@
|
||||
</script>
|
||||
|
||||
<script type="text/html" data-help-name="e-mail in">
|
||||
<p>Repeatedly gets emails from a POP3 or IMAP server and forwards on as a msg if not already seen.</p>
|
||||
<p>The subject is loaded into <code>msg.topic</code> and <code>msg.payload</code> is the plain text body.
|
||||
If there is text/html then that is returned in <code>msg.html</code>. <code>msg.from</code> and <code>msg.date</code> are also set if you need them.</p>
|
||||
<p>Additionally <code>msg.header</code> contains the complete header object including
|
||||
<i>to</i>, <i>cc</i> and other potentially useful properties.</p>
|
||||
<p>It can optionally mark the message as Read (default), Delete it, or leave unmarked (None).</p>
|
||||
<p>Uses the <a href="https://github.com/mscdex/node-imap/blob/master/README.md" target="_new">node-imap module</a> - see that page for
|
||||
information on the <code>msg.criteria</code> format if needed.</p>
|
||||
<p>Any attachments supplied in the incoming email can be found in the <code>msg.attachments</code> property. This will be an array of objects where
|
||||
each object represents a specific attachments. The format of the object is:</p>
|
||||
<pre>
|
||||
{
|
||||
<h3>Overview</h3>
|
||||
<p>The e-mail in node retrieves emails from a POP3 or IMAP server and forwards the email data as a message if it has not already been seen.</p>
|
||||
|
||||
<h3>Message Properties</h3>
|
||||
<p>The following properties are set on the message object:</p>
|
||||
<ul>
|
||||
<li><code>msg.topic</code> - the subject of the email</li>
|
||||
<li><code>msg.payload</code> - the plain text body of the email</li>
|
||||
<li><code>msg.html</code> - the HTML body of the email (if present)</li>
|
||||
<li><code>msg.from</code> - the sender of the email</li>
|
||||
<li><code>msg.date</code> - the date the email was sent</li>
|
||||
<li><code>msg.header</code> - the complete header object including information such as the "to" and "cc" recipients</li>
|
||||
<li><code>msg.attachments</code> - an array of objects representing any attachments included in the email</li>
|
||||
</ul>
|
||||
|
||||
<h3>Module Used</h3>
|
||||
<p>The e-mail in node uses the <a href="https://github.com/mscdex/node-imap/blob/master/README.md" target="_new">node-imap module</a>, see that page for information on the <code>msg.criteria</code> format if needed.</p>
|
||||
<p>It also makes use of <a href="https://github.com/node-pop3/node-pop3#readme" target="_new">node-pop3 module</a></p>
|
||||
|
||||
<h3>Attachment Format</h3>
|
||||
<p>Each object in the <code>msg.attachments</code> array is formatted as follows:</p>
|
||||
<pre>
|
||||
{
|
||||
contentType: // The MIME content description
|
||||
fileName: // A suggested file name associated with this attachment
|
||||
transferEncoding: // How was the original email attachment encodded?
|
||||
transferEncoding: // How was the original email attachment encoded?
|
||||
contentDisposition: // Unknown
|
||||
generatedFileName: // A suggested file name associated with this attachment
|
||||
contentId: // A unique generated ID for this attachment
|
||||
@ -47,11 +68,26 @@
|
||||
length: // Size of data in bytes
|
||||
content: // The actual content of the data contained in a Node.js Buffer object
|
||||
// We can turn this into a base64 data string with content.toString('base64')
|
||||
}
|
||||
</pre>
|
||||
<p><b>Note</b>: For POP3, the default port numbers are 110 for plain TCP and 995 for SSL. For IMAP the port numbers are 143 for plain TCP and 993 for SSL.</p>
|
||||
<p><b>Note</b>: With option 'STARTTLS' an established plain connection is upgraded to an encrypted one. Set to 'always' to always attempt connection upgrades via STARTTLS, 'required' only if upgrading is required, or 'never' to never attempt upgrading.</p>
|
||||
<p><b>Note</b>: The maximum refresh interval is 2147483 seconds (24.8 days).</p>
|
||||
}
|
||||
</pre>
|
||||
<h3>Authentication</h3>
|
||||
<p>When connecting to a POP3 or IMAP server, two authentication types are available: Basic and XOAuth2.</p>
|
||||
<ul>
|
||||
<li><b>Basic:</b> requires a username and password to be entered</li>
|
||||
<li><b>XOAuth2:</b> requires a username and a <code>msg</code> property to extract the access token</li>
|
||||
</ul>
|
||||
<p>With XOAuth2 authentication, periodic fetching is not available. The node will only attemp to login when a new token is receieved.</p>
|
||||
<h3>SASL Formatting:</h3>
|
||||
<p>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:</p>
|
||||
<pre>base64("user=" + userName + "^Aauth=Bearer " + accessToken + "^A^A")</pre>
|
||||
<p>If the checkbox is unticked, flow creators can format the token themselves before passing it to the node.</p>
|
||||
|
||||
<h3>Notes</h3>
|
||||
<ul>
|
||||
<li>For POP3, the default port numbers are 110 for plain TCP and 995 for SSL. For IMAP the port numbers are 143 for plain TCP and 993 for SSL.</li>
|
||||
<li>With option 'STARTTLS' an established plain connection is upgraded to an encrypted one. Set to 'always' to always attempt connection upgrades via STARTTLS, 'required' only if upgrading is required, or 'never' to never attempt upgrading.</li>
|
||||
<li>The maximum refresh interval is 2147483 seconds (24.8 days).</li>
|
||||
</ul>
|
||||
|
||||
</script>
|
||||
|
||||
|
@ -31,6 +31,9 @@
|
||||
"unflagged": "Unflagged",
|
||||
"unseen": "Unseen",
|
||||
"autotls": "Start TLS?",
|
||||
"authtype": "Auth type",
|
||||
"saslformat": "Format to SASL",
|
||||
"token": "Token",
|
||||
"never": "never",
|
||||
"required": "if required",
|
||||
"always": "always",
|
||||
@ -69,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",
|
||||
|
@ -1,15 +1,17 @@
|
||||
{
|
||||
"name": "node-red-node-email",
|
||||
"version": "1.18.5",
|
||||
"version": "1.19.0",
|
||||
"description": "Node-RED nodes to send and receive simple emails.",
|
||||
"dependencies": {
|
||||
"imap": "^0.8.19",
|
||||
"node-pop3": "^0.8.0",
|
||||
"mailparser": "~3.5.0",
|
||||
"nodemailer": "^6.9.1",
|
||||
"smtp-server": "^3.11.0"
|
||||
},
|
||||
"bundledDependencies": [
|
||||
"imap",
|
||||
"node-pop3",
|
||||
"mailparser",
|
||||
"nodemailer",
|
||||
"smtp-server"
|
||||
|
@ -1,859 +0,0 @@
|
||||
/*
|
||||
|
||||
Node.js POP3 client library
|
||||
|
||||
Copyright (C) 2011-2013 by Ditesh Shashikant Gathani <ditesh@gathani.org>
|
||||
|
||||
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;
|
@ -31,6 +31,25 @@ describe('email Node', function () {
|
||||
n1.should.have.property("repeat", 300000);
|
||||
n1.should.have.property("inserver", "imap.gmail.com");
|
||||
n1.should.have.property("inport", "993");
|
||||
n1.should.have.property("authtype", "BASIC");
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should force input on XOAuth2', function (done) {
|
||||
var flow = [{
|
||||
id: "n1",
|
||||
type: "e-mail in",
|
||||
name: "emailin",
|
||||
authtype: "XOAUTH2",
|
||||
wires: [
|
||||
[]
|
||||
]
|
||||
}];
|
||||
helper.load(emailNode, flow, function () {
|
||||
var n1 = helper.getNode("n1");
|
||||
n1.should.have.property("repeat", 0);
|
||||
n1.should.have.property("inputs", 1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
@ -51,6 +70,7 @@ describe('email Node', function () {
|
||||
helper.load(emailNode, flow, function () {
|
||||
var n1 = helper.getNode("n1");
|
||||
n1.should.have.property('name', "emailout");
|
||||
n1.should.have.property("authtype", "BASIC");
|
||||
done();
|
||||
});
|
||||
});
|
||||
@ -83,7 +103,7 @@ describe('email Node', function () {
|
||||
//console.log(helper.log());
|
||||
//logEvents.should.have.length(3);
|
||||
logEvents[0][0].should.have.a.property('msg');
|
||||
logEvents[0][0].msg.toString().should.startWith("email.errors.nopayload");
|
||||
logEvents[2][0].msg.toString().should.startWith("email.errors.nopayload");
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
@ -134,7 +154,7 @@ describe('email Node', function () {
|
||||
// console.log(logEvents[0][0].msg.toString());
|
||||
//logEvents.should.have.length(3);
|
||||
logEvents[0][0].should.have.a.property('msg');
|
||||
logEvents[0][0].msg.toString().should.startWith("Error:");
|
||||
logEvents[2][0].msg.toString().should.startWith("Error:");
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
@ -179,7 +199,7 @@ describe('email Node', function () {
|
||||
//console.log(helper.log().args);
|
||||
//logEvents.should.have.length(3);
|
||||
logEvents[0][0].should.have.a.property('msg');
|
||||
logEvents[0][0].msg.toString().should.startWith("Error:");
|
||||
logEvents[2][0].msg.toString().should.startWith("Error:");
|
||||
done();
|
||||
} catch (e) {
|
||||
done(e);
|
||||
|
Loading…
Reference in New Issue
Block a user