/* 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 = []; if (msgnumber !== undefined) { msgcount = 1 listitem = data.split(" "); returnValue[listitem[1]] = listitem[2]; } else { var offset = 0; var listitem = ""; 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) { 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 = []; if (msgnumber !== undefined) { listitem = data.split(" "); returnValue[listitem[1]] = listitem[2].trim(); } else { var offset = 0; var listitem = ""; 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;