
package com.fsck.k9.mail;

import java.security.cert.CertPathValidatorException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLHandshakeException;

public class CertificateValidationException extends MessagingException {
    public static final long serialVersionUID = -1;
    private final Reason mReason;
    private X509Certificate[] mCertChain;
    private boolean mNeedsUserAttention = false;
    private String mAlias;

    public enum Reason {
        Unknown, UseMessage, Expired, MissingCapability, RetrievalFailure
    }

    public CertificateValidationException(String message) {
        this(message, Reason.UseMessage, null);
    }

    public CertificateValidationException(Reason reason) {
        this(null, reason, null);
    }

    public CertificateValidationException(String message, Reason reason, String alias) {
        super(message);
        /*
         * Instances created without a Throwable parameter as a cause are
         * presumed to need user attention.
         */
        mNeedsUserAttention = true;
        mReason = reason;
        mAlias = alias;
    }

    public CertificateValidationException(final String message, Throwable throwable) {
        super(message, throwable);
        mReason = Reason.Unknown;
        scanForCause();
    }

    public String getAlias() {
        return mAlias;
    }

    public Reason getReason() {
        return mReason;
    }

    private void scanForCause() {
        Throwable throwable = getCause();

        /*
         * User attention is required if the server certificate was deemed
         * invalid or if there was a problem with a client certificate.
         *
         * A CertificateException is known to be thrown by the default
         * X509TrustManager.checkServerTrusted() if the server certificate
         * doesn't validate. The cause of the CertificateException will be a
         * CertPathValidatorException. However, it's unlikely those exceptions
         * will be encountered here, because they are caught in
         * SecureX509TrustManager.checkServerTrusted(), which throws a
         * CertificateChainException instead (an extension of
         * CertificateException).
         *
         * A CertificateChainException will likely result in (or, be the cause
         * of) an SSLHandshakeException (an extension of SSLException).
         *
         * The various mail protocol handlers (IMAP, POP3, ...) will catch an
         * SSLException and throw a CertificateValidationException (this class)
         * with the SSLException as the cause. (They may also throw a
         * CertificateValidationException when STARTTLS is not available, just
         * for the purpose of triggering a user notification.)
         *
         * SSLHandshakeException is also known to occur if the *client*
         * certificate was not accepted by the server (unknown CA, certificate
         * expired, etc.). In this case, the SSLHandshakeException will not have
         * a CertificateChainException as a cause.
         *
         * KeyChainException is known to occur if the device has no client
         * certificate that's associated with the alias stored in the server
         * settings.
         */
        while (throwable != null
                && !(throwable instanceof CertPathValidatorException)
                && !(throwable instanceof CertificateException)
                && !("android.security.KeyChainException".equals(throwable.getClass().getCanonicalName()))
                && !(throwable instanceof SSLHandshakeException)) {
            throwable = throwable.getCause();
        }

        if (throwable != null) {
            mNeedsUserAttention = true;

            // See if there is a server certificate chain attached to the SSLHandshakeException
            if (throwable instanceof SSLHandshakeException) {
                while (throwable != null && !(throwable instanceof CertificateChainException)) {
                  throwable = throwable.getCause();
                }
            }

            if (throwable != null && throwable instanceof CertificateChainException) {
                mCertChain = ((CertificateChainException) throwable).getCertChain();
            }
        }
    }

    public boolean needsUserAttention() {
        return mNeedsUserAttention;
    }

    /**
     * If the cause of this {@link CertificateValidationException} was a
     * {@link CertificateChainException}, then the offending chain is available
     * for return.
     * 
     * @return An {@link X509Certificate X509Certificate[]} containing the Cert.
     *         chain, or else null.
     */
    public X509Certificate[] getCertChain() {
        return mCertChain;
    }
}
