/*
 * Created on Nov 4, 2004
 * 
 *  This file is part of susimail project, see http://susi.i2p/
 *  
 *  Copyright (C) 2004-2005  <susi23@mail.i2p>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *  
 * $Revision: 1.1 $
 */
package i2p.susi.webmail.pop3;

import i2p.susi.debug.Debug;
import i2p.susi.webmail.Messages;
import i2p.susi.webmail.NewMailListener;
import i2p.susi.webmail.WebMail;
import i2p.susi.util.Config;
import i2p.susi.util.ReadBuffer;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicLong;

import net.i2p.data.DataHelper;

/**
 * @author susi23
 */
public class POP3MailBox implements NewMailListener {

	private final String host, user, pass;

	private String lastLine, lastError;

	private final int port;
	private int mails;

	private boolean connected;
	private boolean gotCAPA;
	private boolean supportsPipelining;
	private boolean supportsTOP;
	private boolean supportsUIDL;

	/** ID to size */
	private final HashMap<Integer, Integer> sizes;
	/** UIDL to ID */
	private final HashMap<String, Integer> uidlToID;

	private Socket socket;
	private final AtomicLong lastActive;
	private final AtomicLong lastChecked;

	private final Object synchronizer;
	private final DelayedDeleter delayedDeleter;
	// instantiated after first successful connection
	private BackgroundChecker backgroundChecker;
	// instantiated after every successful connection
	private IdleCloser idleCloser;
	private volatile NewMailListener newMailListener;

	/**
	 * Does not connect. Caller must call connectToServer() if desired.
	 *
	 * @param host
	 * @param port
	 * @param user
	 * @param pass
	 */
	public POP3MailBox(String host, int port, String user, String pass) {
		Debug.debug(
			Debug.DEBUG,
			"Mailbox(" + host + "," + port + "," + user + ",password)");
		this.host = host;
		this.port = port;
		this.user = user;
		this.pass = pass;
		uidlToID = new HashMap<String, Integer>();
		sizes = new HashMap<Integer, Integer>();
		synchronizer = new Object();
		// this appears in the UI so translate
		lastLine = _t("No response from server");
		lastActive = new AtomicLong(System.currentTimeMillis());
		lastChecked = new AtomicLong();
		delayedDeleter = new DelayedDeleter(this);
	}

	/**
	 * Fetch the header. Does not cache.
	 * 
	 * @param uidl
	 * @return Byte buffer containing header data or null
	 */
	public ReadBuffer getHeader( String uidl ) {
		synchronized( synchronizer ) {
			try {
				// we must be connected to know the UIDL to ID mapping
				checkConnection();
			} catch (IOException ioe) {
				Debug.debug( Debug.DEBUG, "Error fetching header: " + ioe);
				return null;
			}
			int id = getIDfromUIDL(uidl);
			if (id < 0)
				return null;
			return getHeader(id);
		}
	}

	/**
	 * retrieves header from pop3 server (with TOP command and RETR as fallback)
	 * Caller must sync.
	 * 
	 * @param id message id
	 * @return Byte buffer containing header data or null
	 */
	private ReadBuffer getHeader( int id ) {
			Debug.debug(Debug.DEBUG, "getHeader(" + id + ")");
			ReadBuffer header = null;
			if (id >= 1 && id <= mails) {
				/*
				 * try 'TOP n 0' command
				 */
				header = sendCmdN("TOP " + id + " 0" );
				if( header == null) {
					/*
					 * try 'RETR n' command
					 */
					header = sendCmdN("RETR " + id );
					if (header == null)
						Debug.debug( Debug.DEBUG, "RETR returned null" );
				}
			} else {
				lastError = "Message id out of range.";
			}
			return header;
	}

	/**
	 * Fetch the body. Does not cache.
	 * 
	 * @param uidl
	 * @return Byte buffer containing body data or null
	 */
	public ReadBuffer getBody( String uidl ) {
		synchronized( synchronizer ) {
			try {
				// we must be connected to know the UIDL to ID mapping
				checkConnection();
			} catch (IOException ioe) {
				Debug.debug( Debug.DEBUG, "Error fetching body: " + ioe);
				return null;
			}
			int id = getIDfromUIDL(uidl);
			if (id < 0)
				return null;
			return getBody(id);
		}
	}

