/*

	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 = [];

				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;
