Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c427f6df authored by cketti's avatar cketti
Browse files

Extract methods to make ImapConnection.open() more readable

parent c4a4a3bb
Loading
Loading
Loading
Loading
+183 −147
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ package com.fsck.k9.mail.store.imap;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.InetAddress;
@@ -12,10 +13,12 @@ import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.security.GeneralSecurityException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Set;
@@ -27,7 +30,6 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.Log;

import com.fsck.k9.mail.AuthType;
import com.fsck.k9.mail.Authentication;
import com.fsck.k9.mail.AuthenticationFailedException;
import com.fsck.k9.mail.CertificateValidationException;
@@ -107,96 +109,51 @@ class ImapConnection {

        boolean authSuccess = false;
        nextCommandTag = 1;
        adjustDNSCacheTTL();

        try {
            socket = connect(settings, socketFactory);
            setReadTimeout(socketReadTimeout);

            inputStream = new PeekableInputStream(new BufferedInputStream(socket.getInputStream(), BUFFER_SIZE));
            responseParser = new ImapResponseParser(inputStream);
            outputStream = new BufferedOutputStream(socket.getOutputStream(), BUFFER_SIZE);

        capabilities.clear();
            ImapResponse nullResponse = responseParser.readResponse();

            if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) {
                Log.v(LOG_TAG, getLogId() + "<<<" + nullResponse);
            }

            List<ImapResponse> nullResponses = new LinkedList<ImapResponse>();
            nullResponses.add(nullResponse);
            receiveCapabilities(nullResponses);
        adjustDNSCacheTTL();

            if (capabilities.isEmpty()) {
                if (K9MailLib.isDebug()) {
                    Log.i(LOG_TAG, "Did not get capabilities in banner, requesting CAPABILITY for " + getLogId());
                }
        try {
            socket = connect();
            configureSocket();
            setUpStreamsAndParserFromSocket();

                List<ImapResponse> responses = receiveCapabilities(executeSimpleCommand(COMMAND_CAPABILITY));
                if (responses.size() != 2) {
                    throw new MessagingException("Invalid CAPABILITY response received");
                }
            }
            readInitialResponse();
            requestCapabilitiesIfNecessary();

            if (settings.getConnectionSecurity() == STARTTLS_REQUIRED) {
                if (hasCapability("STARTTLS")) {
                    startTLS();
                } else {
                    /*
                     * This exception triggers a "Certificate error"
                     * notification that takes the user to the incoming
                     * server settings for review. This might be needed if
                     * the account was configured with an obsolete
                     * "STARTTLS (if available)" setting.
                     */
                    throw new CertificateValidationException("STARTTLS connection security not available");
                }
            }
            upgradeToTlsIfNecessary();

            authenticate(settings.getAuthType());
            authenticate();
            authSuccess = true;

            if (K9MailLib.isDebug()) {
                Log.d(LOG_TAG, CAPABILITY_COMPRESS_DEFLATE + " = " + hasCapability(CAPABILITY_COMPRESS_DEFLATE));
            }
            enableCompressionIfRequested();

            if (hasCapability(CAPABILITY_COMPRESS_DEFLATE) && shouldEnableCompression()) {
                enableCompression();
            }

            if (K9MailLib.isDebug()) {
                Log.d(LOG_TAG, "NAMESPACE = " + hasCapability(CAPABILITY_NAMESPACE) +
                        ", mPathPrefix = " + settings.getPathPrefix());
            }
            retrievePathPrefixIfNecessary();
            retrievePathDelimiterIfNecessary();

            if (settings.getPathPrefix() == null) {
                if (hasCapability(CAPABILITY_NAMESPACE)) {
                    if (K9MailLib.isDebug()) {
                        Log.i(LOG_TAG, "pathPrefix is unset and server has NAMESPACE capability");
                    }
                    handleNamespace();
                } else {
                    if (K9MailLib.isDebug()) {
                        Log.i(LOG_TAG, "pathPrefix is unset but server does not have NAMESPACE capability");
        } catch (SSLException e) {
            handleSslException(e);
        } catch (ConnectException e) {
            handleConnectException(e);
        } catch (GeneralSecurityException e) {
            throw new MessagingException("Unable to open connection to IMAP server due to security error.", e);
        } finally {
            if (!authSuccess) {
                Log.e(LOG_TAG, "Failed to login, closing connection for " + getLogId());
                close();
            }
                    settings.setPathPrefix("");
        }
    }

            if (settings.getPathDelimiter() == null) {
                getPathDelimiter();
            }

        } catch (SSLException e) {
    private void handleSslException(SSLException e) throws CertificateValidationException, SSLException {
        if (e.getCause() instanceof CertificateException) {
            throw new CertificateValidationException(e.getMessage(), e);
        } else {
            throw e;
        }
        } catch (GeneralSecurityException e) {
            throw new MessagingException("Unable to open connection to IMAP server due to security error.", e);
        } catch (ConnectException e) {
    }

    private void handleConnectException(ConnectException e) throws ConnectException {
        String message = e.getMessage();
        String[] tokens = message.split("-");

@@ -206,12 +163,6 @@ class ImapConnection {
        } else {
            throw e;
        }
        } finally {
            if (!authSuccess) {
                Log.e(LOG_TAG, "Failed to login, closing connection for " + getLogId());
                close();
            }
        }
    }

    public boolean isOpen() {
@@ -233,17 +184,29 @@ class ImapConnection {
        }
    }

    private Socket connect(ImapSettings settings, TrustedSocketFactory socketFactory) throws GeneralSecurityException,
            MessagingException, IOException {
    private Socket connect() throws GeneralSecurityException, MessagingException, IOException {
        Exception connectException = null;

        InetAddress[] inetAddresses = InetAddress.getAllByName(settings.getHost());
        for (InetAddress address : inetAddresses) {
            try {
                return connectToAddress(address);
            } catch (IOException e) {
                Log.w(LOG_TAG, "Could not connect to " + address, e);
                connectException = e;
            }
        }

        throw new MessagingException("Cannot connect to host", connectException);
    }

    private Socket connectToAddress(InetAddress address) throws NoSuchAlgorithmException, KeyManagementException,
            MessagingException, IOException {

        String host = settings.getHost();
        int port = settings.getPort();
        String clientCertificateAlias = settings.getClientCertificateAlias();

        InetAddress[] inetAddresses = InetAddress.getAllByName(host);
        for (InetAddress address : inetAddresses) {
            try {
        if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) {
            Log.d(LOG_TAG, "Connecting to " + host + " as " + address);
        }
@@ -259,64 +222,109 @@ class ImapConnection {

        socket.connect(socketAddress, socketConnectTimeout);

                // Successfully connected to the server; don't try any other addresses
        return socket;
            } catch (IOException e) {
                Log.w(LOG_TAG, "could not connect to " + address, e);
                connectException = e;
    }

    private void configureSocket() throws SocketException {
        socket.setSoTimeout(socketReadTimeout);
    }

        throw new MessagingException("Cannot connect to host", connectException);
    private void setUpStreamsAndParserFromSocket() throws IOException {
        setUpStreamsAndParser(socket.getInputStream(), socket.getOutputStream());
    }

    private void startTLS() throws IOException, MessagingException, GeneralSecurityException {
        executeSimpleCommand("STARTTLS");
    private void setUpStreamsAndParser(InputStream input, OutputStream output) {
        inputStream = new PeekableInputStream(new BufferedInputStream(input, BUFFER_SIZE));
        responseParser = new ImapResponseParser(inputStream);
        outputStream = new BufferedOutputStream(output, BUFFER_SIZE);
    }

        String host = settings.getHost();
        int port = settings.getPort();
        String clientCertificateAlias = settings.getClientCertificateAlias();
    private void readInitialResponse() throws IOException {
        ImapResponse initialResponse = responseParser.readResponse();

        socket = socketFactory.createSocket(socket, host, port, clientCertificateAlias);
        socket.setSoTimeout(socketReadTimeout);
        if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) {
            Log.v(LOG_TAG, getLogId() + "<<<" + initialResponse);
        }

        inputStream = new PeekableInputStream(new BufferedInputStream(socket.getInputStream(), BUFFER_SIZE));
        responseParser = new ImapResponseParser(inputStream);
        outputStream = new BufferedOutputStream(socket.getOutputStream(), BUFFER_SIZE);
        extractCapabilities(Collections.singletonList(initialResponse));
    }

    private List<ImapResponse> extractCapabilities(List<ImapResponse> responses) {
        Set<String> receivedCapabilities = ImapResponseParser.parseCapabilities(responses);

        // Per RFC 2595 (3.1):  Once TLS has been started, reissue CAPABILITY command
        if (K9MailLib.isDebug()) {
            Log.i(LOG_TAG, "Updating capabilities after STARTTLS for " + getLogId());
            Log.d(LOG_TAG, "Saving " + receivedCapabilities + " capabilities for " + getLogId());
        }

        capabilities.addAll(receivedCapabilities);

        return responses;
    }

    private void requestCapabilitiesIfNecessary() throws IOException, MessagingException {
        if (!capabilities.isEmpty()) {
            return;
        }

        if (K9MailLib.isDebug()) {
            Log.i(LOG_TAG, "Did not get capabilities in banner, requesting CAPABILITY for " + getLogId());
        }

        requestCapabilities();
    }

    private void requestCapabilities() throws IOException, MessagingException {
        capabilities.clear();
        List<ImapResponse> responses = receiveCapabilities(executeSimpleCommand(COMMAND_CAPABILITY));

        List<ImapResponse> responses = extractCapabilities(executeSimpleCommand(COMMAND_CAPABILITY));
        if (responses.size() != 2) {
            throw new MessagingException("Invalid CAPABILITY response received");
        }
    }

    private List<ImapResponse> receiveCapabilities(List<ImapResponse> responses) {
        Set<String> receivedCapabilities = ImapResponseParser.parseCapabilities(responses);
    private void upgradeToTlsIfNecessary() throws IOException, MessagingException, GeneralSecurityException {
        if (settings.getConnectionSecurity() == STARTTLS_REQUIRED) {
            upgradeToTls();
        }
    }

        /* RFC 3501 6.2.3
            A server MAY include a CAPABILITY response code in the tagged OK
            response to a successful LOGIN command in order to send
            capabilities automatically.  It is unnecessary for a client to
            send a separate CAPABILITY command if it recognizes these
            automatic capabilities.
    private void upgradeToTls() throws IOException, MessagingException, GeneralSecurityException {
        if (!hasCapability("STARTTLS")) {
            /*
             * This exception triggers a "Certificate error"
             * notification that takes the user to the incoming
             * server settings for review. This might be needed if
             * the account was configured with an obsolete
             * "STARTTLS (if available)" setting.
             */
        if (K9MailLib.isDebug()) {
            Log.d(LOG_TAG, "Saving " + receivedCapabilities + " capabilities for " + getLogId());
            throw new CertificateValidationException("STARTTLS connection security not available");
        }

        capabilities.addAll(receivedCapabilities);
        startTLS();
    }

        return responses;
    private void startTLS() throws IOException, MessagingException, GeneralSecurityException {
        executeSimpleCommand("STARTTLS");

        String host = settings.getHost();
        int port = settings.getPort();
        String clientCertificateAlias = settings.getClientCertificateAlias();

        socket = socketFactory.createSocket(socket, host, port, clientCertificateAlias);
        configureSocket();
        setUpStreamsAndParserFromSocket();

        // Per RFC 2595 (3.1):  Once TLS has been started, reissue CAPABILITY command
        if (K9MailLib.isDebug()) {
            Log.i(LOG_TAG, "Updating capabilities after STARTTLS for " + getLogId());
        }

    private void authenticate(AuthType authType) throws MessagingException, IOException {
        switch (authType) {
        requestCapabilities();
    }

    @SuppressWarnings("EnumSwitchStatementWhichMissesCases")
    private void authenticate() throws MessagingException, IOException {
        switch (settings.getAuthType()) {
            case CRAM_MD5: {
                if (hasCapability(CAPABILITY_AUTH_CRAM_MD5)) {
                    authCramMD5();
@@ -369,7 +377,7 @@ class ImapConnection {
        outputStream.flush();

        try {
            receiveCapabilities(responseParser.readStatusResponse(tag, command, getLogId(), null));
            extractCapabilities(responseParser.readStatusResponse(tag, command, getLogId(), null));
        } catch (MessagingException e) {
            throw new AuthenticationFailedException(e.getMessage());
        }
@@ -398,7 +406,7 @@ class ImapConnection {
        outputStream.flush();

        try {
            receiveCapabilities(responseParser.readStatusResponse(tag, command, getLogId(), null));
            extractCapabilities(responseParser.readStatusResponse(tag, command, getLogId(), null));
        } catch (MessagingException e) {
            throw new AuthenticationFailedException(e.getMessage());
        }
@@ -418,7 +426,7 @@ class ImapConnection {
        String password = p.matcher(settings.getPassword()).replaceAll(replacement);

        try {
            receiveCapabilities(executeSimpleCommand(String.format("LOGIN \"%s\" \"%s\"", username, password), true));
            extractCapabilities(executeSimpleCommand(String.format("LOGIN \"%s\" \"%s\"", username, password), true));
        } catch (ImapException e) {
            throw new AuthenticationFailedException(e.getMessage());
        }
@@ -426,7 +434,7 @@ class ImapConnection {

    private void saslAuthExternal() throws IOException, MessagingException {
        try {
            receiveCapabilities(executeSimpleCommand(
            extractCapabilities(executeSimpleCommand(
                    String.format("AUTHENTICATE EXTERNAL %s", Base64.encode(settings.getUsername())), false));
        } catch (ImapException e) {
            /*
@@ -440,6 +448,12 @@ class ImapConnection {
        }
    }

    private void enableCompressionIfRequested() throws IOException, MessagingException {
        if (hasCapability(CAPABILITY_COMPRESS_DEFLATE) && shouldEnableCompression()) {
            enableCompression();
        }
    }

    private boolean shouldEnableCompression() {
        boolean useCompression = true;

@@ -470,13 +484,11 @@ class ImapConnection {
        }

        try {
            InflaterInputStream zInputStream = new InflaterInputStream(socket.getInputStream(), new Inflater(true));
            inputStream = new PeekableInputStream(new BufferedInputStream(zInputStream, BUFFER_SIZE));
            responseParser = new ImapResponseParser(inputStream);
            InflaterInputStream input = new InflaterInputStream(socket.getInputStream(), new Inflater(true));
            ZOutputStream output = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_SPEED, true);
            output.setFlushMode(JZlib.Z_PARTIAL_FLUSH);

            ZOutputStream zOutputStream = new ZOutputStream(socket.getOutputStream(), JZlib.Z_BEST_SPEED, true);
            outputStream = new BufferedOutputStream(zOutputStream, BUFFER_SIZE);
            zOutputStream.setFlushMode(JZlib.Z_PARTIAL_FLUSH);
            setUpStreamsAndParser(input, output);

            if (K9MailLib.isDebug()) {
                Log.i(LOG_TAG, "Compression enabled for " + getLogId());
@@ -487,6 +499,24 @@ class ImapConnection {
        }
    }

    private void retrievePathPrefixIfNecessary() throws IOException, MessagingException {
        if (settings.getPathPrefix() != null) {
            return;
        }

        if (hasCapability(CAPABILITY_NAMESPACE)) {
            if (K9MailLib.isDebug()) {
                Log.i(LOG_TAG, "pathPrefix is unset and server has NAMESPACE capability");
            }
            handleNamespace();
        } else {
            if (K9MailLib.isDebug()) {
                Log.i(LOG_TAG, "pathPrefix is unset but server does not have NAMESPACE capability");
            }
            settings.setPathPrefix("");
        }
    }

    private void handleNamespace() throws IOException, MessagingException {
        List<ImapResponse> responses = executeSimpleCommand(COMMAND_NAMESPACE);

@@ -525,7 +555,13 @@ class ImapConnection {
        }
    }

    private void getPathDelimiter() throws IOException, MessagingException {
    private void retrievePathDelimiterIfNecessary() throws IOException, MessagingException {
        if (settings.getPathDelimiter() == null) {
            retrievePathDelimiter();
        }
    }

    private void retrievePathDelimiter() throws IOException, MessagingException {
        List<ImapResponse> listResponses;
        try {
            listResponses = executeSimpleCommand("LIST \"\" \"\"");