	/**
	 * Fetch headers and/or bodies. Does not cache.
	 * ReadBuffer objects are inserted into the requests.
	 * No total time limit.
	 * 
	 * @since 0.9.13
	 */
	public void getBodies(Collection<FetchRequest> requests) {
		List<SendRecv> srs = new ArrayList<SendRecv>(requests.size());
		synchronized( synchronizer ) {
			try {
				// we must be connected to know the UIDL to ID mapping
				checkConnection();
			} catch (IOException ioe) {
				Debug.debug( Debug.DEBUG, "Error fetching: " + ioe);
				return;
			}
			for (FetchRequest fr : requests) {
				int id = getIDfromUIDL(fr.getUIDL());
				if (id < 0)
					continue;
				SendRecv sr;
				if (fr.getHeaderOnly() && supportsTOP)
					sr = new SendRecv("TOP " + id + " 0", Mode.RB);
				else
					sr = new SendRecv("RETR " + id, Mode.RB);
				sr.savedObject = fr;
				srs.add(sr);
			}
			if (srs.isEmpty())
				return;
			try {
				sendCmds(srs);
			} catch (IOException ioe) {
				Debug.debug( Debug.DEBUG, "Error fetching bodies: " + ioe);
				// todo maybe
			}
		}
		for (SendRecv sr : srs) {
			if (sr.result) {
				FetchRequest fr = (FetchRequest) sr.savedObject;
				fr.setBuffer(sr.rb);
			}
		}
	}
	
	/**
	 * retrieve message body from pop3 server (via RETR command)
	 * Caller must sync.
	 * 
	 * @param id message id
	 * @return Byte buffer containing body data or null
	 */
	private ReadBuffer getBody(int id) {
			Debug.debug(Debug.DEBUG, "getBody(" + id + ")");
			ReadBuffer body = null;
			if (id >= 1 && id <= mails) {
				try {
					body = sendCmdN( "RETR " + id );
					if (body == null)
						Debug.debug( Debug.DEBUG, "RETR returned null" );
				} catch (OutOfMemoryError oom) {
					Debug.debug( Debug.ERROR, "OOM fetching mail" );
					lastError = oom.toString();
					close();
				}
			}
			else {
				lastError = "Message id out of range.";
			}
			return body;
	}

	/**
	 * Call performDelete() after this or they will come back
	 * UNUSED
	 * 
	 * @param uidl
	 * @return Success of delete operation: true if successful.
	 */
/****
	public boolean delete( String uidl )
	{
		Debug.debug(Debug.DEBUG, "delete(" + uidl + ")");
		synchronized( synchronizer ) {
			try {
				// we must be connected to know the UIDL to ID mapping
				checkConnection();
			} catch (IOException ioe) {
				Debug.debug( Debug.DEBUG, "Error deleting: " + ioe);
				return false;
			}
			int id = getIDfromUIDL(uidl);
			if (id < 0)
				return false;
			return delete(id);
		}
	}
****/

	/**
	 * Queue for later deletion. Non-blocking.
	 * 
	 * @since 0.9.13
	 */
	public void queueForDeletion(Collection<String> uidls) {
		for (String uidl : uidls) {
			queueForDeletion(uidl);
		}
	}

	/**
	 * Queue for later deletion. Non-blocking.
	 * 
	 * @since 0.9.13
	 */
	public void queueForDeletion(String uidl) {
		Debug.debug(Debug.DEBUG, "Queueing for deletion: " + uidl);
		delayedDeleter.queueDelete(uidl);
	}

