package zutil.network;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.net.UnknownHostException;

import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;

/**
 * A simple class that connects and logs in to a POP3
 * server and then can read and delete messages.
 * INFO: http://pages.prodigy.net/michael_santovec/pop3telnet.htm
 * 
 * @author Ziver
 *
 */
public class POP3Client {
	public static boolean DEBUG = false;
	public static final int POP3_PORT = 110;
	public static final int POP3_SSL_PORT = 995;

	private BufferedReader in;
	private PrintStream out;
	private Socket socket;

	/**
	 * Connect to a POP3 server without username
	 * 
	 * @param host The hostname of the server
	 * @throws UnknownHostException
	 * @throws IOException
	 */
	public POP3Client(String host) throws UnknownHostException, IOException{
		this(host, POP3_PORT, null, null, false);
	}
	
	/**
	 * Connect to a POP3 server with username and password
	 * 
	 * @param host The hostname of the server
	 * @param user The username
	 * @param password the password
	 * @throws UnknownHostException
	 * @throws IOException
	 */
	public POP3Client(String host, String user, String password) throws UnknownHostException, IOException{
		this(host, POP3_PORT, user, password, false);
	}
	
	/**
	 * Connects to a POP3 server with username and password and SSL
	 * 
	 * @param host The hostname of the server
	 * @param user The username
	 * @param password the password
	 * @param ssl If SSL should be used
	 * @throws UnknownHostException
	 * @throws IOException
	 */
	public POP3Client(String host, String user, String password, boolean ssl) throws UnknownHostException, IOException{
		this(host, (ssl ? POP3_SSL_PORT : POP3_PORT), user, password, ssl);		
	}

	/**
	 * Connects to a POP3 server with username and password and 
	 * SSL and with costume port.
	 * 
	 * @param host The hostname of the server
	 * @param port The port number to connect to on the server
	 * @param user The username
	 * @param password the password
	 * @param ssl If SSL should be used
	 * @throws UnknownHostException
	 * @throws IOException
	 */
	public POP3Client(String host, int port, String user, String password, boolean ssl) throws UnknownHostException, IOException{
		if(ssl) connectSSL(host, port);
		else connect(host, port);
		
		if(user != null){
			login(user, password);
		}
	}
	
	/**
	 * Connects to the server
	 * 
	 * @param host The hostname of the server
	 * @param port The port to connect to on the server
	 * @throws UnknownHostException
	 * @throws IOException
	 */
	private void connect(String host, int port) throws UnknownHostException, IOException{
		socket = new Socket(host, port);
		in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		out = new PrintStream(socket.getOutputStream());

		readCommand(true);
	}
	
	/**
	 * Connects to the server with SSL.
	 * http://www.exampledepot.com/egs/javax.net.ssl/Client.html
	 * 
	 * @param host The hostname of the server
	 * @param port The port to connect to on the server
	 * @throws UnknownHostException
	 * @throws IOException
	 */
	private void connectSSL(String host, int port) throws UnknownHostException, IOException{
		SocketFactory socketFactory = SSLSocketFactory.getDefault();
		socket = socketFactory.createSocket(host, port);
         
		in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
		out = new PrintStream(socket.getOutputStream());

		readCommand(DEBUG);
	}

	/**
	 * Logs in to the POP3 server with the username and password if the password is set
	 * 
	 * @param user The user name
	 * @param password The password or null if no password is required
	 * @throws IOException
	 */
	private void login(String user, String password) throws IOException{
		sendCommand("USER "+user);
		if(password != null){
			sendNoReplyCommand("PASS "+password, false);
			if(DEBUG)System.out.println("PASS ***");
			readCommand(DEBUG);
		}
	}
	
	/**
	 * Returns the number of messages that is on the server
	 * 
	 * @return Message count
	 * @throws IOException
	 */
	public int getMessageCount() throws IOException{
		String msg = sendCommand("STAT", DEBUG);
		return Integer.parseInt(
				msg.substring(
						msg.indexOf(' ')+1,
						msg.indexOf(' ', msg.indexOf(' ')+1)));
	}
	
