/*
 * @(#)ImapSsl.java
 *
 * Copyright (c) 2005-2006 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.imapssl;

import com.jscape.inet.imap.Imap;
import com.jscape.inet.imap.ImapException;
import com.jscape.util.StringTools;
import com.jscape.util.keystore.KeyStoreTools;
import com.jscape.util.net.connection.ConnectionProtector;
import com.jscape.util.net.connection.Connector;
import com.jscape.util.net.connection.SyncRawConnection;
import com.jscape.util.net.socket.ClientAuthenticationNecessity;
import com.jscape.util.net.socket.SslSocketConnectionProtector;
import com.jscape.util.net.ssl.SslContextFactory;

import javax.net.ssl.SSLContext;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.List;


/**
 * Implements the basic functionality of a secure IMAP client using SSL/TLS.  Client supports both
 * implicit SSL/TLS on port 993 (default) and explicit SSL/TLS using STARTTLS command on port 143.<P>
 * Example Usage (implicit SSL/TLS):
 * <p/>
 * <pre>
 * // new instance with hostname, SSL port, username and password
 * ImapSsl imap = new ImapSsl("imap4.myserver.com",993,"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>
 * Example Usage (explicit SSL/TLS using STARTTLS command):
 * <p/>
 * <pre>
 * // new instance with hostname, SSL port, username and password
 * ImapSsl imap = new ImapSsl("imap4.myserver.com",143,"jsmith","secret");
 * imap.setConnectionType(ImapSsl.STARTTLS);
 * 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>
 */