	/**
	 * Delete all at once and close. Does not reconnect.
	 * Do NOT call performDelete() after this.
	 * Returns all UIDLs successfully deleted OR were not known by the server.
	 * 
	 * @since 0.9.13
	 */
	Collection<String> delete(Collection<String> uidls) {
		List<String> rv = new ArrayList<String>(uidls.size());
		List<SendRecv> srs = new ArrayList<SendRecv>(uidls.size() + 1);
		synchronized( synchronizer ) {
			try {
				// we must be connected to know the UIDL to ID mapping
				checkConnection();
			} catch (IOException ioe) {
				Debug.debug( Debug.DEBUG, "Error deleting: " + ioe);
				return rv;
			}
			for (String uidl : uidls) {
				int id = getIDfromUIDL(uidl);
				if (id < 0) {
					// presumed already deleted
					rv.add(uidl);
					continue;
				}
				SendRecv sr = new SendRecv("DELE " + id, Mode.A1);
				sr.savedObject = uidl;
				srs.add(sr);
			}
			if (srs.isEmpty())
				return rv;
			// TODO don't quit now, just set timer to quit later
			SendRecv quit = new SendRecv("QUIT", Mode.A1);
			srs.add(quit);
			try {
				sendCmds(srs);
				// do NOT call close() here, we included QUIT above
				try {
					socket.close();
				} catch (IOException e) {}
				clear();
				// result of QUIT
				boolean success = srs.get(srs.size() - 1).result;
				if (success) {
					for (int i = 0; i < srs.size() - 1; i++) {
						SendRecv sr = srs.get(i);
						// ignore sr.result, if it failed it's because
						// it's already deleted
						rv.add((String) sr.savedObject);
					}
				}
				// why reconnect?
				//connect();
			} catch (IOException ioe) {
				Debug.debug( Debug.DEBUG, "Error deleting: " + ioe);
				// todo maybe
			}
		}
		return rv;
	}
	
	/**
	 * delete message on pop3 server
	 * UNUSED
	 * 
	 * @param id message id
	 * @return Success of delete operation: true if successful.
	 */
/****
	private boolean delete(int id)
	{
		Debug.debug(Debug.DEBUG, "delete(" + id + ")");
		
		boolean result = false;
		
		synchronized( synchronizer ) {
			
			try {
				result = sendCmd1a( "DELE " + id );
			}
			catch (IOException e) {
			}
		}
		return result;
	}
****/

	/**
	 * Get cached size of a message (via previous LIST command).
	 * 
	 * @param uidl
	 * @return Message size in bytes or 0 if not found
	 */
	public int getSize( String uidl ) {
		synchronized( synchronizer ) {
			int id = getIDfromUIDL(uidl);
			if (id < 0)
				return 0;
			return getSize(id);
		}
	}
	
	/**
	 * Get cached size of a message (via previous LIST command).
	 * Caller must sync.
	 * 
	 * @param id message id
	 * @return Message size in bytes or 0 if not found
	 */
	private int getSize(int id) {
			int result = 0;
			/*
			 * find value in hashtable
			 */
			Integer resultObj = sizes.get(Integer.valueOf(id));
			if (resultObj != null)
				result = resultObj.intValue();
			Debug.debug(Debug.DEBUG, "getSize(" + id + ") = " + result);
			return result;
	}

	/**
	 * Is the connection is still alive
	 * 
	 * @return true or false
	 */
	public boolean isConnected() {
		if (socket == null
			|| !socket.isConnected()
			|| socket.isInputShutdown()
			|| socket.isOutputShutdown()
			|| socket.isClosed()) {
			connected = false;
		}
		return connected;
	}

	/**
	 * If not connected, connect now.
	 * Should be called from all public methods before sending a command.
	 * Caller must sync.
	 * 
	 * @return true or false
	 */
	private void checkConnection() throws IOException {
		Debug.debug(Debug.DEBUG, "checkConnection()");
		if (!isConnected()) {
			connect();
			if (!isConnected())
				throw new IOException("Cannot connect");
		}
	}

	/**
	 * Timestamp.
	 * 
	 * @since 0.9.13
	 */
	private void updateActivity() {
		lastActive.set(System.currentTimeMillis());
	}

	/**
	 * Timestamp.
	 * 
	 * @since 0.9.13
	 */
	long getLastActivity() {
		return lastActive.get();
	}

	/**
	 * Timestamp. When we last successfully got the UIDL list.
	 * 
	 * @since 0.9.13
	 */
	long getLastChecked() {
		return lastChecked.get();
	}

	/**
	 * 
	 * @param response line starting with +OK
	 */
	private void updateMailCount(String response) {
		if (response == null || response.length() < 4) {
			mails = 0;
			return;
		}
		response = response.trim();
		try {
			int i = response.indexOf(' ', 5);
			mails =
				Integer.parseInt(
					i != -1
						? response.substring(4, i)
						: response.substring(4));
		} catch (NumberFormatException nfe) {
			mails = 0;
		}
	}

