/*
 * @(#)Imap.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.imap;

import com.jscape.inet.email.EmailMessage;
import com.jscape.inet.mime.MimeHeader;
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.Base64;
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 IMAP client.
 * <p/>
 * Example Usage:
 * <p/>
 * <pre>
 * // new instance with hostname, username and password
 * Imap imap = new Imap("imap4.myserver.com","jsmith","secret");
 * try {
 *   // connect to IMAP4 server
 *   imap.connect();
 *
 *   // get messages
 *   int messageCount = imap.getMessageCount();
 *   for(int i = 1; i <= messageCount(); ++i) {
 *     EmailMessage em = imap.getMessage(i);
 *   }
 *
 *   // disconnect
 *   imap.disconnect();
 * }
 * catch(Exception e) {
 *   System.out.println(e);
 * }
 * </pre>
 */
@SuppressWarnings("unchecked")
public class Imap
        implements AutoCloseable,
                   Serializable {

    /**
     * Represents a carriage return.
     */
    protected static final byte CR = 13;

    /**
     * Represents a line feed.
     */
    protected static final byte LF = 10;

    protected static final String CRLF = "\r\n";

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

    /**
     * Flags.
     */
    private static final String[] FLAGS = {
            "\\Seen", "\\Answered",
            "\\Flagged", "\\Deleted", "\\Draft", "\\Recent"
    };

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

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

    public static final int AUTH_XOAUTH2 = 2;

    /**
     * Flag mode used to replace flags.
     */
    public static final int FLAG_MODE_REPLACE = 0;

    /**
     * Flag mode used to add flags.
     */
    public static final int FLAG_MODE_ADD = 1;

    /**
     * Flag mode used to delete flags.
     */
    public static final int FLAG_MODE_DELETE = 2;

    /**
     * Flag modes.
     */
    private static final String[] FLAG_MODES = { "FLAGS", "+FLAGS", "-FLAGS" };

    /**
     * Flag used in marking a message as Seen.
     */
    public static final int FLAG_SEEN = 0;

    /**
     * Flag used in marking a message as Answered.
     */
    public static final int FLAG_ANSWERED = 1;

    /**
     * Flag used in marking a message as Flagged.
     */
    public static final int FLAG_FLAGGED = 2;

    /**
     * Flag used in marking a message as Deleted.
     */
    public static final int FLAG_DELETED = 3;

    /**
     * Flag used in marking a message as Draft.
     */
    public static final int FLAG_DRAFT = 4;

    /**
     * Flag used in marking a message as Recent.
     */
    public static final int FLAG_RECENT = 5;

    protected transient SyncRawConnection connection;
    protected transient BufferedInputStream in;
    protected transient BufferedOutputStream out;
    protected transient Vector imapListeners = new Vector();
    protected String currentMailbox = "";
    protected String hostname = "";
    protected String password = "";
    protected String username = "";
    protected Authentication authentication = Authentication.BASIC;
    protected int connectTimeout = 30000;
    protected int port = 143;
    protected int readTimeout = 30000;
    protected int timeout = 30000;
    protected transient PrintStream debugStream = System.out;
    protected String proxyHostname = null;
    protected int proxyPort = DEFAULT_PROXY_PORT;
    protected String proxyUsername = null;
    protected String proxyPassword = null;
    protected String proxyType = DEFAULT_PROXY_TYPE;
    protected boolean delete;

    /**
     * This is for debugging purposes only. If true, all traffic to/from IMAP server is
     * written to standard output.
     */
    protected boolean debug = false;

    /**
     * This variable is used to construct incrementing command identifiers, as required by
     * IMAP specification.
     */
    protected int commandCount = 0;

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

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

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

        return this;
    }

    /**
     * Sets the authentication mode used when logging into IMAP4 server.
     * Possible values are Imap.AUTH_LOGIN for username/password authentication
     * or Imap.AUTH_CRAM_MD5 for CRAM-MD5 authentication.
     *
     * @param mode authentication mode
     */
    public void setAuthMode(int mode) {
        this.authentication = Authentication.authenticationFor(mode);
    }

    /**
     * Gets the authentication mode used when logging into IMAP4 server.
     * Possible values are Imap.AUTH_LOGIN for username/password authentication
     * or Imap.AUTH_CRAM_MD5 for CRAM-MD5 authentication.
     *
     * @return authentication mode
     */
    public int getAuthMode() {
        return this.authentication.code;
    }

    /**
     * Sets the timeout for opening the 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 connection.
     * <p/>
     * Default is 30 seconds.
     *
     * @return timeout server connection.
     */
    public int getConnectTimeout() {
        return this.connectTimeout;
    }

    /**
     * True, if there is a connection to the IMAP server.
     *
     * @return state of the connection
     *
     * @see #connect
     * @see #disconnect
     */
    public boolean isConnected() {
        return (in != 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 IMAP session.
     * Debug messages are sent to debug stream.  Default 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 debug stream
     *
     * @see PrintStream
     */
    public PrintStream getDebugStream() {
        return this.debugStream;
    }

    /**
     * Defines whether to automatically mark messages for deletion after retrieval.  Messages will NOT
     * be deleted until expunge method is invoked.
     * Default is <code>false</code>.
     *
     * @param delete <code>true</code> to delete automatically
     *
     * @see #expunge
     */
    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 this.delete;
    }

    /**
     * Sets the name of the remote host to cennect to.
     *
     * @param hostname the hostname of IMAP server
     */
    public void setHostname(String hostname) {
        this.hostname = hostname;
    }

    /**
     * Gets the name of the remote host.
     *
     * @return <code>remoteHost</code> to connect to.
     */
    public String getHostname() {
        return hostname;
    }

    /**
     * Gets all mailboxes found for account.
     *
     * @return an Enumeration of String with each element representing a mailbox name
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see #listMailboxes
     * @deprecated this method uses the FIND ALL.MAILBOXES command as identifed in RFC 1730 and has been obsoleted by RFC 2060, it is recommended you use the listMailboxes command instead.
     */
    @SuppressWarnings("unchecked")
    public Enumeration getMailboxes()
            throws ImapException {
        Vector v = new Vector();
        String response = issueCommand("FIND ALL.MAILBOXES *");
        StringTokenizer st = new StringTokenizer(response, "\r\n");

        while (st.hasMoreTokens()) {
            String mailbox = st.nextToken();

            if ((mailbox != null) && (mailbox.length() > 10)) {
                mailbox = mailbox.substring(10);
                v.addElement(mailbox);
            }
        }

        return v.elements();
    }

    /**
     * Gets all mailboxes for account.
     *
     * @return an Enumeration of String with each element representing a mailbox name
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public Enumeration listMailboxes()
            throws ImapException {
        return listMailboxes(null, null);

    }

    /**
     * Gets all mailboxes for account.
     *
     * @param mailbox the root mailbox to search under or null to return all mailboxes
     * @param filter  the filter to use or null to return all mailboxes
     *
     * @return an Enumeration of String with each element representing a mailbox name
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    @SuppressWarnings("unchecked")
    public Enumeration listMailboxes(String mailbox, String filter)
            throws ImapException {
        Vector v = new Vector();
        if (mailbox == null || mailbox.length() == 0) {
            mailbox = "\"\"";
        }

        if (filter == null || filter.length() == 0) {
            filter = "*";
        }

        String response = issueCommand("LIST " + mailbox + " " + filter);
        StringTokenizer st = new StringTokenizer(response, "\r\n");

        while (st.hasMoreElements()) {
            String box = st.nextToken();


            if (box.endsWith("\"")) {
                char[] values = box.toCharArray();
                int index = -1;
                for (int i = values.length - 2; i > 0; i--) {
                    if (values[i] == '\"') {
                        index = i;
                        break;
                    }
                }
                box = box.substring(index + 1, box.length() - 1);
            } else {
                // find last instance of space character
                int last = box.lastIndexOf(" ");
                box = box.substring(last).trim();
            }

            if (box.startsWith("\"") && box.endsWith("\"")) {
                box = box.substring(1, box.lastIndexOf("\""));
            }

            v.addElement(box);
        }

        return v.elements();
    }

    /**
     * Retrieves a message from the current mailbox. The method retrieves the
     * message with the given index from the IMAP server. Message numbering
     * is one based, meaning that message numbers range is from 1 to
     * <code>getMessageCount()</code>.
     *
     * @param index the index of the message to retrieve
     *
     * @return an <CODE>EmailMessage</CODE>
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see #getMessageCount
     * @see EmailMessage
     */
    public EmailMessage getMessage(int index)
            throws ImapException {
        synchronized (this) {
            EmailMessage message = null;
            String response = null;
            String tag = "A" + commandCount++;
            String command = tag + " " + "FETCH " + (index) + " (RFC822)";

            try {
                if (!isConnected()) {
                    throw new ImapException("not connected to IMAP server");
                } else {
                    out.write((command + CRLF).getBytes(StandardCharsets.US_ASCII));
                    out.flush();

                    // send command notification
                    fireImapCommand(command);
                    debug("C: " + command);
                }

                response = readLine(in);
                debug("S: " + response);

                if (response.startsWith(tag + " BAD ")
                        || response.startsWith(tag + " NO ")) {
                    throw new ImapException(response);
                }

                while (!response.startsWith("* " + index + " FETCH")) {
                    response = readLine(in);
                    debug("S: " + response);
                }

                int left = response.indexOf('{');
                int right = response.indexOf('}', left);
                int totalBytes = Integer.parseInt(response.substring(left + 1,
                                                                     right));

                ByteArrayOutputStream msg = new ByteArrayOutputStream();

                // read contents of message
                for (int i = 0; i < totalBytes; ++i) {
                    // TODO could do some optimizations here
                    int b = in.read();
                    msg.write((byte) b);

                    if ((i != 0) && ((i % 1024) == 0)) {
                        this.fireImapMessageProgress(index, i, totalBytes);
                    }
                }

                this.fireImapMessageProgress(index, totalBytes, totalBytes);
                message = new EmailMessage(msg.toByteArray());

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

                // get final response ... must read 2 lines one for ending paren and one for actual response
                while (true) {
                    response = readLine(in);

                    if (response.startsWith(tag + " OK")) {
                        break;
                    }
                }

                debug("S: " + response);
                fireImapResponse(response);

                // delete message if flag set
                if (delete) {
                    deleteMessage(index);
                }
            } catch (Exception e) {
                throw new ImapException(e.getMessage(), e);
            }

            return message;
        }
    }

    /**
     * Gets the number of messages in the current mailbox.
     *
     * @return the number of message in currently selected mailbox
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public int getMessageCount()
            throws ImapException {
        int messageCount = -1;
        String command = "STATUS";
        String args = quote(currentMailbox) + " (MESSAGES)";

        synchronized (this) {
            String buffer = issueCommand(command + " " + args);
            String answer = getAnswer(command, buffer);

            if (answer != null) {
                int space = answer.indexOf(' ');
                messageCount = Integer.parseInt(answer.substring(space + 1));
            }

            return messageCount;
        }
    }

    /**
     * Gets named header for a message.
     *
     * @param index the index of the message to retrieve
     * @param name  the name of the header to retrieve
     *
     * @return a MimeHeader
     *
     * @throws ImapException If a Imap error occurs.
     * @see MimeHeader
     */
    public MimeHeader getMessageHeader(int index, String name)
            throws ImapException {
        MimeHeader header = null;

        if (name == null) {
            throw new ImapException("Invalid header name.");
        }

        name = name.toLowerCase(Locale.ENGLISH);

        Enumeration e = getMessageHeaders(index);

        while (e.hasMoreElements()) {
            MimeHeader tmp = (MimeHeader) e.nextElement();

            if (tmp.getName().toLowerCase(Locale.ENGLISH).equals(name)) {
                header = tmp;

                break;
            }
        }

        return header;
    }

    /**
     * Gets message headers for a message.
     *
     * @param index the index of message to retrieve
     *
     * @return an Enumeration of MimeHeader
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see Enumeration
     * @see MimeHeader
     */
    public Enumeration getMessageHeaders(int index)
            throws ImapException {
        synchronized (this) {
            String tag = "A" + commandCount++;
            String command = tag + " FETCH " + index + " (RFC822.HEADER)";
            String response = null;
            EmailMessage message = null;

            try {
                if (!isConnected()) {
                    throw new ImapException("not connected to IMAP server");
                } else {
                    out.write((command + CRLF).getBytes(StandardCharsets.US_ASCII));
                    out.flush();

                    // send command notification
                    fireImapCommand(command);
                    debug("C: " + command);
                }

                response = readLine(in);
                debug("S: " + response);

                if (response.startsWith(tag + " BAD ")
                        || response.startsWith(tag + " NO ")) {
                    throw new ImapException(response);
                }

                while (!response.startsWith("* " + index + " FETCH")) {
                    response = readLine(in);
                    debug("S: " + response);
                }

                int left = response.indexOf('{');
                int right = response.indexOf('}', left);
                int totalBytes = Integer.parseInt(response.substring(left + 1,
                                                                     right));

                ByteArrayOutputStream msg = new ByteArrayOutputStream();

                // read contents of message
                for (int i = 0; i < totalBytes; ++i) {
                    // TODO could do some optimizations here
                    int b = in.read();
                    msg.write((byte) b);
                }

                message = new EmailMessage(msg.toByteArray());

                // get final response ... must read 2 lines one for ending paren and one for actual response
                while (true) {
                    response = readLine(in);

                    if (response.startsWith(tag + " OK")) {
                        break;
                    }
                }

                debug("S: " + response);
                fireImapResponse(response);
            } catch (Exception e) {
                throw new ImapException(e.getMessage(), e);
            }

            return message.getHeaders();
        }
    }

    /**
     * Gets messages matching specified SEARCH expression.
     *
     * @param expression a valid SEARCH key
     *
     * @return an int[] of message ID's matching expression
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    @SuppressWarnings("unchecked")
    public int[] getMessageID(String expression)
            throws ImapException {
        int[] ids = null;
        Vector messages = new Vector();

        synchronized (this) {
            String response = issueCommand("SEARCH " + expression);
            int startIndex = response.indexOf("SEARCH");
            int endIndex = response.indexOf(CRLF, startIndex);
            StringTokenizer str = new StringTokenizer(response.substring(
                    startIndex, endIndex));
            str.nextToken();

            while (str.hasMoreTokens()) {
                Integer tmp = new Integer(str.nextToken());
                messages.add(tmp);
            }

            if (messages.size() != 0) {
                ids = new int[messages.size()];
            }

            for (int i = 0; i < messages.size(); ++i) {
                Integer tmp = (Integer) messages.elementAt(i);
                ids[i] = tmp.intValue();
            }

            return ids;
        }
    }

    /**
     * Gets message from current mailbox along with message flags. Message
     * number is one based, meaning that message numbers range from 1 to
     * <code>getMessageCount()</code>
     *
     * @param index Message index
     *
     * @return an ImapMessage
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see #getMessageCount
     * @see ImapMessage
     */
    public ImapMessage getMessageWithFlags(int index)
            throws ImapException {
        String flgs = issueCommand("FETCH " + index + " FLAGS");
        EmailMessage message = getMessage(index);

        return new ImapMessage(message, index, flgs);
    }

    /**
     * Gets messages for specified message ID's.
     *
     * @param ids the message ID's to retrieve
     *
     * @return an Enumeration of EmailMessage
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see EmailMessage
     */
    @SuppressWarnings("unchecked")
    public Enumeration getMessages(int[] ids)
            throws ImapException {
        Vector messages = new Vector();

        synchronized (this) {
            if (ids != null) {
                for (int i = 0; i < ids.length; ++i) {
                    EmailMessage message = getMessage(ids[i]);
                    messages.add(message);
                }
            }

            return messages.elements();
        }
    }

    /**
     * Gets messages matching specified SEARCH expression.
     *
     * @param expression a valid SEARCH key
     *
     * @return an Enumeration of EmailMessage
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see EmailMessage
     */
    public Enumeration getMessages(String expression)
            throws ImapException {
        return getMessages(getMessageID(expression));
    }

    /**
     * Gets all available messages in current mailbox from IMAP server.
     * All messages are stored in memory resulting in potential memory issues if
     * not enough memory is available.  To avoid potential memory issue you may retrieve messages
     * individually using the <code>getMessage(int index)</code> method.
     *
     * @return an Enumeration of <code>EmailMessage</code>
     *
     * @throws ImapException if I/O or IMAP related error occurs
     * @see EmailMessage
     */
    public Enumeration getMessages()
            throws ImapException {
        Vector messages = new Vector();

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

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

            return messages.elements();
        }
    }

    /**
     * Gets messages for specified message ID's with flags.
     *
     * @param ids the message ID's to retrieve
     *
     * @return an Enumeration of ImapMessage
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see ImapMessage
     */
    public Enumeration getMessagesWithFlags(int[] ids)
            throws ImapException {
        Vector messages = new Vector();

        synchronized (this) {
            if (ids != null) {
                for (int i = 0; i < ids.length; ++i) {
                    ImapMessage message = getMessageWithFlags(ids[i]);
                    messages.add(message);
                }
            }

            return messages.elements();
        }
    }

    /**
     * Gets messages with flags matching specified SEARCH expression.
     *
     * @param expression a valid SEARCH key
     *
     * @return an Enumeration of ImapMessage
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see ImapMessage
     */
    public Enumeration getMessagesWithFlags(String expression)
            throws ImapException {
        return getMessagesWithFlags(getMessageID(expression));
    }

    /**
     * Gets all available messages with flags in current mailbox from IMAP server.
     * All messages are stored in memory resulting in potential memory issues if
     * not enough memory is available.  To avoid potential memory issue you may retrieve messages
     * individually using the <code>getMessageWithFlags(int index)</code> method.
     *
     * @return an Enumeration of <code>ImapMessage</code>
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see ImapMessage
     */
    public Enumeration getMessagesWithFlags()
            throws ImapException {
        Vector messages = new Vector();

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

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

            return messages.elements();
        }
    }

    /**
     * Gets an int[] representing the ID(s) of new messages in current mailbox from IMAP server.
     * Each ID in int[] represents the corresponding new message id on server.  You can then retrieve
     * the new message using the getMessage method passing the message ID as an argument.  To get all new
     * messages automatically use the getNewMessages method.
     *
     * @return an int[] of all new message ID(s) or null if no new messages are found
     *
     * @throws ImapException if I/O or IMAP related error occurs
     * @see #getMessage
     * @see #getNewMessages
     */
    public int[] getNewMessageID()
            throws ImapException {
//        return getMessageID("NEW");
        return getMessageID("UNSEEN");
    }

    /**
     * Gets all new messages in current mailbox from IMAP server.
     * Upon retrieving new messages the messages are flagged as "read" and are no longer considered new.
     * To get the ID(s) of the new messages without retrieving the actual message content use the
     * getNewMessageID method.
     *
     * @return an Enumeration of EmailMessage
     *
     * @throws ImapException if I/O or IMAP related error occurs
     * @see EmailMessage
     * @see #getNewMessageID
     */
    public Enumeration getNewMessages()
            throws ImapException {
        return getMessages(getNewMessageID());
    }

    /**
     * Gets all new messages in current mailbox with message flags.
     *
     * @return an Enumeration of ImapMessage
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see ImapMessage
     */
    public Enumeration getNewMessagesWithFlags()
            throws ImapException {
        return getMessagesWithFlags(getNewMessageID());
    }

    /**
     * Sets the password to be sent to the remote host.
     *
     * @param password to be sent to the remote host.
     */
    public void setPassword(String password) {
        this.password = password;
    }

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

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

    /**
     * Sets the timeout for reading responses from 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 server.
     * Default is 30 seconds.
     *
     * @return reading from server timeout.
     */
    public int getReadTimeout() {
        return this.readTimeout;
    }

    /**
     * Sets the timeout for opening connection to 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 for opening connection to server.
     * Default is 30 seconds.
     *
     * @return timeout in milliseconds
     *
     * @see #getConnectTimeout
     * @see #getReadTimeout
     * @deprecated Replaced by getConnectTimeout and getReadTimeout
     */
    public int getTimeout() {
        return timeout;
    }

    /**
     * Gets unique message id for a message on IMAP server.
     *
     * @param msgId 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 ImapException if an I/O or IMAP related error occurs
     */
    public int getUid(int msgId)
            throws ImapException {
        try {
            String command = "UID SEARCH " + msgId;
            String response = issueCommand(command);
            StringTokenizer st = new StringTokenizer(response);

            // get asterisk
            st.nextToken();

            // get SEARCH
            st.nextToken();

            // get ID
            return Integer.parseInt(st.nextToken());
        } catch (Exception e) {
            throw new ImapException(e.getMessage(), e);
        }
    }

    /**
     * Gets unique message id's for a range of messages on IMAP server.
     *
     * @param startMsgId the starting 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.
     * @param endMsgId   the ending message number to get the unique message id for
     *
     * @return a int[] of unique message id's
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public int[] getUid(int startMsgId, int endMsgId)
            throws ImapException {
        try {
            String command = "UID SEARCH " + startMsgId + ":" + endMsgId;
            String response = issueCommand(command);
            StringTokenizer st = new StringTokenizer(response);
            Vector ids = new Vector();

            // get asterisk
            st.nextToken();

            // get SEARCH
            st.nextToken();

            while (st.hasMoreElements()) {
                Integer value = new Integer(st.nextToken());
                ids.addElement(value);
            }

            int[] values = new int[ids.size()];

            for (int i = 0; i < ids.size(); ++i) {
                Integer val = (Integer) ids.elementAt(i);
                values[i] = val.intValue();
            }

            return values;
        } catch (Exception e) {
            throw new ImapException(e.getMessage(), e);
        }
    }

    /**
     * Sets the username to be sent to the remote host.
     *
     * @param username to be sent to the remote host.
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * Gets the username to be sent to the remote host.
     *
     * @return <code>username</code> to be sent.
     */
    public String getUsername() {
        return username;
    }

    /**
     * Adds Imap event listener.
     *
     * @param listener the listener to add
     *
     * @see ImapListener
     */
    public synchronized void addImapListener(ImapListener listener) {
        imapListeners.addElement(listener);
    }

    /**
     * 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);
    }

    /**
     * Appends an email message to the specified mailbox.
     *
     * @param mailbox  the mailbox to append to
     * @param messages the email message to append
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public void append(String mailbox, Enumeration messages)
            throws ImapException {
        synchronized (this) {
            while (messages.hasMoreElements()) {
                this.append(mailbox, (EmailMessage) messages.nextElement());
            }
        }
    }

    /**
     * Appends an email message to the specified mailbox.
     *
     * @param mailbox the mailbox to append to
     * @param message the email message to append
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public void append(String mailbox, EmailMessage message)
            throws ImapException {
        String tag = "A" + commandCount++;
        StringBuffer responseBuffer = new StringBuffer();
        mailbox = quote(mailbox);

        try {
            byte[] data = message.getMessage();
            String command = tag + " " + "APPEND " + mailbox + " {"
                    + data.length + "}";
            out.write((command + CRLF).getBytes(StandardCharsets.US_ASCII));
            out.flush();

            // send command notification
            debug("C: " + command);
            fireImapCommand(command);

            // get response
            String response = readLine(in);
            responseBuffer.append(response + CRLF);

            if (response.startsWith(tag + " BAD ")
                    || response.startsWith(tag + " NO ")) {
                throw new ImapException(response);
            }

            debug("S: " + response);

            // send message
            out.write(data);
            out.write(CRLF.getBytes(StandardCharsets.US_ASCII));
            out.flush();

            // get response
            response = readLine(in);
            responseBuffer.append(response + CRLF);

            if (response.startsWith(tag + " BAD ")
                    || response.startsWith(tag + " NO ")) {
                throw new ImapException(response);
            }

            debug("S: " + response);

            // fire response
            fireImapResponse(responseBuffer.toString());
        } catch (Exception me) {
            throw new ImapException(me.getMessage(), me);
        }
    }

    /**
     * Connects to an IMAP server.
     * <p/>
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public Imap connect()
            throws ImapException {
        String banner = null;

        synchronized (this) {
            if (isConnected()) {
                throw new ImapException("Already connected to host " + hostname + ":" + port);
            }

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

                in = new BufferedInputStream(new SyncRawConnection.ConnectionInputStream(this.connection));
                //output = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
                out = new BufferedOutputStream(new SyncRawConnection.ConnectionOutputStream(this.connection));

                // notify that client has connected
                fireImapConnected(hostname);
            } catch (Connector.OperationException e) {
                throw new ImapException("Unknown IMAP server: " + hostname, e);
            }

            //Authenticate using the given username and password.
            try {
                banner = readLine(in);
                debug("S: " + banner);

                this.authentication.apply(this);

                issueCommand("SELECT INBOX");
                currentMailbox = "INBOX";

                return this;
            } catch (IOException ioe) {
                throw new ImapException(ioe.getMessage(), ioe);
            } catch (ImapException e) {
                connection.close();
                in = null;
                out = null;
                connection = null;
                throw e;
            }
        }
    }

    /**
     * Creates a new mailbox on IMAP server.
     *
     * @param mailbox the name of the mailbox
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public void createMailbox(String mailbox)
            throws ImapException {
        issueCommand("CREATE " + quote(mailbox));
    }

    /**
     * Deletes mailbox on IMAP server.
     *
     * @param mailbox the name of the mailbox to delete
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public void deleteMailbox(String mailbox)
            throws ImapException {
        issueCommand("DELETE " + quote(mailbox));
    }

    /**
     * Removes a message from the current mailbox. The method marks the message with
     * the given index in the current mailbox for deletion.  To process messages marked for deletion
     * invoke the expunge() method.  Message numbering is one based, meaning that message numbers
     * range is from 1 to
     * <code>getMessageCount()</code>.
     *
     * @param index the index of message to mark for deletion
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see #getMessageCount
     * @see #expunge
     */
    public void deleteMessage(int index)
            throws ImapException {
        store(index, FLAG_MODE_ADD, FLAG_DELETED);
    }

    /**
     * Removes range of messages from the current mailbox. The method marks the messages within
     * the given start index and end index in the current mailbox for deletion.  To process messages
     * marked for deletion invoke the expunge() method.  Message numbering is one based, meaning that
     * message numbers range is from 1 to <code>getMessageCount()</code>.
     *
     * @param startIndex the start index of message range to mark for deletion
     * @param endIndex   the end index of message range to mark for deletion
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see #getMessageCount
     * @see #expunge
     */
    public void deleteMessages(int startIndex, int endIndex)
            throws ImapException {
        store(startIndex, endIndex, FLAG_MODE_ADD, FLAG_DELETED);
    }

    /**
     * Closes the IMAP session.
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see #connect
     */
    public void disconnect()
            throws ImapException {
        if (!isConnected()) {
            return;
        }

        try {
            issueCommand("CLOSE");
            issueCommand("LOGOUT");
            connection.close();
        } finally {
            in = null;
            out = null;
            connection = null;
            // notify that client has disconnected
            fireImapDisconnected(hostname);
        }
    }

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

    /**
     * Deletes messages marked for deletion from current mailbox.
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see #deleteMessage
     * @see #setDelete
     */
    public void expunge()
            throws ImapException {
        issueCommand("EXPUNGE");
    }

    /**
     * This method sends out a request and waits for a response.
     * The response with a "-" sign in place of the first character denotes
     * IMAP protocol error, and an <code>ImapException</code> is thrown to signal it.
     *
     * @param command to send to server
     *
     * @return string containing an answer message from the server. If NO, or BAD are received, an
     *         ImapException is raised to signal that command didn't have completed successfuly.
     *
     * @throws ImapException if an IMAP related error occurs
     */
    public String issueCommand(String command)
            throws ImapException {
        String tag = String.format("A%03d", ++commandCount);
        command = tag + " " + command;

        synchronized (this) {
            if (!isConnected()) {
                throw new ImapException("not connected to IMAP server");
            } else {
                try {
                    out.write((command + CRLF).getBytes(StandardCharsets.US_ASCII));
                    out.flush();
                } catch (IOException ioe) {
                    throw new ImapException(ioe.getMessage(), ioe);
                }

                // send command notification
                fireImapCommand(command);

                debug("C: " + command);
            }

            return readResponse(tag);
        }
    }

    private String readResponse(String tag)
            throws ImapException {
        String error = null;
        StringBuilder buffer = new StringBuilder();
        String response = null;

        try {
            response = readLine(in);

            if (response.startsWith(tag + " BAD ") || response.startsWith(tag + " NO ")) {
                error = response;
            }

            buffer.append(response);
            buffer.append(CRLF);
            debug("S: " + response);
        } catch (IOException ioe) {
            throw new ImapException(ioe.getMessage(), ioe);
        }

        //the tag denotes command success report, and is issued AFTER the response to that particular command
        if (error == null) {
            // XOAUTH2 response
            if (response.startsWith("+")) {
                return buffer.toString();
            }

            while (!response.startsWith(tag + " OK")) {
                // fix for servers that do not respond to LOGOUT command
                if (response.trim().equals("")) {
                    break;
                }

                try {
                    response = readLine(in); //receive command completion status
                    debug("S: " + response);

                    // do not include command completion in response
                    if (!response.startsWith(tag + " OK")) {
                        buffer.append(response);
                        buffer.append(CRLF);
                    }
                } catch (IOException e) {
                    throw new ImapException("IO error occured while communicating with server.\n" + e.toString());
                }
            }
        } else {
            throw new ImapException(error);
        }

        // send response notification
        fireImapResponse(buffer.toString());

        return buffer.toString();
    }

    /**
     * Removes specified IMAP event listener.
     *
     * @param listener the listener to remove
     *
     * @see ImapListener
     */
    public synchronized void removeImapListener(ImapListener listener) {
        imapListeners.removeElement(listener);
    }

    /**
     * Renames mailbox on IMAP server.
     *
     * @param mailbox    the name of mailbox to be renamed
     * @param newMailbox the new name of mailbox
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public void renameMailbox(String mailbox, String newMailbox)
            throws ImapException {
        issueCommand("RENAME " + quote(mailbox) + " " + quote(newMailbox));
    }

    /**
     * Selects mailbox on IMAP server.
     *
     * @param mailbox the name of the mailbox to select
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     */
    public void selectMailbox(String mailbox)
            throws ImapException {
        synchronized (this) {
            issueCommand("SELECT " + quote(mailbox));
            currentMailbox = mailbox;
        }
    }

    /**
     * Sets message flags for a single message in an IMAP mailbox.
     *
     * @param message the message id
     * @param mode    the flag mode
     * @param flag    the flag
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see #FLAG_MODE_REPLACE
     * @see #FLAG_MODE_ADD
     * @see #FLAG_MODE_DELETE
     * @see #FLAG_SEEN
     * @see #FLAG_ANSWERED
     * @see #FLAG_FLAGGED
     * @see #FLAG_DELETED
     * @see #FLAG_DRAFT
     * @see #FLAG_RECENT
     */
    public void store(int message, int mode, int flag)
            throws ImapException {
        store(message, message, mode, flag);
    }

    /**
     * Sets message flags for a range of messages in an IMAP mailbox.
     *
     * @param startMessage the start message id
     * @param endMessage   the end message id
     * @param mode         the flag mode
     * @param flag         the flag
     *
     * @throws ImapException if an I/O or IMAP related error occurs
     * @see #FLAG_MODE_REPLACE
     * @see #FLAG_MODE_ADD
     * @see #FLAG_MODE_DELETE
     * @see #FLAG_SEEN
     * @see #FLAG_ANSWERED
     * @see #FLAG_FLAGGED
     * @see #FLAG_DELETED
     * @see #FLAG_DRAFT
     * @see #FLAG_RECENT
     */
    public void store(int startMessage, int endMessage, int mode, int flag)
            throws ImapException {
        StringBuffer command = new StringBuffer();

        if (startMessage > endMessage) {
            throw new ImapException(
                    "Start message index larger than end message index in STORE.");
        } else if (startMessage == endMessage) {
            command.append("STORE " + startMessage);
        } else {
            command.append("STORE " + startMessage + ":" + endMessage);
        }

        // get flag mode
        try {
            command.append(" " + FLAG_MODES[mode]);
        } catch (ArrayIndexOutOfBoundsException ae) {
            throw new ImapException("Invalid mode " + mode + " in STORE.");
        }

        // get flags
        try {
            command.append(" (" + FLAGS[flag] + ")");
        } catch (ArrayIndexOutOfBoundsException ae) {
            throw new ImapException("Invalid flag " + flag + " in STORE.");
        }

        issueCommand(command.toString());
    }

    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 statement.
     *
     * @param s the debug message
     */
    protected void debug(String s) {
        if (debug) {
            if ((s.indexOf("LOGIN") != -1) && s.startsWith("C:")) {
                StringTokenizer st = new StringTokenizer(s);

                if (st.countTokens() == 5) {
                    String token1 = st.nextToken();
                    String token2 = st.nextToken();
                    String token3 = st.nextToken();
                    String token4 = st.nextToken();

                    debugStream.println(token1 + " " + token2 + " " + token3
                                                + " " + token4 + " ******");
                } else {
                    debugStream.println(s);
                }
            } else {
                debugStream.println(s);
            }
        }
    }

    /**
     * Fires <code>ImapCommandEvent</code>.
     * This event is fired upon sending a command to IMAP server.
     *
     * @param command the command sent to IMAP server
     *
     * @see ImapCommandEvent
     */
    protected void fireImapCommand(String command) {
        Vector thisList;
        ImapCommandEvent thisEvent = new ImapCommandEvent(this, command);

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

        // Send a ImapConnectedEvent to every listener.
        for (int elem = 0; elem < thisList.size(); elem++) {
            ((ImapListener) thisList.elementAt(elem)).commandSent(thisEvent);
        }
    }

    /**
     * Fires <code>ImapConnectedEvent</code>.
     * This event is fired upon connecting to Imap server.
     *
     * @param hostname the hostname of IMAP server
     *
     * @see ImapConnectedEvent
     */
    protected void fireImapConnected(String hostname) {
        Vector thisList;
        ImapConnectedEvent thisEvent = new ImapConnectedEvent(this, hostname);

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

        // Send a ImapConnectedEvent to every listener.
        for (int elem = 0; elem < thisList.size(); elem++) {
            ((ImapListener) thisList.elementAt(elem)).connected(thisEvent);
        }
    }

    /**
     * Fires <code>ImapDisconnectedEvent</code>.
     * This event is fired upon disconnecting from Imap server.
     *
     * @param hostname the hostname of IMAP server
     *
     * @see ImapDisconnectedEvent
     */
    protected void fireImapDisconnected(String hostname) {
        Vector thisList;
        ImapDisconnectedEvent thisEvent = new ImapDisconnectedEvent(this,
                                                                    hostname);

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

        // Send a ImapDisconnectedEvent to every listener.
        for (int elem = 0; elem < thisList.size(); elem++) {
            ((ImapListener) thisList.elementAt(elem)).disconnected(thisEvent);
        }
    }

    /**
     * Fires ImapMessageProgressEvent. This event is fired freqently during the retrieval of a message
     * from IMAP server.
     *
     * @param msg        the message index of message
     * @param bytes      Bytes quatity
     * @param totalBytes Total bytes progress
     *
     * @see ImapMessageProgressEvent
     */
    protected void fireImapMessageProgress(int msg, long bytes, long totalBytes) {
        Vector thisList;
        ImapMessageProgressEvent thisEvent = new ImapMessageProgressEvent(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) imapListeners.clone();
        }

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

    /**
     * Fires <code>ImapMessageEvent</code>.
     * This event is fired after retrieving a message.
     *
     * @param message the EmailMessage received
     *
     * @see ImapMessageEvent
     * @see EmailMessage
     */
    protected void fireImapMessageRetrieved(EmailMessage message) {
        Vector thisList;
        ImapMessageEvent thisEvent = new ImapMessageEvent(this, message);

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

        // Send a ImapMessageEvent to every listener.
        for (int elem = 0; elem < thisList.size(); elem++) {
            ((ImapListener) thisList.elementAt(elem))
                    .messageRetrieved(thisEvent);
        }
    }

    /**
     * Fires <code>ImapResponseEvent</code>.
     * This event is fired upon receiving a response from IMAP server.
     *
     * @param response the response received from IMAP server
     *
     * @see ImapResponseEvent
     */
    protected void fireImapResponse(String response) {
        Vector thisList;
        ImapResponseEvent thisEvent = new ImapResponseEvent(this, response);

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

        for (int elem = 0; elem < thisList.size(); elem++) {
            ((ImapListener) 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 static String readLine(InputStream input)
            throws IOException {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        int b = 0;

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

        return bout.toString();
    }

    /**
     * Gets parsed answer returned from command.
     *
     * @param command the command issued
     * @param buffer  the buffer containing answer
     *
     * @return the answer
     */
    private static String getAnswer(String command, String buffer) {
        String answer = null;

        if (buffer.indexOf(command) != -1) {
            int p = buffer.indexOf('(');
            int q = buffer.indexOf(')', p + 1);

            if (p != -1) {
                //one line answer
                if (q > p) {
                    answer = buffer.substring(p + 1, q);
                }
            }
        }

        return answer;
    }

    /**
     * Encloses in quotes if contains spaces.
     *
     * @param val the value
     *
     * @return the return value
     */
    private static String quote(String val) {
        if (val.contains(" ")) {
            return "\"" + val + "\"";
        } else {
            return val;
        }
    }

    @SuppressObfuscation
    public enum Authentication {

        BASIC(0) {
            @Override
            public void apply(Imap client)
                    throws ImapException {
                client.issueCommand("LOGIN " + client.username + " " + client.password);
            }
        },
        CRAM_MD5(1) {
            @Override
            public void apply(Imap client)
                    throws ImapException {
                StringBuilder buffer = new StringBuilder();
                String response = null;
                String error = null;
                String tag = "A" + client.commandCount++;
                String command = tag + " " + "AUTHENTICATE CRAM-MD5";

                synchronized (this) {
                    if (!client.isConnected()) {
                        throw new ImapException("not connected to IMAP server");
                    } else {
                        try {
                            client.out.write((command + CRLF).getBytes(StandardCharsets.US_ASCII));
                            client.out.flush();
                        } catch (IOException ioe) {
                            throw new ImapException(ioe.getMessage(), ioe);
                        }

                        // send command notification
                        client.fireImapCommand(command);
                        client.debug("C: " + command);
                    }

                    try {
                        response = readLine(client.in);

                        if (response.startsWith(tag + " BAD ")
                            || response.startsWith(tag + " NO ")) {
                            error = response;
                        }

                        client.debug("S: " + response);

                        // output hashed value
                        StringTokenizer tokenizer = new StringTokenizer(response);
                        tokenizer.nextToken();

                        String value = tokenizer.nextToken();
                        Base64 base64 = new Base64();
                        byte[] data = base64.decode(value.toCharArray());
                        MD5 md5 = new MD5(new String(data, StandardCharsets.US_ASCII), client.password);
                        String combined = client.username + " " + md5.getHash();
                        char[] results = base64.encode(combined.getBytes(StandardCharsets.US_ASCII));
                        String resultsResponse = new String(results);
                        client.debug("C: " + resultsResponse);
                        client.out.write((resultsResponse + CRLF).getBytes(StandardCharsets.US_ASCII));
                        client.out.flush();
                        response = readLine(client.in);
                        client.debug(response);

                        if (response.startsWith(tag + " BAD ")
                            || response.startsWith(tag + " NO ")) {
                            error = response;
                        }
                    } catch (IOException ioe) {
                        throw new ImapException("I/O error occured", ioe);
                    }

                    //the tag denotes command success report, and is issued AFTER the response to that particular command
                    if (error == null) {
                        while (!response.startsWith(tag + " OK")) {
                            try {
                                response = readLine(client.in); //receive command completion status
                                client.debug("S: " + response);
                                buffer.append(response);
                                buffer.append(CRLF);
                            } catch (IOException e) {
                                throw new ImapException("IO error occured while communicating with server.\n" + e.toString());
                            }
                        }
                    } else {
                        throw new ImapException(error);
                    }

                    // send response notification
                    client.fireImapResponse(buffer.toString());
                }
            }
        },
        XOAUTH2(2) {
            @Override
            public void apply(Imap client)
                    throws ImapException {
                String command = String.format("user=%s\001auth=Bearer %s\001\001", client.username, client.password);
                String response = client.issueCommand(String.format("AUTHENTICATE XOAUTH2 %s", Base64Format.formatData(command.getBytes(StandardCharsets.UTF_8))));
                if (!response.startsWith("+")) {
                    return;
                }

                try {
                    client.out.write(CRLF.getBytes(StandardCharsets.US_ASCII));
                    client.out.flush();
                    client.debug("C: ");
                    client.readResponse(String.format("A%03d", client.commandCount));
                } catch (IOException e) {
                    throw new ImapException(e);
                }
            }
        };

        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(Imap client)
                throws ImapException;
    }
}
