Home | History | Annotate | Download | only in store
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.email.mail.store;
     18 
     19 import android.text.TextUtils;
     20 import android.util.Log;
     21 
     22 import com.android.email.Email;
     23 import com.android.email.mail.Transport;
     24 import com.android.email.mail.store.ImapStore.ImapException;
     25 import com.android.email.mail.store.imap.ImapConstants;
     26 import com.android.email.mail.store.imap.ImapList;
     27 import com.android.email.mail.store.imap.ImapResponse;
     28 import com.android.email.mail.store.imap.ImapResponseParser;
     29 import com.android.email.mail.store.imap.ImapUtility;
     30 import com.android.email.mail.transport.DiscourseLogger;
     31 import com.android.email.mail.transport.MailTransport;
     32 import com.android.emailcommon.Logging;
     33 import com.android.emailcommon.mail.AuthenticationFailedException;
     34 import com.android.emailcommon.mail.CertificateValidationException;
     35 import com.android.emailcommon.mail.MessagingException;
     36 
     37 import java.io.IOException;
     38 import java.util.ArrayList;
     39 import java.util.Collections;
     40 import java.util.List;
     41 import java.util.concurrent.atomic.AtomicInteger;
     42 
     43 import javax.net.ssl.SSLException;
     44 
     45 /**
     46  * A cacheable class that stores the details for a single IMAP connection.
     47  */
     48 class ImapConnection {
     49     // Always check in FALSE
     50     private static final boolean DEBUG_FORCE_SEND_ID = false;
     51 
     52     /** ID capability per RFC 2971*/
     53     public static final int CAPABILITY_ID        = 1 << 0;
     54     /** NAMESPACE capability per RFC 2342 */
     55     public static final int CAPABILITY_NAMESPACE = 1 << 1;
     56     /** STARTTLS capability per RFC 3501 */
     57     public static final int CAPABILITY_STARTTLS  = 1 << 2;
     58     /** UIDPLUS capability per RFC 4315 */
     59     public static final int CAPABILITY_UIDPLUS   = 1 << 3;
     60 
     61     /** The capabilities supported; a set of CAPABILITY_* values. */
     62     private int mCapabilities;
     63     private static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
     64     Transport mTransport;
     65     private ImapResponseParser mParser;
     66     private ImapStore mImapStore;
     67     private String mUsername;
     68     private String mLoginPhrase;
     69     private String mIdPhrase = null;
     70     /** # of command/response lines to log upon crash. */
     71     private static final int DISCOURSE_LOGGER_SIZE = 64;
     72     private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
     73     /**
     74      * Next tag to use.  All connections associated to the same ImapStore instance share the same
     75      * counter to make tests simpler.
     76      * (Some of the tests involve multiple connections but only have a single counter to track the
     77      * tag.)
     78      */
     79     private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
     80 
     81 
     82     // Keep others from instantiating directly
     83     ImapConnection(ImapStore store, String username, String password) {
     84         setStore(store, username, password);
     85     }
     86 
     87     void setStore(ImapStore store, String username, String password) {
     88         if (username != null && password != null) {
     89             mUsername = username;
     90 
     91             // build the LOGIN string once (instead of over-and-over again.)
     92             // apply the quoting here around the built-up password
     93             mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
     94                     + ImapUtility.imapQuoted(password);
     95         }
     96         mImapStore = store;
     97     }
     98     void open() throws IOException, MessagingException {
     99         if (mTransport != null && mTransport.isOpen()) {
    100             return;
    101         }
    102 
    103         try {
    104             // copy configuration into a clean transport, if necessary
    105             if (mTransport == null) {
    106                 mTransport = mImapStore.cloneTransport();
    107             }
    108 
    109             mTransport.open();
    110             mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
    111 
    112             createParser();
    113 
    114             // BANNER
    115             mParser.readResponse();
    116 
    117             // CAPABILITY
    118             ImapResponse capabilities = queryCapabilities();
    119 
    120             boolean hasStartTlsCapability =
    121                 capabilities.contains(ImapConstants.STARTTLS);
    122 
    123             // TLS
    124             ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
    125             if (newCapabilities != null) {
    126                 capabilities = newCapabilities;
    127             }
    128 
    129             // NOTE: An IMAP response MUST be processed before issuing any new IMAP
    130             // requests. Subsequent requests may destroy previous response data. As
    131             // such, we save away capability information here for future use.
    132             setCapabilities(capabilities);
    133             String capabilityString = capabilities.flatten();
    134 
    135             // ID
    136             doSendId(isCapable(CAPABILITY_ID), capabilityString);
    137 
    138             // LOGIN
    139             doLogin();
    140 
    141             // NAMESPACE (only valid in the Authenticated state)
    142             doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
    143 
    144             // Gets the path separator from the server
    145             doGetPathSeparator();
    146 
    147             mImapStore.ensurePrefixIsValid();
    148         } catch (SSLException e) {
    149             if (Email.DEBUG) {
    150                 Log.d(Logging.LOG_TAG, e.toString());
    151             }
    152             throw new CertificateValidationException(e.getMessage(), e);
    153         } catch (IOException ioe) {
    154             // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
    155             // of other code here that catches IOException and I don't want to break it.
    156             // This catch is only here to enhance logging of connection-time issues.
    157             if (Email.DEBUG) {
    158                 Log.d(Logging.LOG_TAG, ioe.toString());
    159             }
    160             throw ioe;
    161         } finally {
    162             destroyResponses();
    163         }
    164     }
    165 
    166     /**
    167      * Closes the connection and releases all resources. This connection can not be used again
    168      * until {@link #setStore(ImapStore, String, String)} is called.
    169      */
    170     void close() {
    171         if (mTransport != null) {
    172             mTransport.close();
    173             mTransport = null;
    174         }
    175         destroyResponses();
    176         mParser = null;
    177         mImapStore = null;
    178     }
    179 
    180     /**
    181      * Returns whether or not the specified capability is supported by the server.
    182      */
    183     private boolean isCapable(int capability) {
    184         return (mCapabilities & capability) != 0;
    185     }
    186 
    187     /**
    188      * Sets the capability flags according to the response provided by the server.
    189      * Note: We only set the capability flags that we are interested in. There are many IMAP
    190      * capabilities that we do not track.
    191      */
    192     private void setCapabilities(ImapResponse capabilities) {
    193         if (capabilities.contains(ImapConstants.ID)) {
    194             mCapabilities |= CAPABILITY_ID;
    195         }
    196         if (capabilities.contains(ImapConstants.NAMESPACE)) {
    197             mCapabilities |= CAPABILITY_NAMESPACE;
    198         }
    199         if (capabilities.contains(ImapConstants.UIDPLUS)) {
    200             mCapabilities |= CAPABILITY_UIDPLUS;
    201         }
    202         if (capabilities.contains(ImapConstants.STARTTLS)) {
    203             mCapabilities |= CAPABILITY_STARTTLS;
    204         }
    205     }
    206 
    207     /**
    208      * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
    209      * set it to {@link #mParser}.
    210      *
    211      * If we already have an {@link ImapResponseParser}, we
    212      * {@link #destroyResponses()} and throw it away.
    213      */
    214     private void createParser() {
    215         destroyResponses();
    216         mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
    217     }
    218 
    219     void destroyResponses() {
    220         if (mParser != null) {
    221             mParser.destroyResponses();
    222         }
    223     }
    224 
    225     boolean isTransportOpenForTest() {
    226         return mTransport != null ? mTransport.isOpen() : false;
    227     }
    228 
    229     ImapResponse readResponse() throws IOException, MessagingException {
    230         return mParser.readResponse();
    231     }
    232 
    233     /**
    234      * Send a single command to the server.  The command will be preceded by an IMAP command
    235      * tag and followed by \r\n (caller need not supply them).
    236      *
    237      * @param command The command to send to the server
    238      * @param sensitive If true, the command will not be logged
    239      * @return Returns the command tag that was sent
    240      */
    241     String sendCommand(String command, boolean sensitive)
    242         throws MessagingException, IOException {
    243         open();
    244         String tag = Integer.toString(mNextCommandTag.incrementAndGet());
    245         String commandToSend = tag + " " + command;
    246         mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
    247         mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
    248         return tag;
    249     }
    250 
    251 
    252     /**
    253      * Send a single, complex command to the server.  The command will be preceded by an IMAP
    254      * command tag and followed by \r\n (caller need not supply them).  After each piece of the
    255      * command, a response will be read which MUST be a continuation request.
    256      *
    257      * @param commands An array of Strings comprising the command to be sent to the server
    258      * @return Returns the command tag that was sent
    259      */
    260     String sendComplexCommand(List<String> commands, boolean sensitive) throws MessagingException,
    261             IOException {
    262         open();
    263         String tag = Integer.toString(mNextCommandTag.incrementAndGet());
    264         int len = commands.size();
    265         for (int i = 0; i < len; i++) {
    266             String commandToSend = commands.get(i);
    267             // The first part of the command gets the tag
    268             if (i == 0) {
    269                 commandToSend = tag + " " + commandToSend;
    270             } else {
    271                 // Otherwise, read the response from the previous part of the command
    272                 ImapResponse response = readResponse();
    273                 // If it isn't a continuation request, that's an error
    274                 if (!response.isContinuationRequest()) {
    275                     throw new MessagingException("Expected continuation request");
    276                 }
    277             }
    278             // Send the command
    279             mTransport.writeLine(commandToSend, null);
    280             mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
    281         }
    282         return tag;
    283     }
    284 
    285     List<ImapResponse> executeSimpleCommand(String command) throws IOException,
    286             MessagingException {
    287         return executeSimpleCommand(command, false);
    288     }
    289 
    290     /**
    291      * Read and return all of the responses from the most recent command sent to the server
    292      *
    293      * @return a list of ImapResponses
    294      * @throws IOException
    295      * @throws MessagingException
    296      */
    297     List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
    298         ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
    299         ImapResponse response;
    300         do {
    301             response = mParser.readResponse();
    302             responses.add(response);
    303         } while (!response.isTagged());
    304         if (!response.isOk()) {
    305             final String toString = response.toString();
    306             final String alert = response.getAlertTextOrEmpty().getString();
    307             destroyResponses();
    308             throw new ImapException(toString, alert);
    309         }
    310         return responses;
    311     }
    312 
    313     /**
    314      * Execute a simple command at the server, a simple command being one that is sent in a single
    315      * line of text
    316      *
    317      * @param command the command to send to the server
    318      * @param sensitive whether the command should be redacted in logs (used for login)
    319      * @return a list of ImapResponses
    320      * @throws IOException
    321      * @throws MessagingException
    322      */
    323      List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
    324             throws IOException, MessagingException {
    325         sendCommand(command, sensitive);
    326         return getCommandResponses();
    327     }
    328 
    329      /**
    330       * Execute a complex command at the server, a complex command being one that must be sent in
    331       * multiple lines due to the use of string literals
    332       *
    333       * @param commands a list of strings that comprise the command to be sent to the server
    334       * @param sensitive whether the command should be redacted in logs (used for login)
    335       * @return a list of ImapResponses
    336       * @throws IOException
    337       * @throws MessagingException
    338       */
    339       List<ImapResponse> executeComplexCommand(List<String> commands, boolean sensitive)
    340             throws IOException, MessagingException {
    341         sendComplexCommand(commands, sensitive);
    342         return getCommandResponses();
    343     }
    344 
    345     /**
    346      * Query server for capabilities.
    347      */
    348     private ImapResponse queryCapabilities() throws IOException, MessagingException {
    349         ImapResponse capabilityResponse = null;
    350         for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
    351             if (r.is(0, ImapConstants.CAPABILITY)) {
    352                 capabilityResponse = r;
    353                 break;
    354             }
    355         }
    356         if (capabilityResponse == null) {
    357             throw new MessagingException("Invalid CAPABILITY response received");
    358         }
    359         return capabilityResponse;
    360     }
    361 
    362     /**
    363      * Sends client identification information to the IMAP server per RFC 2971. If
    364      * the server does not support the ID command, this will perform no operation.
    365      *
    366      * Interoperability hack:  Never send ID to *.secureserver.net, which sends back a
    367      * malformed response that our parser can't deal with.
    368      */
    369     private void doSendId(boolean hasIdCapability, String capabilities)
    370             throws MessagingException {
    371         if (!hasIdCapability) return;
    372 
    373         // Never send ID to *.secureserver.net
    374         String host = mTransport.getHost();
    375         if (host.toLowerCase().endsWith(".secureserver.net")) return;
    376 
    377         // Assign user-agent string (for RFC2971 ID command)
    378         String mUserAgent =
    379                 ImapStore.getImapId(mImapStore.getContext(), mUsername, host, capabilities);
    380 
    381         if (mUserAgent != null) {
    382             mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
    383         } else if (DEBUG_FORCE_SEND_ID) {
    384             mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
    385         }
    386         // else: mIdPhrase = null, no ID will be emitted
    387 
    388         // Send user-agent in an RFC2971 ID command
    389         if (mIdPhrase != null) {
    390             try {
    391                 executeSimpleCommand(mIdPhrase);
    392             } catch (ImapException ie) {
    393                 // Log for debugging, but this is not a fatal problem.
    394                 if (Email.DEBUG) {
    395                     Log.d(Logging.LOG_TAG, ie.toString());
    396                 }
    397             } catch (IOException ioe) {
    398                 // Special case to handle malformed OK responses and ignore them.
    399                 // A true IOException will recur on the following login steps
    400                 // This can go away after the parser is fixed - see bug 2138981
    401             }
    402         }
    403     }
    404 
    405     /**
    406      * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
    407      * explicitly sets a namespace (using setup UI) or if the server does not support the
    408      * namespace command, this will perform no operation.
    409      */
    410     private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
    411         // user did not specify a hard-coded prefix; try to get it from the server
    412         if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) {
    413             List<ImapResponse> responseList = Collections.emptyList();
    414 
    415             try {
    416                 responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
    417             } catch (ImapException ie) {
    418                 // Log for debugging, but this is not a fatal problem.
    419                 if (Email.DEBUG) {
    420                     Log.d(Logging.LOG_TAG, ie.toString());
    421                 }
    422             } catch (IOException ioe) {
    423                 // Special case to handle malformed OK responses and ignore them.
    424             }
    425 
    426             for (ImapResponse response: responseList) {
    427                 if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
    428                     ImapList namespaceList = response.getListOrEmpty(1);
    429                     ImapList namespace = namespaceList.getListOrEmpty(0);
    430                     String namespaceString = namespace.getStringOrEmpty(0).getString();
    431                     if (!TextUtils.isEmpty(namespaceString)) {
    432                         mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null));
    433                         mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString());
    434                     }
    435                 }
    436             }
    437         }
    438     }
    439 
    440     /**
    441      * Logs into the IMAP server
    442      */
    443     private void doLogin()
    444             throws IOException, MessagingException, AuthenticationFailedException {
    445         try {
    446             // TODO eventually we need to add additional authentication
    447             // options such as SASL
    448             executeSimpleCommand(mLoginPhrase, true);
    449         } catch (ImapException ie) {
    450             if (Email.DEBUG) {
    451                 Log.d(Logging.LOG_TAG, ie.toString());
    452             }
    453             throw new AuthenticationFailedException(ie.getAlertText(), ie);
    454 
    455         } catch (MessagingException me) {
    456             throw new AuthenticationFailedException(null, me);
    457         }
    458     }
    459 
    460     /**
    461      * Gets the path separator per the LIST command in RFC 3501. If the path separator
    462      * was obtained while obtaining the namespace or there is no prefix defined, this
    463      * will perform no operation.
    464      */
    465     private void doGetPathSeparator() throws MessagingException {
    466         // user did not specify a hard-coded prefix; try to get it from the server
    467         if (mImapStore.isUserPrefixSet()) {
    468             List<ImapResponse> responseList = Collections.emptyList();
    469 
    470             try {
    471                 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
    472             } catch (ImapException ie) {
    473                 // Log for debugging, but this is not a fatal problem.
    474                 if (Email.DEBUG) {
    475                     Log.d(Logging.LOG_TAG, ie.toString());
    476                 }
    477             } catch (IOException ioe) {
    478                 // Special case to handle malformed OK responses and ignore them.
    479             }
    480 
    481             for (ImapResponse response: responseList) {
    482                 if (response.isDataResponse(0, ImapConstants.LIST)) {
    483                     mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString());
    484                 }
    485             }
    486         }
    487     }
    488 
    489     /**
    490      * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
    491      * to use TLS or the server does not support the TLS capability, this will perform
    492      * no operation.
    493      */
    494     private ImapResponse doStartTls(boolean hasStartTlsCapability)
    495             throws IOException, MessagingException {
    496         if (mTransport.canTryTlsSecurity()) {
    497             if (hasStartTlsCapability) {
    498                 // STARTTLS
    499                 executeSimpleCommand(ImapConstants.STARTTLS);
    500 
    501                 mTransport.reopenTls();
    502                 mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
    503                 createParser();
    504                 // Per RFC requirement (3501-6.2.1) gather new capabilities
    505                 return(queryCapabilities());
    506             } else {
    507                 if (Email.DEBUG) {
    508                     Log.d(Logging.LOG_TAG, "TLS not supported but required");
    509                 }
    510                 throw new MessagingException(MessagingException.TLS_REQUIRED);
    511             }
    512         }
    513         return null;
    514     }
    515 
    516     /** @see DiscourseLogger#logLastDiscourse() */
    517     void logLastDiscourse() {
    518         mDiscourse.logLastDiscourse();
    519     }
    520 }