	/**
	 * Caller must sync.
	 * 
	 * @throws IOException
	 */
	private void updateUIDLs(List<String> lines) {
			uidlToID.clear();
			if (lines != null) {
				for (String line : lines) {
					int j = line.indexOf(' ');
					if( j != -1 ) {
						try {
							int n = Integer.parseInt( line.substring( 0, j ) );
							String uidl = line.substring(j + 1).trim();
							uidlToID.put( uidl, Integer.valueOf( n ) );
						} catch (NumberFormatException nfe) {
							Debug.debug(Debug.DEBUG, "UIDL error " + nfe);
						} catch (IndexOutOfBoundsException ioobe) {
							Debug.debug(Debug.DEBUG, "UIDL error " + ioobe);
						}
					}
				}
				lastChecked.set(System.currentTimeMillis());
			} else {
				Debug.debug(Debug.DEBUG, "Error getting UIDL list from server.");
			}
	}

	/**
	 * Caller must sync.
	 * 
	 * @throws IOException
	 */
	private void updateSizes(List<String> lines) {
		/*
		 * try LIST
		 */
		sizes.clear();
		if (lines != null) {
			for (String line : lines) {
				int j = line.indexOf(' ');
				if (j != -1) {
					try {
						int key = Integer.parseInt(line.substring(0, j));
						int value = Integer.parseInt(line.substring(j + 1).trim());
						sizes.put(Integer.valueOf(key), Integer.valueOf(value));
					} catch (NumberFormatException nfe) {
						Debug.debug(Debug.DEBUG, "LIST error " + nfe);
					}
				}
			}
		} else {
			Debug.debug(Debug.DEBUG, "Error getting LIST from server.");
		}
	}

	/**
	 * 
	 *
	 */
	public void refresh() {
		synchronized( synchronizer ) {
			close(true);
			connect();
		}
	}

	/**
	 * Caller must sync.
	 */
	private void clear()
	{
		uidlToID.clear();
		sizes.clear();
		mails = 0;
	}

	/**
	 * Connect to pop3 server if not connected.
	 * Does nothing if already connected.
	 *
	 * @return true if connected
	 * @since 0.9.13
	 */
	public boolean connectToServer() {
		synchronized( synchronizer ) {
			if (isConnected())
				return true;
			connect();
			return isConnected();
		}
	}

	/**
	 * connect to pop3 server, login with USER and PASS and try STAT then
	 *
	 * Caller must sync.
	 */
	private void connect() {
		Debug.debug(Debug.DEBUG, "connect()");
		if (Debug.getLevel() == Debug.DEBUG)
			(new Exception("I did it")).printStackTrace();

		clear();
		
		if (socket != null && socket.isConnected())
			close();
		
		try {
			socket = new Socket(host, port);
		} catch (UnknownHostException e) {
			lastError = e.toString();
			return;
		} catch (IOException e) {
			Debug.debug( Debug.DEBUG, "Error connecting: " + e);
			lastError = e.toString();
			return;
		}
		if (socket != null) {
			try {
				// pipeline 2 commands
				lastError = "";
				socket.setSoTimeout(120*1000);
				boolean ok = doHandshake();
				if (ok) {
					// TODO APOP (unsupported by postman)
					List<SendRecv> cmds = new ArrayList<SendRecv>(4);
					cmds.add(new SendRecv("USER " + user, Mode.A1));
					cmds.add(new SendRecv("PASS " + pass, Mode.A1));
					socket.setSoTimeout(60*1000);
					ok =  sendCmds(cmds);
				}
				if (ok) {
					connected = true;
					List<SendRecv> cmds = new ArrayList<SendRecv>(4);
					SendRecv stat = new SendRecv("STAT", Mode.A1);
					cmds.add(stat);
					SendRecv uidl = new SendRecv("UIDL", Mode.LS);
					cmds.add(uidl);
					SendRecv list = new SendRecv("LIST", Mode.LS);
					cmds.add(list);
					// check individual responses
					socket.setSoTimeout(120*1000);
					ok = sendCmds(cmds);
					if (stat.result)
						updateMailCount(stat.response);
					else
						Debug.debug(Debug.DEBUG, "STAT failed");
					if (uidl.result)
						updateUIDLs(uidl.ls);
					else
						Debug.debug(Debug.DEBUG, "UIDL failed");
					if (list.result)
						updateSizes(list.ls);
					else
						Debug.debug(Debug.DEBUG, "LIST failed");
					socket.setSoTimeout(300*1000);
					if (ok && backgroundChecker == null &&
						Boolean.parseBoolean(Config.getProperty(WebMail.CONFIG_BACKGROUND_CHECK)))
						backgroundChecker = new BackgroundChecker(this);
					if (ok && idleCloser == null)
						idleCloser = new IdleCloser(this);
				} else {
					if (lastError.equals(""))
						lastError = _t("Error connecting to server");
					close();
				}
			}
			catch (NumberFormatException e1) {
				lastError = _t("Error opening mailbox") + ": " + e1;
			}
			catch (IOException e1) {
				lastError = _t("Error opening mailbox") + ": " + e1.getLocalizedMessage();
			}
		}
	}

