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