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

import com.jscape.inet.pop.Pop;
import com.jscape.inet.pop.PopException;
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.File;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.List;


/**
 * Implements the basic functionality of a secure POP3 client using SSL/TLS.  Client supports both
 * implicit SSL/TLS on port 995 (default) and explicit SSL/TLS using STLS command on port 110.<P>
 * Example Usage (implicit SSL/TLS):
 * <P>
 * <pre>
 * // connection parameters for SSL connection
 * String hostname = "pop3.myserver.com";
 * String username = "jsmith";
 * String password = "secret";
 * int port = 995;
 *
 * // new instance hostname, port, username and password
 * PopSsl pop = new PopSsl(hostname,port,username,password);
 * 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>
 * <p/>
 * Example Usage (explicit SSL/TLS using STLS command on port 110):
 * <P>
 * <pre>
 * // connection parameters for SSL connection
 * String hostname = "pop3.myserver.com";
 * String username = "jsmith";
 * String password = "secret";
 * int port = 110;
 *
 * // new instance hostname, port, username and password
 * PopSsl pop = new PopSsl(hostname,port,username,password);
 * pop.setConnectionType(PopSsl.STARTTLS);
 * 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 PopSsl
        extends Pop {

    /**
     * Connection type for implicit SSL/TLS connections on port 995.
     * 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 110.
     * Value of 1.
     *
     * @see #setConnectionType
     * @see #IMPLICIT_SSL
     */
    public static final int STARTTLS = 1;

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

    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;

    /**
     * Constructs a new PopSsl 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 PopSsl(String hostname, String username, String password) {
        this(hostname, DEFAULT_PORT, username, password);
    }

    /**
     * Constructs a new PopSsl instance. Sets hostname, username and password for
     * POP3 connection.
     *
     * @param hostname the hostname of POP3 server
     * @param port     the port of secure POP3 server
     * @param username the username of POP3 user
     * @param password the password of POP3 user
     */
    public PopSsl(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 POP3 server.
     *
     * @throws PopException if I/O or POP3 related error occurs
     */
    public PopSsl connect()
            throws PopException {
        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 PrintWriter(new OutputStreamWriter(new SyncRawConnection.ConnectionOutputStream(this.connection)));

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

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

                protector.protect(this.connection);

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

            this.authentication.apply(this, banner);

            this.firePopConnected();

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

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

        try {
            synchronized (this) {
                issueCommand("QUIT");
                this.connection.close();
            }
        } catch (NullPointerException npe) {
            throw new PopException("POP client already disconnected");
        } catch (Exception e) {
            throw new PopException(e.getMessage(), e);
        } finally {
            out = null;
            in = null;
            this.firePopDisconnected();
        }
    }

    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();
    }
}