	/**
	 * Check the initial response, send CAPA, check the CAPA result
	 * Caller must sync.
	 * 
	 * @return true if successful
	 * @throws IOException
	 * @since 0.9.13
	 */
	private boolean doHandshake() throws IOException {
		List<SendRecv> cmds = new ArrayList<SendRecv>(2);
		cmds.add(new SendRecv(null, Mode.A1));
		SendRecv capa = null;
		if (gotCAPA) {
			Debug.debug(Debug.DEBUG, "Skipping CAPA");
		} else {
			capa = new SendRecv("CAPA", Mode.LS);
			cmds.add(capa);
		}
		boolean rv = sendCmds(cmds);
		if (rv && capa != null) {
			if (capa.ls != null) {
				for (String cap : capa.ls) {
					String t = cap.trim();
					if (t.equals("PIPELINING"))
						supportsPipelining = true;
					else if (t.equals("UIDL"))
						supportsUIDL = true;
					else if (t.equals("TOP"))
						supportsTOP = true;
				}
			}
			gotCAPA = true;
			Debug.debug(Debug.DEBUG, "POP3 server caps: pipelining? " + supportsPipelining +
		                                           " UIDL? " + supportsUIDL +
		                                           " TOP? " + supportsTOP);
		}
		return rv;
	}
	
	/**
	 * send command to pop3 server (and expect single line answer)
	 * Response will be in lastLine. Does not read past the first line of the response.
	 * Caller must sync.
	 * 
	 * @param cmd command to send
	 * @return true if command was successful (+OK)
	 * @throws IOException
	 */
	private boolean sendCmd1a(String cmd) throws IOException {
		boolean result = false;
		sendCmd1aNoWait(cmd);
		socket.getOutputStream().flush();
		String foo = DataHelper.readLine(socket.getInputStream());
		updateActivity();
		// Debug.debug(Debug.DEBUG, "sendCmd1a: read " + read + " bytes");
		if (foo != null) {
			lastLine = foo;
			if (lastLine.startsWith("+OK")) {
				if (cmd.startsWith("PASS"))
					cmd = "PASS provided";
				Debug.debug(Debug.DEBUG, "sendCmd1a: (" + cmd + ") success: \"" + lastLine.trim() + '"');
				result = true;
			} else {
				if (cmd.startsWith("PASS"))
					cmd = "PASS provided";
				Debug.debug(Debug.DEBUG, "sendCmd1a: (" + cmd + ") FAIL: \"" + lastLine.trim() + '"');
				lastError = lastLine;
			}
		} else {
			Debug.debug(Debug.DEBUG, "sendCmd1a: (" + cmd + ") NO RESPONSE");
			lastError = _t("No response from server");
			throw new IOException(lastError);
		}
		return result;
	}

