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

import com.jscape.inet.smtp.Smtp;
import com.jscape.inet.smtp.SmtpException;
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.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.util.List;


/**
 * Implements the basic functionality of a secure SMTP client using SSL/TLS.  Client supports both
 * implicit SSL/TLS on port 465 (default) and explicit SSL/TLS using STARTTLS command on port 25.<P>
 * Example Usage (implicit SSL/TLS):
 * <p/>
 * <P>
 * <pre>
 * // create new instance
 * SmtpSsl smtp = new SmtpSsl("smtp.myserver.com",465);
 *  // address the message
 * EmailMessage message = new EmailMessage();
 * message.setTo("mjones@myserver.com");
 * message.setFrom("jsmith@myserver.com");
 * message.setSubject("Meeting today at 8");
 * message setBody("see you then");
 *  // connect, send the message, disconnect
 * try
 * {
 *   smtp.connect();
 *   smtp.send(message);
 *   smtp.disconnect();
 * }
 * catch(Exception e)
 * {
 *   System.out.println(e);
 * }
 * </pre>
 * </p>
 * <P>
 * Example Usage (explicit SSL/TLS using STARTTLS command):
 * <p/>
 * <P>
 * <pre>
 * // create new instance
 * SmtpSsl smtp = new SmtpSsl("smtp.myserver.com",25);
 * smtp.setConnectionType(SmtpSsl.STARTTLS);
 *  // address the message
 * EmailMessage message = new EmailMessage();
 * message.setTo("mjones@myserver.com");
 * message.setFrom("jsmith@myserver.com");
 * message.setSubject("Meeting today at 8");
 * message setBody("see you then");
 *  // connect, send the message, disconnect
 * try
 * {
 *   smtp.connect();
 *   smtp.send(message);
 *   smtp.disconnect();
 * }
 * catch(Exception e)
 * {
 *   System.out.println(e);
 * }
 * </pre>
 * </p>
 */
public class SmtpSsl
        extends Smtp {

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

    /**
     * The default port to connect to for secure SMTP over SSL connections.
     * Value of 465.
     */
    public static final int DEFAULT_PORT = 465;

    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;

    /**
     * The current connection mode.
     */
    private int mode = IMPLICIT_SSL;

    /**
     * Constructs new SmtpSsl instance.
     *
     * @param hostname the SMTP server hostname
     */
    public SmtpSsl(String hostname) {
        this(hostname, DEFAULT_PORT);
    }

    /**
     * Constructs new SmtpSsl instance.
     *
     * @param hostname the SMTP server hostname
     * @param port     the SMTP SSL server port
     */
    public SmtpSsl(String hostname, int port) {
        super(hostname);
        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);
    }

    /**
     * Securely connects to SMTP server using SSL.
     *
     * @throws SmtpException thrown for I/O or SMTP related error
     */
    public Smtp connect()
            throws SmtpException {
        try {
            Connector<SyncRawConnection> rawConnector = rawConnector();
            this.connection = rawConnector.connect();
            ConnectionProtector protector = connectionProtector();

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

            in = new BufferedReader(new InputStreamReader(new SyncRawConnection.ConnectionInputStream(this.connection), "8859_1"));
            out = new BufferedOutputStream(new SyncRawConnection.ConnectionOutputStream(this.connection));

            fireSmtpConnected(hostname);

            // read SMTP banner
            readResponse();

            // issue HELO command
            String ip;

            if ((heloAddress != null) && (heloAddress.trim().length() > 0)) {
                ip = heloAddress;
            } else {
                try {
                    ip = InetAddress.getLocalHost().getHostAddress();
                } catch (UnknownHostException uh) {
                    ip = "localhost";
                }
            }

            try {
                writeCommand(out, "EHLO " + ip);
                readResponse();
            } catch (Exception ez) {
                writeCommand(out, "HELO " + ip);
                readResponse();
            }

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

                protector.protect(this.connection);

                // reset streams
                in = new BufferedReader(new InputStreamReader(new SyncRawConnection.ConnectionInputStream(this.connection), "8859_1"));
                out = new BufferedOutputStream(new SyncRawConnection.ConnectionOutputStream(this.connection));

                // re-issue EHLO command
                writeCommand(out, "EHLO " + ip);
                readResponse();
            }

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

    /**
     * Disconnects from SMTP server.
     *
     * @throws SmtpException thrown to handle SMTP and I/O related errors
     * @see SmtpException
     */
    public void disconnect()
            throws SmtpException {
        synchronized (this) {
            try {
                if (out != null) {
                    writeCommand(out, "QUIT");
                    readResponse();
                    connection.close();
                    out = null;
                    in = null;
                    connection = null;
                }
            } catch (Exception ioe) {
                throw new SmtpException(ioe.getMessage(), ioe);
            }
        }

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