1
0
mirror of https://github.com/node-red/node-red-nodes.git synced 2023-10-10 13:36:58 +02:00
node-red-nodes/social/email/poplib.js
2022-01-03 17:33:14 +00:00

859 lines
23 KiB
JavaScript

/*
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;