	/**
	 * Send commands to pop3 server all at once (and expect answers).
	 * Sets lastError to the FIRST error.
	 * Caller must sync.
	 * 
	 * @param cmd command to send
	 * @param rcvLines lines to receive
	 * @return true if ALL received lines were successful (+OK)
	 * @throws IOException
	 * @since 0.9.13
	 */
	private boolean sendCmds(List<SendRecv> cmds) throws IOException {
		boolean result = true;
		boolean pipe = supportsPipelining;
		if (pipe) {
			Debug.debug(Debug.DEBUG, "POP3 pipelining " + cmds.size() + " commands");
			for (SendRecv sr : cmds) {
				String cmd = sr.send;
				if (cmd != null)
					sendCmd1aNoWait(cmd);
			}
		} // else we will do it below
		socket.getOutputStream().flush();
		InputStream in = socket.getInputStream();
		int i = 0;
		for (SendRecv sr : cmds) {
			if (!pipe) {
				String cmd = sr.send;
				if (cmd != null) {
					sendCmd1aNoWait(cmd);
					socket.getOutputStream().flush();
				}
			}
			String foo = DataHelper.readLine(in);
			updateActivity();
			if (foo == null) {
				lastError = _t("No response from server");
				throw new IOException(lastError);
			}
			sr.response = foo.trim();
			i++;
			if (!foo.startsWith("+OK")) {
				Debug.debug(Debug.DEBUG, "Fail after " + i + " of " + cmds.size() + " responses: \"" + foo.trim() + '"');
				if (result)
				    lastError = foo;   // actually the first error, for better info to the user
				result = false;
				sr.result = false;
			} else {
				Debug.debug(Debug.DEBUG, "OK after " + i + " of " + cmds.size() + " responses: \"" + foo.trim() + '"');
				switch (sr.mode) {
				    case A1:
					sr.result = true;
					break;

				    case RB:
					try {
						sr.rb = getResultNa();
						sr.result = true;
					} catch (IOException ioe) {
						Debug.debug( Debug.DEBUG, "Error getting RB: " + ioe);
						result = false;
						sr.result = false;
					}
					break;

				    case LS:
					try {
						sr.ls = getResultNl();
						sr.result = true;
					} catch (IOException ioe) {
						Debug.debug( Debug.DEBUG, "Error getting LS: " + ioe);
						result = false;
						sr.result = false;
					}
					break;
				}
			}
			lastLine = foo;
		}
		return result;
	}
	
	/**
	 * send command to pop3 server. Does NOT flush or read or wait.
	 * Caller must sync.
	 * 
	 * @param cmd command to send non-null
	 * @throws IOException
         * @since 0.9.13
	 */
	private void sendCmd1aNoWait(String cmd) throws IOException {
		/*
		 * dont log password
		 */
		String msg = cmd;
		if (msg.startsWith("PASS"))
			msg = "PASS provided";
		Debug.debug(Debug.DEBUG, "sendCmd1a(" + msg + ")");
		cmd += "\r\n";
		socket.getOutputStream().write(DataHelper.getASCII(cmd));
		updateActivity();
	}

	/**
	 * Tries twice
	 * Caller must sync.
	 * 
	 * @return buffer or null
	 */
	private ReadBuffer sendCmdN(String cmd )
	{
		synchronized (synchronizer) {
			try {
				return sendCmdNa(cmd);
			} catch (IOException e) {
				lastError = e.toString();
				Debug.debug( Debug.DEBUG, "sendCmdNa throws: " + e);
			}
			connect();
			if (connected) {
				try {
					return sendCmdNa(cmd);
				} catch (IOException e2) {
					lastError = e2.toString();
					Debug.debug( Debug.DEBUG, "2nd sendCmdNa throws: " + e2);
				}
			} else {
				Debug.debug( Debug.DEBUG, "not connected after reconnect" );					
			}
		}
		return null;
	}

	/**
	 * No total timeout (result could be large)
	 * Caller must sync.
	 *
	 * @return buffer or null
	 * @throws IOException
	 */
	private ReadBuffer sendCmdNa(String cmd) throws IOException
	{
		if (sendCmd1a(cmd)) {
			return getResultNa();
		} else {
			Debug.debug( Debug.DEBUG, "sendCmd1a returned false" );
			return null;
		}
	}

	/**
	 * Like sendCmdNa but returns a list of strings, one per line.
	 * Strings will have trailing \r but not \n.
	 * Total timeout 2 minutes.
	 * Caller must sync.
	 *
	 * @return the lines or null on error
	 * @throws IOException on timeout
         * @since 0.9.13
	 */
	private List<String> sendCmdNl(String cmd) throws IOException
	{
		if (sendCmd1a(cmd)) {
			return getResultNl();
		} else {
			Debug.debug( Debug.DEBUG, "sendCmd1a returned false" );
			return null;
		}
	}

