/*
 * @(#)Pop.java
 *
 * Copyright (c) 2001-2002 JScape
 * 1147 S. 53rd Pl., Mesa, Arizona, 85206, U.S.A.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of
 * JScape. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JScape.
 */
package com.jscape.inet.pop;

import com.jscape.inet.email.EmailMessage;
import com.jscape.inet.mime.MimeException;
import com.jscape.inet.proxy.Proxies;
import com.jscape.inet.proxy.Proxy;
import com.jscape.inet.proxy.ProxyDescriptor;
import com.jscape.inet.proxy.ProxySocketConnector;
import com.jscape.inet.util.MD5;
import com.jscape.util.Base64Format;
import com.jscape.util.SuppressObfuscation;
import com.jscape.util.net.ConnectionDescriptor;
import com.jscape.util.net.TransportAddress;
import com.jscape.util.net.connection.Connector;
import com.jscape.util.net.connection.SyncRawConnection;
import com.jscape.util.net.socket.SocketConnector;
import com.jscape.util.net.socket.SocketParameters;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.Vector;

import static com.jscape.util.Parameters.assertNotNull;


/**
 * Implements the basic functionality of a POP3 client.
 * <P>
 * Example Usage:
 * <P>
 * <pre>
 * // new instance with hostname, username and password
 * Pop pop = new Pop("pop3.myserver.com","jsmith","secret");
 * try
 * {
 *   // connect to POP3 server
 *   pop.connect();
 *    // get messages
 *   Enumeration e = pop.getMessages();
 *   while(e.hasMoreElements()) {
 *     EmailMessage em = (EmailMessage)e.nextElement();
 *   }
 *    // disconnect
 *   pop.disconnect();
 * }
 * catch(Exception e)
 * {
 *   System.out.println(e);
 * }
 * </pre>
 * </p>
 */