	/**
	 * Retrieves the message with the given id.
	 * 
	 * @param id The id of the message to get
	 * @return The message
	 * @throws IOException 
	 */
	public String getMessage(int id) throws IOException{
		sendCommand("RETR "+id);
		return readMultipleLines(DEBUG);
	}
	
	/**
	 * Returns the title of the message
	 * 
	 * @param id The message id
	 * @return The title
	 * @throws IOException
	 */
	public String getMessageTitle(int id) throws IOException{
		String tmp = getMessageHeader(id);
		String tmp2 = tmp.toLowerCase();
		if(tmp2.contains("subject:")){
			return tmp.substring(
					tmp2.indexOf("subject:")+8, 
					tmp2.indexOf('\n', 
							tmp2.indexOf("subject:")));
		}
		else
			return null;
	}
	
	/**
	 * Returns the header of the given message id.
	 * 
	 * @param id The id of the message to get
	 * @return The message
	 * @throws IOException 
	 */
	public String getMessageHeader(int id) throws IOException{
		sendCommand("TOP "+id+" 0");
		return readMultipleLines(DEBUG);
	}
	
	/**
	 * Deletes the message with the given id
	 * 
	 * @param id The id of the message to be deleted
	 * @throws IOException
	 */
	public void delete(int id) throws IOException{
		sendCommand("DELE "+id);
	}


//*********************** IO Stuff *********************************************
	
	/**
	 * Sends the given line to the server and returns a status integer
	 * 
	 * @param cmd The command to send
	 * @return The return code from the server
	 * @throws IOException if the cmd fails
	 */
	private boolean sendCommand(String cmd) throws IOException{
		return parseReturnCode(sendCommand(cmd, DEBUG));
	}

	/**
	 * Sends the given line to the server and returns the last line
	 * 
	 * @param cmd The command to send
	 * @param print To print out the received lines
	 * @return Last String line from the server
	 * @throws IOException 
	 */
	private String sendCommand(String cmd, boolean print) throws IOException{
		sendNoReplyCommand(cmd, print);
		return readCommand(print);
	}

	/**
	 * Sends a given command and don't cares about the reply
	 * 
	 * @param cmd The command
	 * @param print If it should print to System.out
	 * @throws IOException 
	 */
	private void sendNoReplyCommand(String cmd, boolean print) throws IOException{
		out.println(cmd);
		if(print)System.out.println(cmd);
	}

	/**
	 * Reads on line from the command channel
	 * 
	 * @param print If the method should print the input line
	 * @return The input line
	 * @throws IOException if the server returns a error code
	 */
	private String readCommand(boolean print) throws IOException{
		String tmp = in.readLine();
		if(print)System.out.println(tmp);
		if( !parseReturnCode(tmp) ) throw new IOException(tmp);

		return tmp;
	}

	/**
	 * Reads from the server until there are a line with 
	 * only one '.'
	 * 
	 * @param print To print out the received lines
	 * @return String with the text
	 * @throws IOException
	 */
	private String readMultipleLines(boolean print) throws IOException{
		StringBuffer msg = new StringBuffer();
		String tmp = in.readLine();
		while(!tmp.equals(".")){
			msg.append(tmp);
			msg.append('\n');
			tmp = in.readLine();
			if(print)System.out.println(tmp);
		}
		
		return msg.toString();
	}	

	
	/**
	 * Parses the return line from the server and returns the status code
	 * 
	 * @param msg The message from the server
	 * @return Returns true if return code is OK false if it is ERR
	 * @throws IOException 
	 */
	private boolean parseReturnCode(String msg){
		int endpos = (msg.indexOf(' ')<0 ? msg.length() : msg.indexOf(' '));
		return msg.substring(0, endpos).equals("+OK");
	}
	
//*********************************************************************************

	/**
	 * All the delete marked messages are unmarkt
	 * @throws IOException 
	 */
	public void reset() throws IOException{
		sendCommand("RSET", DEBUG);
	}

	/**
	 * All the changes(DELETE) are performed and then the connection is closed
	 * 
	 * @throws IOException
	 */
	public void close() throws IOException{
		sendCommand("QUIT", DEBUG);
		in.close();
		out.close();
		socket.close();
	}
}