	/**
	 * No total timeout (result could be large)
	 * Caller must sync.
	 *
	 * @return buffer non-null
	 * @throws IOException
	 */
	private ReadBuffer getResultNa() throws IOException
	{
		InputStream input = socket.getInputStream();
		StringBuilder buf = new StringBuilder(512);
		ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
		while (DataHelper.readLine(input, buf)) {
			updateActivity();
			int len = buf.length();
			if (len == 0)
				break; // huh? no \r?
			if (len == 2 && buf.charAt(0) == '.' && buf.charAt(1) == '\r')
				break;
			String line;
			// RFC 1939 sec. 3 de-byte-stuffing
			if (buf.charAt(0) == '.')
				line = buf.substring(1);
			else
				line = buf.toString();
			baos.write(DataHelper.getASCII(line));
			if (buf.charAt(len - 1) != '\r')
				baos.write((byte) '\n');
			baos.write((byte) '\n');
			buf.setLength(0);
		}
		return new ReadBuffer(baos.toByteArray(), 0, baos.size());
	}

	/**
	 * Like getResultNa but returns a list of strings, one per line.
	 * Strings will have trailing \r but not \n.
	 * Total timeout 2 minutes.
	 * Caller must sync.
	 *
	 * @return the lines non-null
	 * @throws IOException on timeout
         * @since 0.9.13
	 */
	private List<String> getResultNl() throws IOException
	{
		List<String> rv = new ArrayList<String>(16);
		long timeOut = 120*1000;
		InputStream input = socket.getInputStream();
		long startTime = System.currentTimeMillis();
		StringBuilder buf = new StringBuilder(512);
		while (DataHelper.readLine(input, buf)) {
			updateActivity();
			int len = buf.length();
			if (len == 0)
				break; // huh? no \r?
			if (len == 2 && buf.charAt(0) == '.' && buf.charAt(1) == '\r')
				break;
			if( System.currentTimeMillis() - startTime > timeOut )
				throw new IOException( "Timeout while waiting on server response." );
			String line;
			// RFC 1939 sec. 3 de-byte-stuffing
			if (buf.charAt(0) == '.')
				line = buf.substring(1);
			else
				line = buf.toString();
			rv.add(line);
			buf.setLength(0);
		}
		return rv;
	}

	/**
	 * Warning - forces a connection.
	 *
	 * @return The amount of e-mails available.
	 */
	public int getNumMails() {
		synchronized( synchronizer ) {
			Debug.debug(Debug.DEBUG, "getNumMails()");
			try {
				checkConnection();
			} catch (IOException ioe) {}
			return connected ? mails : 0;
		}
	}

	/**
	 * @return The most recent error message.
	 */
	public String lastError() {
		//Debug.debug(Debug.DEBUG, "lastError()");
		// Hide the "-ERR" from the user
		String e = lastError;
		if (e.startsWith("-ERR ") && e.length() > 5)
			e = e.substring(5);
		// translate this common error
		if (e.trim().equals("Login failed."))
			e = _t("Login failed");
		return e;
	}


	/**
	 *  Relay from the checker to the webmail session object,
	 *  which relays to MailCache, which will fetch the mail from us
	 *  in a big circle
	 *
	 *  @since 0.9.13
	 */
	public void setNewMailListener(NewMailListener nml) {
		newMailListener = nml;
	}

	/**
	 *  Relay from the checker to the webmail session object,
	 *  which relays to MailCache, which will fetch the mail from us
	 *  in a big circle
	 *
	 *  @since 0.9.13
	 */
	public void foundNewMail() {
		NewMailListener  nml = newMailListener;
		if (nml != null)
			nml.foundNewMail();
	}

	/**
	 *  Close without waiting for response,
	 *  and remove any delayed tasks and resources.
	 */
	public void destroy() {
		delayedDeleter.cancel();
		synchronized( synchronizer ) {
			if (backgroundChecker != null)
				backgroundChecker.cancel();
			close(false);
		}
	}

	/**
	 *  For helper threads to lock
	 *  @since 0.9.13
	 */
	Object getLock() {
		return synchronizer;
	}

	/**
	 *  Do we have UIDLs to delete?
	 *  @since 0.9.13
	 */
	boolean hasQueuedDeletions() {
		return !delayedDeleter.getQueued().isEmpty();
	}

	/**
	 *  Close without waiting for response.
	 *  Deletes all queued deletions.
	 */
	public void close() {
		close(false);
	}