public class ImapSsl
        extends Imap {

    /**
     * Connection type for implicit SSL/TLS connections on port 993.
     * Value of 0.
     *
     * @see #setConnectionType
     * @see #STARTTLS
     */
    public static final int IMPLICIT_SSL = 0;

    /**
     * Connection type for explicit SSL/TLS connections using STARTTLS command on port 143.
     * Value of 1.
     *
     * @see #setConnectionType
     * @see #IMPLICIT_SSL
     */
    public static final int STARTTLS = 1;

    /**
     * The default port to connect to for secure IMAP over SSL connections (993).
     */
    public static final int DEFAULT_PORT = 993;

    private SSLContext context = null;
    private String clientFilename = null;
    private String clientPassword = null;
    private String clientStoretype = null;
    private String serverFilename = null;
    private String serverPassword = null;
    private String serverStoretype = null;
    private int mode = IMPLICIT_SSL;

    /**
     * Creates a new ImapSsl instance.
     *
     * @param hostname the IMAP hostname
     * @param username the IMAP username
     * @param password the IMAP password
     */
    public ImapSsl(String hostname, String username, String password) {
        super(hostname, username, password);
        setPort(DEFAULT_PORT);
    }

    /**
     * Creates a new ImapSsl instance.
     *
     * @param hostname the IMAP hostname
     * @param username the IMAP username
     * @param password the IMAP password
     * @param port     the IMAP SSL port
     */
    public ImapSsl(String hostname, int port, String username, String password) {
        super(hostname, username, password);
        setPort(port);
    }

    /**
     * Sets optional client certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename the keystore file containing the client certificate
     * @param password the keystore password
     */
    public void setClientCertificates(String filename, String password) {
        this.clientFilename = filename;
        this.clientPassword = password;
    }

    /**
     * Sets optional client certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename  the keystore file containing the client certificate
     * @param password  the keystore password
     * @param storetype the keystore type valid values include jks and pkcs12
     */
    public void setClientCertificates(String filename, String password,
                                      String storetype) {
        this.clientFilename = filename;
        this.clientPassword = password;
        this.clientStoretype = storetype;
    }

    /**
     * Sets the connection type for this session.
     *
     * @param mode a valid connection type
     *
     * @see #IMPLICIT_SSL
     * @see #STARTTLS
     */
    public void setConnectionType(int mode) {
        this.mode = mode;
    }

    /**
     * Gets the connection type for this session.
     * Default is IMPLICIT_SSL
     *
     * @return the connection type
     *
     * @see #IMPLICIT_SSL
     * @see #STARTTLS
     */
    public int getConnectionType() {
        return mode;
    }

    /**
     * Sets SSL context for this connection.
     *
     * @param context an SSLContext instance
     *
     * @see SSLContext
     */
    public void setSSLContext(SSLContext context) {
        this.context = context;
    }

    /**
     * Sets optional server certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename the keystore file containing the server certificate
     * @param password the keystore password
     */
    public void setServerCertificates(String filename, String password) {
        this.serverFilename = filename;
        this.serverPassword = password;
    }

    /**
     * Sets optional server certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename  the keystore file containing the server certificate
     * @param password  the keystore password
     * @param storetype the keystore type valid values include jks and pkcs12
     */
    public void setServerCertificates(String filename, String password,
                                      String storetype) {
        this.serverFilename = filename;
        this.serverPassword = password;
        this.serverStoretype = storetype;
    }

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

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

    /**
     * Establishes secure SSL connection to IMAP server.
     *
     * @throws ImapException if I/O or IMAP related error occurs
     */
    public ImapSsl connect()
            throws ImapException {
        try {
            Connector<SyncRawConnection> rawConnector = rawConnector();
            this.connection = rawConnector.connect();
            ConnectionProtector protector = connectionProtector();

            if (mode == IMPLICIT_SSL) {
                protector.protect(this.connection);
            }

            in = new BufferedInputStream(new SyncRawConnection.ConnectionInputStream(this.connection));
            out = new BufferedOutputStream(new SyncRawConnection.ConnectionOutputStream(this.connection));

            // notify that client has connected
            fireImapConnected(hostname);

            // get IMAP banner
            String banner = readLine(in);
            debug(banner);

            if (mode == STARTTLS) {
                // issue START TLS command
                issueCommand("STARTTLS");

                protector.protect(this.connection);

                // reset streams
                in = new BufferedInputStream(new SyncRawConnection.ConnectionInputStream(this.connection));
                out = new BufferedOutputStream(new SyncRawConnection.ConnectionOutputStream(this.connection));
            }

            this.authentication.apply(this);

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

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

    /**
     * Disconnects from IMAP server. Issues CLOSE and LOGOUT commands to IMAP server.
     *
     * @throws ImapException if I/O or IMAP related error occurs
     */
    public void disconnect()
            throws ImapException {
        if (!isConnected()) {
            return;
        }

        try {
            synchronized (this) {
                issueCommand("CLOSE");
                issueCommand("LOGOUT");
                this.connection.close();
            }
        } catch (NullPointerException npe) {
            throw new ImapException("IMAP client already disconnected");
        } catch (Exception e) {
            throw new ImapException(e.getMessage(), e);
        } finally {
            out = null;
            in = null;
            this.fireImapDisconnected(hostname);
        }
    }

    private ConnectionProtector connectionProtector()
            throws Exception {
        SSLContext context = sslContext();

        return new SslSocketConnectionProtector(context.getSocketFactory(),
                                                null,
                                                null,
                                                ClientAuthenticationNecessity.NOT_REQUIRED,
                                                true,
                                                true,
                                                null);
    }

    private SSLContext sslContext()
            throws Exception {
        if (this.context != null) {
            return this.context;
        }

        KeyStore keyStore = StringTools.isValidString(this.clientFilename) ? KeyStoreTools.load(new File(this.clientFilename), this.clientPassword, this.clientStoretype, null) : null;
        KeyStore trustedStore = StringTools.isValidString(this.serverFilename) ? KeyStoreTools.load(new File(this.serverFilename), this.serverPassword, this.serverStoretype, null) : null;

        if (keyStore != null && trustedStore != null) {
            return SslContextFactory.contextFor(keyStore, this.clientPassword, trustedStore);
        } else if (keyStore != null) {
            return SslContextFactory.contextFor(keyStore, this.clientPassword);
        } else if (trustedStore != null) {
            List<Certificate> certificates = KeyStoreTools.certificatesOf(trustedStore);
            return SslContextFactory.contextFor(certificates.toArray(new Certificate[0]));
        }

        return SslContextFactory.defaultContext();
    }
}