public class Pop
        implements AutoCloseable,
                   Serializable {

    /**
     * Standard username/password authentication. Use with
     * <code>setAuthMode</code> method.
     *
     * @deprecated replaced by AUTH_LOGIN variable for consistency
     */
    public static final int AUTH_PASS = 0;

    /**
     * Standard username/password authentication. Use with
     * <code>setAuthMode</code> method.
     */
    public static final int AUTH_LOGIN = 0;

    /**
     * APOP authentication. Use with <code>setAuthMode</code> method.
     */
    public static final int AUTH_APOP = 1;

    public static final int AUTH_XOAUTH2 = 2;

    protected static final byte CR = 13;
    protected static final byte LF = 10;
    protected static final byte PERIOD = 46;
    protected static final String CRLF = "\r\n";

    protected static final int DEFAULT_PROXY_PORT = 1080;
    protected static final String DEFAULT_PROXY_TYPE = "SOCKS5";

    protected SyncRawConnection connection;
    protected BufferedInputStream in;
    protected PrintWriter out;
    protected String hostname;
    protected Authentication authentication = Authentication.BASIC;
    protected int connectTimeout = 30000;
    protected int port = 110;
    protected int readTimeout = 30000;
    protected int timeout = 30000;

    /**
     * Holds value of property debugStream.
     */
    protected transient PrintStream debugStream = System.out;
    protected String password;
    protected String username;
    protected transient Vector listeners = new Vector();
    protected boolean debug = false;
    protected boolean delete = false;
    protected String proxyHostname = null;
    protected int proxyPort = DEFAULT_PROXY_PORT;
    protected String proxyUsername = null;
    protected String proxyPassword = null;
    protected String proxyType = DEFAULT_PROXY_TYPE;

    /**
     * Constructs a new Pop instance.
     */
    public Pop() {
        
    }

    /**
     * Constructs a new Pop instance. Sets hostname, username and password for
     * POP3 connection.
     *
     * @param hostname the hostname of POP3 server
     * @param username the username of POP3 user
     * @param password the password of POP3 user
     */
    public Pop(String hostname, String username, String password) {
        this();
        this.hostname = hostname;
        this.username = username;
        this.password = password;
    }

    public Pop setAuthentication(Authentication authentication) {
        assertNotNull(authentication);
        this.authentication = authentication;

        return this;
    }

    /**
     * Sets the authentication mode used when logging into POP3 server.
     * Possible values are Pop.AUTH_LOGIN for username/password authentication
     * or Pop.AUTH_APOP for APOP authentication.
     *
     * @param mode authentication mode
     */
    public void setAuthMode(int mode) {
        this.authentication = Authentication.authenticationFor(mode);
    }

    /**
     * Gets the authentication mode used when logging into POP3 server.
     * Possible values are Pop.AUTH_LOGIN for username/password authentication
     * or Pop.AUTH_APOP for APOP authentication.
     *
     * @return authentication mode
     */
    public int getAuthMode() {
        return this.authentication.code;
    }

    /**
     * Sets the timeout for opening a POP3 server connection.
     * <p/>
     * Default is 30 seconds.
     *
     * @param timeout the timeout in milliseconds
     */
    public void setConnectTimeout(int timeout) {
        this.connectTimeout = timeout;
    }

    /**
     * Gets the timeout used when opening a POP3 server connection.
     * <p/>
     * Default is 30 seconds.
     *
     * @return timeout server connection.
     */
    public int getConnectTimeout() {
        return this.connectTimeout;
    }

    /**
     * Checks if client is connected to POP3 server.
     *
     * @return connection status
     */
    public boolean isConnected() {
        return (out != null);
    }

    /**
     * Sets debugging state. All debug statements are reported to debug stream.
     * Default debug stream is System.out
     *
     * @param debug <code>true</code> to turn debugging on
     *
     * @see #setDebugStream
     */
    public void setDebug(boolean debug) {
        this.debug = debug;
    }

    /**
     * Gets debug level of POP3 session. Debug messages are sent to debug
     * stream.  Defaul debug stream is System.out
     *
     * @return <code>true</code> if debugging is on
     *
     * @see #setDebugStream
     */
    public boolean isDebug() {
        return debug;
    }

    /**
     * Sets PrintStream used in reporting debug statements. Default PrintStream
     * is System.out
     *
     * @param debugStream the PrintStream to send debug statements to
     *
     * @see PrintStream
     */
    public void setDebugStream(PrintStream debugStream) {
        this.debugStream = debugStream;
    }

    /**
     * Gets PrintStream used in reporting debug statements. Default PrintStream
     * is System.out
     *
     * @return the PrintStream used for debugging
     *
     * @see PrintStream
     */
    public PrintStream getDebugStream() {
        return this.debugStream;
    }

    /**
     * Defines whether to delete messages automatically after retrieval.
     * Default is <code>false</code>.
     *
     * @param delete <code>true</code> to delete automatically
     */
    public void setDelete(boolean delete) {
        this.delete = delete;
    }

    /**
     * Checks whether to delete messages automatically after retrieval. Default
     * is <code>false</code>.
     *
     * @return <code>true</code> if should delete automatically
     */
    public boolean isDelete() {
        return delete;
    }

    /**
     * Sets the hostname of the POP3 server.
     *
     * @param hostname the hostname of POP3 server
     */
    public void setHostname(String hostname) {
        synchronized (this) {
            this.hostname = hostname;
        }
    }

    /**
     * Gets the hostname of the POP3 server.
     *
     * @return hostname
     */
    public String getHostname() {
        return hostname;
    }

    /**
     * Gets message from POP3 server. Message id is always one (1) based, first
     * message id beginning with 1, second with 2 and so on.
     *
     * @param msg message id of message
     *
     * @return the email message with supplied message id
     *
     * @throws PopException if I/O or POP3 related error occurs
     * @see EmailMessage
     */
    public EmailMessage getMessage(int msg)
            throws PopException {
        byte[] delim = { CR, LF, PERIOD, CR, LF };
        EmailMessage message = null;

        try {
            synchronized (this) {
                int totalBytes = this.getMessageSize(msg);

                ByteArrayOutputStream bout = new ByteArrayOutputStream();
                issueCommand("RETR " + msg, false);

                String answer = readLine(in);
                firePopResponse(answer);
                debug(answer);

                if (answer.toLowerCase(Locale.ENGLISH).startsWith("-err")) {
                    throw new PopException("Error in RETR: " + answer);
                } else {
                    int count = 0;
                    int total = 0;

                    while (true) {
                        int b = in.read();
                        ++total;

                        // fire PopMessageProgressEvent every 1K
                        if ((total % 1024) == 0) {
                            this.firePopMessageProgress(msg, total, totalBytes);
                        }

                        if (b == -1) {
                            break;
                        }

                        bout.write(b);

                        if (delim[count] == b) {
                            count++;
                        } else {
                            // reset counter
                            if (count != 0) {
                                count = 0;
                            }

                            // check again for match
                            if (delim[count] == b) {
                                count++;
                            }
                        }

                        if (count == delim.length) {
                            break;
                        }
                    }
                }

                this.firePopMessageProgress(msg, totalBytes, totalBytes);

                byte[] data = stripLeadingPeriods(bout.toByteArray());

                // TODO fix for NAV problem
                if (data.length > 3) {
                    byte[] messageData = new byte[data.length - 3];
                    System.arraycopy(data, 0, messageData, 0, data.length - 3);
                    message = new EmailMessage(messageData);
                } else {
                    message = new EmailMessage("Unable to retrieve message contents due to potential conflict with anti-virus software".getBytes(StandardCharsets.US_ASCII));
                }

                bout.close();

                // notify that message was retrieved
                firePopMessageRetrieved(message);

                // delete message if flag set
                if (delete) {
                    deleteMessage(msg);
                }
            }
        } catch (MimeException me) {
            throw new PopException("an error occurred in retrieving message", me);
        } catch (IOException ioe) {
            throw new PopException("an I/O error occurred", ioe);
        }

        return message;
    }

    /**
     * Gets number of messages currently in POP3 mailbox.
     *
     * @return the total number of messages in POP3 account mailbox
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public int getMessageCount()
            throws PopException {
        int messages = 0;

        try {
            synchronized (this) {
                issueCommand("STAT", false);

                String answer = readLine(in);
                debug(answer);
                firePopResponse(answer);

                StringTokenizer tok = new StringTokenizer(answer);

                if ("-err".equalsIgnoreCase(tok.nextToken())) {
                    throw new PopException("Error in STAT: " + answer);
                } else {
                    try {
                        messages = Integer.parseInt(tok.nextToken().trim());
                    } catch (Exception e) {
                        messages = -1;
                    }
                }
            }
        } catch (IOException ioe) {
            throw new PopException("an I/O error occurred", ioe);
        }

        return messages;
    }

    /**
     * Gets a reader that may be used to read message.  Once this method is invoked you MAY NOT invoke any other methods
     * until you have fully read the message.  Failure to comply will result in a corrupt POP session.  PopMessageProgressEvent and
     * PopMessageEvent events will not be fired when using this method.
     *
     * @param msg the message id of message
     *
     * @return a PopMessageReader
     *
     * @throws PopException if an I/O or POP3 related error occurs.
     * @see com.jscape.inet.pop.PopMessageReader
     */
    public PopMessageReader getMessageReader(int msg)
            throws PopException {
        PopMessageReader reader = null;

        try {
            issueCommand("RETR " + msg, false);

            String answer = readLine(in);
            firePopResponse(answer);
            debug(answer);

            if (answer.toLowerCase(Locale.ENGLISH).startsWith("-err")) {
                throw new PopException("Error in RETR: " + answer);
            } else {
                reader = new PopMessageReader(in);
            }
        } catch (IOException ioe) {
            throw new PopException("an I/O error occured.");
        }

        return reader;
    }

    /**
     * Gets size of message in bytes from POP3 server. Message id is always one
     * (1) based, first message id beginning with 1, second with 2 and so on.
     *
     * @param msg message id of message
     *
     * @return the size of email message in bytes
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public int getMessageSize(int msg)
            throws PopException {
        int size = 0;

        try {
            synchronized (this) {
                issueCommand("LIST " + msg, false);

                String answer = readLine(in);
                debug(answer);
                firePopResponse(answer);

                StringTokenizer tok = new StringTokenizer(answer);

                if ("-err".equalsIgnoreCase(tok.nextToken())) {
                    throw new PopException("Error in LIST: " + answer);
                } else {
                    try {
                        // get message id
                        tok.nextToken();

                        // get message size
                        size = Integer.parseInt(tok.nextToken().trim());
                    } catch (Exception e) {
                        size = -1;
                    }
                }
            }
        } catch (IOException ioe) {
            throw new PopException("an I/O error occurred", ioe);
        }

        return size;
    }

    /**
     * Gets all available messages in mailbox from POP3 server.
     *
     * @return an Enumeration of EmailMessage
     *
     * @throws PopException if I/O or POP3 related error occurs
     * @see EmailMessage
     */
    @SuppressWarnings("unchecked")
    public Enumeration getMessages()
            throws PopException {
        Vector messages = new Vector();

        synchronized (this) {
            int count = getMessageCount();

            for (int i = 1; i <= count; ++i) {
                messages.addElement(getMessage(i));
            }
        }

        return messages.elements();
    }

    /**
     * Sets the password to be used in authentication to POP3 server.
     *
     * @param password the password of POP3 user
     */
    public void setPassword(String password) {
        synchronized (this) {
            this.password = password;
        }
    }

    /**
     * Sets the port of the POP3 server. Default value is 110.
     *
     * @param port the port of POP3 server
     */
    public void setPort(int port) {
        this.port = port;
    }

    /**
     * Gets the port of the POP3 server. Default value is 110.
     *
     * @return port
     */
    public int getPort() {
        return port;
    }

    /**
     * Sets the timeout for reading responses from POP server.
     * Default is 30 seconds.
     *
     * @param timeout the timeout in milliseconds
     */
    public void setReadTimeout(int timeout) {
        this.readTimeout = timeout;
    }

    /**
     * Gets the timeout used when reading responses from POP server.
     * Default is 30 seconds.
     *
     * @return reading from server timeout.
     */
    public int getReadTimeout() {
        return this.readTimeout;
    }

    /**
     * Sets the timeout for opening a POP3 server connection and reading responses from POP server.
     * Default is 30 seconds.
     *
     * @param timeout the timeout in milliseconds
     *
     * @see #setConnectTimeout
     * @see #setReadTimeout
     * @deprecated Replaced by setConnectTimeout and setReadTimeout
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
        setConnectTimeout(timeout);
        setReadTimeout(timeout);
    }

    /**
     * Gets the timeout used when opening a POP3 server connection and reading responses from POP server.
     * Default is 30 seconds.
     *
     * @return timeout
     *
     * @see #getConnectTimeout
     * @see #getReadTimeout
     * @deprecated Replaced by getConnectTimeout and getReadTimeout
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * Gets summary of message from POP3 server. Used primary to peek at a
     * message without downloading entire contents. Message id is always one
     * (1) based, first message id beginning with 1, second with 2 and so on.
     *
     * @param msg   message id
     * @param lines number of lines to retrieve
     *
     * @return portion of message
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public String getTop(int msg, int lines)
            throws PopException {
        return issueCommandMultiLine("TOP " + msg + " " + lines);
    }

    /**
     * Gets unique message id for a message on POP3 server.
     *
     * @param msg the message number to get the unique message id for. Message number is always (1)
     *            based, first message number is 1, second is 2 and so on.
     *
     * @return a unique message id
     *
     * @throws PopException if an I/O or POP3 related error occurs
     */
    public String getUid(int msg)
            throws PopException {
        String response = issueCommand("UIDL " + msg);
        StringTokenizer st = new StringTokenizer(response);

        while (st.hasMoreTokens()) {
            response = st.nextToken();
        }

        return response;
    }

    /**
     * Sets the username to be used in authentication to POP3 server.
     *
     * @param username the username of POP3 user
     */
    public void setUsername(String username) {
        synchronized (this) {
            this.username = username;
        }
    }

    /**
     * Gets the username to be used in authentication to POP3 server.
     *
     * @return username
     */
    public String getUsername() {
        return username;
    }

    /**
     * Sets the username and password to use when for authentication with
     * proxy server.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyUsername the proxy username
     * @param proxyPassword the proxy password
     *
     * @see #clearProxySettings
     */
    public void setProxyAuthentication(String proxyUsername,
                                       String proxyPassword) {
        this.proxyUsername = proxyUsername;
        this.proxyPassword = proxyPassword;
    }

    // PROXY CONTROL

    /**
     * Sets the proxy hostname and port for this connection.
     * To clear these settings invoke the <code>#clearProxySettings</code> method.
     *
     * @param proxyHostname the hostname or ip address of the proxy server
     * @param proxyPort     the port of the proxy server
     *
     * @see #clearProxySettings
     */
    public void setProxyHost(String proxyHostname, int proxyPort) {
        this.proxyHostname = proxyHostname;
        this.proxyPort = proxyPort;
    }

    /**
     * Sets the proxy type will be used for this connection.
     *
     * @param proxyType The proxy type. Valid values: HTTP, SOCKS5
     */
    public void setProxyType(String proxyType) {
        this.proxyType = proxyType;
    }

    /**
     * Clears proxy server values.
     */
    public void clearProxySettings() {
        setProxyAuthentication(null, null);
        setProxyHost(null, DEFAULT_PROXY_PORT);
        setProxyType(DEFAULT_PROXY_TYPE);
    }

    /**
     * Adds Pop event listener.
     *
     * @param listener a PopListener
     *
     * @see PopListener
     */
    @SuppressWarnings("unchecked")
    public synchronized void addPopListener(PopListener listener) {
        listeners.addElement(listener);
    }

    /**
     * Connects to POP3 server and performs login.
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public Pop connect()
            throws PopException {
        try {
            synchronized (this) {
                if (isConnected()) {
                    throw new PopException("Already connected to host " + hostname + ":" + port);
                }

                Connector<SyncRawConnection> rawConnector = rawConnector();
                this.connection = rawConnector.connect();

                in = new BufferedInputStream(new SyncRawConnection.ConnectionInputStream(this.connection));
                out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(new SyncRawConnection.ConnectionOutputStream(this.connection))), true);

                String banner = readLine(in);
                debug(banner);

                this.authentication.apply(this, banner);

                this.firePopConnected();
            }

            return this;
        } catch (Exception ioe) {
            throw new PopException("an I/O error occurred", ioe);
        }
    }

    /**
     * Deletes message from POP3 server.
     *
     * @param msg message id of message
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public void deleteMessage(int msg)
            throws PopException {
        issueCommand("DELE " + msg);
    }

    /**
     * Disconnects from POP3 server. Issues QUIT command to POP3 server.
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public void disconnect()
            throws PopException {
        if (!isConnected()) {
            return;
        }

        try {
            synchronized (this) {
                issueCommand("QUIT", true);
                in.close();
                out.close();
                connection.close();
            }
        } catch (IOException ioe) {
            throw new PopException("an I/O error occurred", ioe);
        } catch (NullPointerException npe) {
            throw new PopException("POP client already disconnected");
        } finally {
            out = null;
            in = null;
            connection = null;
            this.firePopDisconnected();
        }
    }

    @Override
    public void close()
            throws Exception {
        disconnect();
    }

    /**
     * Issues a command to POP3 server and reads response.
     *
     * @param command a POP3 command
     *
     * @return POP3 server response
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public String issueCommand(String command)
            throws PopException {
        return issueCommand(command, true);
    }

    /**
     * Issues a command to POP3 server and optionally reads response.
     *
     * @param command      a POP3 command
     * @param readResponse <code>true</code> if should read response
     *                     <code>false</code> otherwise
     *
     * @return POP3 server response
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public String issueCommand(String command, boolean readResponse)
            throws PopException {
        String response = null;

        try {
            synchronized (this) {
                debug(command);
                command += "\r\n";
                out.print(command);
                out.flush();
                firePopCommand(command);

                if (readResponse) {
                    response = readLine(in);
                    debug(response);
                    firePopResponse(response);

                    if (!response.toLowerCase(Locale.ENGLISH).startsWith("+")) {
                        throw new PopException("Error in " + command + ": " + response);
                    }
                }
            }
        } catch (IOException ioe) {
            throw new PopException("an I/O error occurred in " + command, ioe);
        }

        return response;
    }

    /**
     * Issues a command to POP3 server and reads a multi-line response. This
     * command should only be used for known POP3 commands that issue a known
     * multi-line response such as LIST and TOP.
     *
     * @param command a POP3 command
     *
     * @return POP3 server response
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public String issueCommandMultiLine(String command)
            throws PopException {
        StringBuffer response = new StringBuffer();

        try {
            synchronized (this) {
                issueCommand(command, false);

                String answer = readLine(in);
                debug(answer);

                firePopResponse(answer);

                if (answer.toLowerCase(Locale.ENGLISH).startsWith("-err")) {
                    throw new PopException("Error in " + command + ": " + answer);
                } else if (answer.toLowerCase(Locale.ENGLISH).startsWith("+ok")) {
                    while (true) {
                        answer = readLine(in);

                        if (answer.equals(".")) {
                            break;
                        } else {
                            response.append(answer);
                            response.append(CRLF);
                        }
                    }
                }
            }
        } catch (IOException ioe) {
            throw new PopException("an I/O error occurred", ioe);
        }

        return response.toString();
    }

    /**
     * Removes specified Pop event listener.
     *
     * @param listener the PopListener to remove
     *
     * @see PopListener
     */
    public synchronized void removePopListener(PopListener listener) {
        if (listeners.contains(listener)) {
            listeners.removeElement(listener);
        }
    }

    protected Connector<SyncRawConnection> rawConnector() {
        ConnectionDescriptor connectionDescriptor = new ConnectionDescriptor(new TransportAddress(this.hostname, this.port), this.connectTimeout);
        SocketParameters socketParameters = SocketParameters.defaultParameters();

        if (this.proxyType != null && this.proxyHostname != null) {
            ProxyDescriptor proxyDescriptor = new ProxyDescriptor(this.proxyHostname,
                                                                  this.proxyPort,
                                                                  this.proxyUsername,
                                                                  this.proxyPassword);
            Proxy proxy = Proxies.proxyFor(this.proxyType);
            return new ProxySocketConnector(proxyDescriptor, proxy, connectionDescriptor, socketParameters);
        } else {
            return new SocketConnector(connectionDescriptor, socketParameters);
        }
    }

    /**
     * Prints debugging information.
     *
     * @param line the message to print
     */
    protected void debug(String line) {
        if (debug) {
            if (line.startsWith("PASS")) {
                debugStream.println("PASS ******");
            } else {
                debugStream.println(line);
            }
        }
    }

    /**
     * Fires PopCommandEvent. This event is fired upon sending a command to
     * POP3 server.
     *
     * @param command the command sent to POP3 server
     *
     * @see PopCommandEvent
     */
    protected void firePopCommand(String command) {
        Vector thisList;
        PopCommandEvent thisEvent = new PopCommandEvent(this, command);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((PopListener) thisList.elementAt(elem)).commandSent(thisEvent);
        }
    }

    /**
     * Fires PopConnectedEvent. This event is fired upon connecting to POP3
     * server.
     *
     * @see com.jscape.inet.pop.PopConnectedEvent
     */
    protected void firePopConnected() {
        Vector thisList;
        PopConnectedEvent thisEvent = new PopConnectedEvent(this, getHostname());

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((PopListener) thisList.elementAt(elem)).connected(thisEvent);
        }
    }

    /**
     * Fires <code>PopDisconnectedEvent</code>. This event is fired upon
     * disconnecting from POP3 server.
     *
     * @see com.jscape.inet.pop.PopDisconnectedEvent
     */
    protected void firePopDisconnected() {
        Vector thisList;
        PopDisconnectedEvent thisEvent = new PopDisconnectedEvent(this,
                                                                  getHostname());

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((PopListener) thisList.elementAt(elem)).disconnected(thisEvent);
        }
    }

    /**
     * Fires PopMessageProgressEvent. This event is fired freqently during the retrieval of a message
     * from POP server
     *
     * @param msg        the message being retrieved
     * @param bytes      the number of bytes retrieved thus far
     * @param totalBytes the total length of the message in bytes
     *
     * @see PopMessageProgressEvent
     */
    protected void firePopMessageProgress(int msg, long bytes, long totalBytes) {
        Vector thisList;
        PopMessageProgressEvent thisEvent = new PopMessageProgressEvent(this,
                                                                        msg, bytes, totalBytes);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((PopListener) thisList.elementAt(elem)).progress(thisEvent);
        }
    }

    /**
     * Fires <code>PopMessageEvent</code>. This event is fired for each message
     * retrieved from POP3 server.
     *
     * @param message the email message retrieved
     *
     * @see com.jscape.inet.pop.PopMessageEvent
     * @see EmailMessage
     */
    protected void firePopMessageRetrieved(EmailMessage message) {
        Vector thisList;
        PopMessageEvent thisEvent = new PopMessageEvent(this, message);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((PopListener) thisList.elementAt(elem)).messageRetrieved(thisEvent);
        }
    }

    /**
     * Fires PopResponseEvent. This event is fired upon receiving response from
     * POP3 server.
     *
     * @param response the response sent by POP3 server
     *
     * @see PopResponseEvent
     */
    protected void firePopResponse(String response) {
        Vector thisList;
        PopResponseEvent thisEvent = new PopResponseEvent(this, response);

        // Make a copy of the list so potential changes to it by
        // other threads won't affect traversal.
        synchronized (this) {
            thisList = (Vector) listeners.clone();
        }

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((PopListener) thisList.elementAt(elem)).responseReceived(thisEvent);
        }
    }

    /**
     * Reads a CR LF terminated line from InputStream.
     *
     * @param input the InputStream
     *
     * @return the line read without CRLF
     *
     * @throws IOException if an I/O related error occurs
     */
    protected String readLine(InputStream input)
            throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        int b = 0;

        while (((b = input.read()) != LF) && (b != -1)) {
            if (b != CR) {
                bout.write((byte) b);
            }
        }

        return bout.toString();
    }

    /**
     * Strips leading periods from message retrieved from POP3 server.
     *
     * @param data the data to parse
     *
     * @return the parsed data
     */
    private static byte[] stripLeadingPeriods(byte[] data) {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        byte prev2 = -1;
        byte prev = -1;
        byte curr = -1;
        boolean leadingPeriod = false;

        for (int i = 0; i < data.length; ++i) {
            // set current byte
            curr = data[i];

            // check if current byte is a period
            if (curr == PERIOD) {
                // check if previous byte is a LF
                if ((prev == LF) && (prev2 == CR)) {
                    // current byte is a leading period
                    leadingPeriod = true;
                    bout.write(curr);
                } else if (prev == PERIOD) {
                    if (leadingPeriod) {
                        // leading period is on and current byte is period .. ignore this period and reset
                        leadingPeriod = false;
                    } else {
                        // reset leading period
                        leadingPeriod = false;

                        // write data to array
                        bout.write(curr);
                    }
                } else {
                    // reset leading period
                    leadingPeriod = false;

                    // write data to array
                    bout.write(curr);
                }
            } else {
                // current byte is not a period

                // reset leading period 
                leadingPeriod = false;

                // write data to array
                bout.write(curr);
            }

            prev2 = prev;
            prev = curr;
        }

        return bout.toByteArray();
    }

    @SuppressObfuscation
    public enum Authentication {

        BASIC(0) {
            @Override
            public void apply(Pop client, String banner)
                    throws Exception {
                synchronized (client) {
                    client.issueCommand("USER " + client.username);
                    client.issueCommand("PASS " + client.password);
                }
            }
        },
        APOP(1) {
            @Override
            public void apply(Pop client, String banner)
                    throws Exception {
                if (banner == null) {
                    throw new PopException("Error in APOP: invalid POP3 server timestamp");
                }

                int startTimestamp = banner.indexOf("<");
                int endTimestamp = banner.indexOf(">", startTimestamp);
                String timestamp = banner.substring(startTimestamp, endTimestamp + 1);
                MD5 digest = new MD5(timestamp + client.password);

                synchronized (this) {
                    client.issueCommand("APOP " + client.username + " " + digest.getHash());
                }
            }
        },
        XOAUTH2(2) {
            @Override
            public void apply(Pop client, String banner)
                    throws Exception {
                synchronized (client) {
                    client.issueCommand("AUTH XOAUTH2");

                    String credentials = String.format("user=%s\001auth=Bearer %s\001\001", client.username, client.password);
                    client.issueCommand(Base64Format.formatData(credentials.getBytes(StandardCharsets.UTF_8)));
                }
            }
        };

        public final int code;

        public static Authentication authenticationFor(int code) {
            for (Authentication candidate : values()) {
                if (candidate.code == code) {
                    return candidate;
                }
            }

            throw new IllegalArgumentException(String.format("Unsupported authentication code: %s.", code));
        }

        Authentication(int code) {
            this.code = code;
        }

        public abstract void apply(Pop client, String banner)
                throws Exception;

    }
}