	/**
	 *  Close and optionally wait for response.
	 *  Deletes all queued deletions.
	 *  @since 0.9.13
	 */
	void close(boolean shouldWait) {
		synchronized( synchronizer ) {
			Debug.debug(Debug.DEBUG, "close()");
			if (idleCloser != null)
				idleCloser.cancel();
			if (socket != null && socket.isConnected()) {
				try {
					Collection<String> toDelete = delayedDeleter.getQueued();
					Map<String, Integer> sendDelete = new HashMap<String, Integer>(toDelete.size());
					for (String uidl : toDelete) {
						int id = getIDfromUIDL(uidl);
						if (id >= 0) {
							sendDelete.put(uidl, Integer.valueOf(id));
						}
					}
					if (shouldWait) {
						if (!sendDelete.isEmpty()) {
							// Verify deleted, remove from the delete queue
							// this does the quit and close
							Collection<String> deleted = delete(sendDelete.keySet());
							for (String uidl : deleted) {
								delayedDeleter.removeQueued(uidl);
							}
						} else {
							sendCmd1a("QUIT");
						}
						Debug.debug( Debug.DEBUG, "close() with wait complete");
					} else {
						if (!sendDelete.isEmpty()) {
							// spray and pray the deletions, don't remove from delete queue
							for (Integer id : sendDelete.values()) {
								sendCmd1aNoWait("DELE " + id);
							}
						}
						sendCmd1aNoWait("QUIT");
					}
					socket.close();
				} catch (IOException e) {
					Debug.debug( Debug.DEBUG, "error closing: " + e);
				}
			}
			socket = null;
			connected = false;
			clear();
		}
	}

	/**
	 * returns number of message with given UIDL
	 * Caller must sync.
	 * 
	 * @param uidl
	 * @return Message number or -1
	 */
	private int getIDfromUIDL( String uidl )
	{
		int result = -1;
		Integer intObject = uidlToID.get( uidl );
		if( intObject != null ) {
			result = intObject.intValue();
		}
		return result;
	}

	/**
	 * Unused
	 * @param id
	 * @return UIDL or null
	 */
/****
	public String getUIDLfromID( int id )
	{
		synchronized( synchronizer ) {
			try {
				return uidlList.get( id );
			} catch (IndexOutOfBoundsException ioobe) {
				return null;
			}
		}
	}
****/

	/**
	 * Only if connected. Does not force a connect.
	 * If not connected, returns null.
	 * 
	 * @return A new array of the available UIDLs. No particular order.
	 */
	public Collection<String> getUIDLs()
	{
		if (!isConnected())
			return null;
		synchronized( synchronizer ) {
		       return new ArrayList<String>(uidlToID.keySet());
		}
	}

	/**
	 * 
	 * @param args
	 */
/****
	public static void main( String[] args )
	{
		Debug.setLevel( Debug.DEBUG );
		POP3MailBox mailbox = new POP3MailBox( "localhost", 7660 , "test", "test");
		ReadBuffer readBuffer = mailbox.sendCmdN( "LIST" );
		System.out.println( "list='" + readBuffer + "'" );
	}
****/

	/**
	 *  Close and reconnect. Takes a while.
	 *  UNUSED
	 */
/****
	public void performDelete()
	{
		synchronized( synchronizer ) {
			close(true);
			// why reconnect?
			//connect();
		}
	}
****/

	/** for SendRecv */
	private enum Mode {
		/** no extra lines (sendCmd1a) */
		A1,
		/** return extra lines in ReadBuffer (sendCmdNa) */
		RB,
		/** return extra lines in List of Strings (sendCmdNl) */
		LS
	}

	/**
	 *  A command to send and a mode to receive and return the results
	 *  @since 0.9.13
	 */
	private static class SendRecv {
		public final String send;
		public final Mode mode;
		public String response;
		public boolean result;
		public ReadBuffer rb;
		public List<String> ls;
		// to remember things
		public Object savedObject;

		/** @param s may be null */
		public SendRecv(String s, Mode m) {
			send = s;
			mode = m;
		}
	}

	public interface FetchRequest {
		public String getUIDL();
		public boolean getHeaderOnly();
		public void setBuffer(ReadBuffer buffer);
	}

	/** translate */
	private static String _t(String s) {
		return Messages.getString(s);
	}
}
