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.Base64;
     21 
     22 import com.android.email.DebugUtils;
     23 import com.android.email.mail.internet.AuthenticationCache;
     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 import com.android.mail.utils.LogUtils;
     37 
     38 import java.io.IOException;
     39 import java.util.ArrayList;
     40 import java.util.Collections;
     41 import java.util.List;
     42 import java.util.concurrent.atomic.AtomicInteger;
     43 
     44 import javax.net.ssl.SSLException;
     45 
     46 /**
     47  * A cacheable class that stores the details for a single IMAP connection.
     48  */
     49 class ImapConnection {
     50     // Always check in FALSE
     51     private static final boolean DEBUG_FORCE_SEND_ID = false;
     52 
     53     /** ID capability per RFC 2971*/
     54     public static final int CAPABILITY_ID        = 1 << 0;
     55     /** NAMESPACE capability per RFC 2342 */
     56     public static final int CAPABILITY_NAMESPACE = 1 << 1;
     57     /** STARTTLS capability per RFC 3501 */
     58     public static final int CAPABILITY_STARTTLS  = 1 << 2;
     59     /** UIDPLUS capability per RFC 4315 */
     60     public static final int CAPABILITY_UIDPLUS   = 1 << 3;
     61 
     62     /** The capabilities supported; a set of CAPABILITY_* values. */
     63     private int mCapabilities;
     64     static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
     65     MailTransport mTransport;
     66     private ImapResponseParser mParser;
     67     private ImapStore mImapStore;
     68     private String mLoginPhrase;
     69     private String mAccessToken;
     70     private String mIdPhrase = null;
     71 
     72     /** # of command/response lines to log upon crash. */
     73     private static final int DISCOURSE_LOGGER_SIZE = 64;
     74     private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE);
     75     /**
     76      * Next tag to use.  All connections associated to the same ImapStore instance share the same
     77      * counter to make tests simpler.
     78      * (Some of the tests involve multiple connections but only have a single counter to track the
     79      * tag.)
     80      */
     81     private final AtomicInteger mNextCommandTag = new AtomicInteger(0);
     82 
     83     // Keep others from instantiating directly
     84     ImapConnection(ImapStore store) {
     85         setStore(store);
     86     }
     87 
     88     void setStore(ImapStore store) {
     89         // TODO: maybe we should throw an exception if the connection is not closed here,
     90         // if it's not currently closed, then we won't reopen it, so if the credentials have
     91         // changed, the connection will not be reestablished.
     92         mImapStore = store;
     93         mLoginPhrase = null;
     94     }
     95 
     96     /**
     97      * Generates and returns the phrase to be used for authentication. This will be a LOGIN with
     98      * username and password, or an OAUTH authentication string, with username and access token.
     99      * Currently, these are the only two auth mechanisms supported.
    100      *
    101      * @throws IOException
    102      * @throws AuthenticationFailedException
    103      * @return the login command string to sent to the IMAP server
    104      */
    105     String getLoginPhrase() throws MessagingException, IOException {
    106         // build the LOGIN string once (instead of over-and-over again.)
    107         if (mImapStore.getUseOAuth()) {
    108             // We'll recreate the login phrase if it's null, or if the access token
    109             // has changed.
    110             final String accessToken = AuthenticationCache.getInstance().retrieveAccessToken(
    111                     mImapStore.getContext(), mImapStore.getAccount());
    112             if (mLoginPhrase == null || !TextUtils.equals(mAccessToken, accessToken)) {
    113                 mAccessToken = accessToken;
    114                 final String oauthCode = "user=" + mImapStore.getUsername() + '\001' +
    115                         "auth=Bearer " + mAccessToken + '\001' + '\001';
    116                 mLoginPhrase = ImapConstants.AUTHENTICATE + " " + ImapConstants.XOAUTH2 + " " +
    117                         Base64.encodeToString(oauthCode.getBytes(), Base64.NO_WRAP);
    118             }
    119         } else {
    120             if (mLoginPhrase == null) {
    121                 if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) {
    122                     // build the LOGIN string once (instead of over-and-over again.)
    123                     // apply the quoting here around the built-up password
    124                     mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " "
    125                             + ImapUtility.imapQuoted(mImapStore.getPassword());
    126                 }
    127             }
    128         }
    129         return mLoginPhrase;
    130     }
    131 
    132     void open() throws IOException, MessagingException {
    133         if (mTransport != null && mTransport.isOpen()) {
    134             return;
    135         }
    136 
    137         try {
    138             // copy configuration into a clean transport, if necessary
    139             if (mTransport == null) {
    140                 mTransport = mImapStore.cloneTransport();
    141             }
    142 
    143             mTransport.open();
    144 
    145             createParser();
    146 
    147             // BANNER
    148             mParser.readResponse();
    149 
    150             // CAPABILITY
    151             ImapResponse capabilities = queryCapabilities();
    152 
    153             boolean hasStartTlsCapability =
    154                 capabilities.contains(ImapConstants.STARTTLS);
    155 
    156             // TLS
    157             ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
    158             if (newCapabilities != null) {
    159                 capabilities = newCapabilities;
    160             }
    161 
    162             // NOTE: An IMAP response MUST be processed before issuing any new IMAP
    163             // requests. Subsequent requests may destroy previous response data. As
    164             // such, we save away capability information here for future use.
    165             setCapabilities(capabilities);
    166             String capabilityString = capabilities.flatten();
    167 
    168             // ID
    169             doSendId(isCapable(CAPABILITY_ID), capabilityString);
    170 
    171             // LOGIN
    172             doLogin();
    173 
    174             // NAMESPACE (only valid in the Authenticated state)
    175             doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
    176 
    177             // Gets the path separator from the server
    178             doGetPathSeparator();
    179 
    180             mImapStore.ensurePrefixIsValid();
    181         } catch (SSLException e) {
    182             if (DebugUtils.DEBUG) {
    183                 LogUtils.d(Logging.LOG_TAG, e, "SSLException");
    184             }
    185             throw new CertificateValidationException(e.getMessage(), e);
    186         } catch (IOException ioe) {
    187             // NOTE:  Unlike similar code in POP3, I'm going to rethrow as-is.  There is a lot
    188             // of other code here that catches IOException and I don't want to break it.
    189             // This catch is only here to enhance logging of connection-time issues.
    190             if (DebugUtils.DEBUG) {
    191                 LogUtils.d(Logging.LOG_TAG, ioe, "IOException");
    192             }
    193             throw ioe;
    194         } finally {
    195             destroyResponses();
    196         }
    197     }
    198 
    199     /**
    200      * Closes the connection and releases all resources. This connection can not be used again
    201      * until {@link #setStore(ImapStore)} is called.
    202      */
    203     void close() {
    204         if (mTransport != null) {
    205             mTransport.close();
    206             mTransport = null;
    207         }
    208         destroyResponses();
    209         mParser = null;
    210         mImapStore = null;
    211     }
    212 
    213     /**
    214      * Returns whether or not the specified capability is supported by the server.
    215      */
    216     private boolean isCapable(int capability) {
    217         return (mCapabilities & capability) != 0;
    218     }
    219 
    220     /**
    221      * Sets the capability flags according to the response provided by the server.
    222      * Note: We only set the capability flags that we are interested in. There are many IMAP
    223      * capabilities that we do not track.
    224      */
    225     private void setCapabilities(ImapResponse capabilities) {
    226         if (capabilities.contains(ImapConstants.ID)) {
    227             mCapabilities |= CAPABILITY_ID;
    228         }
    229         if (capabilities.contains(ImapConstants.NAMESPACE)) {
    230             mCapabilities |= CAPABILITY_NAMESPACE;
    231         }
    232         if (capabilities.contains(ImapConstants.UIDPLUS)) {
    233             mCapabilities |= CAPABILITY_UIDPLUS;
    234         }
    235         if (capabilities.contains(ImapConstants.STARTTLS)) {
    236             mCapabilities |= CAPABILITY_STARTTLS;
    237         }
    238     }
    239 
    240     /**
    241      * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
    242      * set it to {@link #mParser}.
    243      *
    244      * If we already have an {@link ImapResponseParser}, we
    245      * {@link #destroyResponses()} and throw it away.
    246      */
    247     private void createParser() {
    248         destroyResponses();
    249         mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse);
    250     }
    251 
    252     void destroyResponses() {
    253         if (mParser != null) {
    254             mParser.destroyResponses();
    255         }
    256     }
    257 
    258     boolean isTransportOpenForTest() {
    259         return mTransport != null && mTransport.isOpen();
    260     }
    261 
    262     ImapResponse readResponse() throws IOException, MessagingException {
    263         return mParser.readResponse();
    264     }
    265 
    266     /**
    267      * Send a single command to the server.  The command will be preceded by an IMAP command
    268      * tag and followed by \r\n (caller need not supply them).
    269      *
    270      * @param command The command to send to the server
    271      * @param sensitive If true, the command will not be logged
    272      * @return Returns the command tag that was sent
    273      */
    274     String sendCommand(String command, boolean sensitive)
    275             throws MessagingException, IOException {
    276         LogUtils.d(Logging.LOG_TAG, "sendCommand %s", (sensitive ? IMAP_REDACTED_LOG : command));
    277         open();
    278         return sendCommandInternal(command, sensitive);
    279     }
    280 
    281     String sendCommandInternal(String command, boolean sensitive)
    282             throws MessagingException, IOException {
    283         if (mTransport == null) {
    284             throw new IOException("Null transport");
    285         }
    286         String tag = Integer.toString(mNextCommandTag.incrementAndGet());
    287         String commandToSend = tag + " " + command;
    288         mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null);
    289         mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
    290         return tag;
    291     }
    292 
    293     /**
    294      * Send a single, complex command to the server.  The command will be preceded by an IMAP
    295      * command tag and followed by \r\n (caller need not supply them).  After each piece of the
    296      * command, a response will be read which MUST be a continuation request.
    297      *
    298      * @param commands An array of Strings comprising the command to be sent to the server
    299      * @return Returns the command tag that was sent
    300      */
    301     String sendComplexCommand(List<String> commands, boolean sensitive) throws MessagingException,
    302             IOException {
    303         open();
    304         String tag = Integer.toString(mNextCommandTag.incrementAndGet());
    305         int len = commands.size();
    306         for (int i = 0; i < len; i++) {
    307             String commandToSend = commands.get(i);
    308             // The first part of the command gets the tag
    309             if (i == 0) {
    310                 commandToSend = tag + " " + commandToSend;
    311             } else {
    312                 // Otherwise, read the response from the previous part of the command
    313                 ImapResponse response = readResponse();
    314                 // If it isn't a continuation request, that's an error
    315                 if (!response.isContinuationRequest()) {
    316                     throw new MessagingException("Expected continuation request");
    317                 }
    318             }
    319             // Send the command
    320             mTransport.writeLine(commandToSend, null);
    321             mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend);
    322         }
    323         return tag;
    324     }
    325 
    326     List<ImapResponse> executeSimpleCommand(String command) throws IOException, MessagingException {
    327         return executeSimpleCommand(command, false);
    328     }
    329 
    330     /**
    331      * Read and return all of the responses from the most recent command sent to the server
    332      *
    333      * @return a list of ImapResponses
    334      * @throws IOException
    335      * @throws MessagingException
    336      */
    337     List<ImapResponse> getCommandResponses() throws IOException, MessagingException {
    338         final List<ImapResponse> responses = new ArrayList<ImapResponse>();
    339         ImapResponse response;
    340         do {
    341             response = mParser.readResponse();
    342             responses.add(response);
    343         } while (!response.isTagged());
    344 
    345         if (!response.isOk()) {
    346             final String toString = response.toString();
    347             final String status = response.getStatusOrEmpty().getString();
    348             final String alert = response.getAlertTextOrEmpty().getString();
    349             final String responseCode = response.getResponseCodeOrEmpty().getString();
    350             destroyResponses();
    351 
    352             // if the response code indicates an error occurred within the server, indicate that
    353             if (ImapConstants.UNAVAILABLE.equals(responseCode)) {
    354                 throw new MessagingException(MessagingException.SERVER_ERROR, alert);
    355             }
    356 
    357             throw new ImapException(toString, status, alert, responseCode);
    358         }
    359         return responses;
    360     }
    361 
    362     /**
    363      * Execute a simple command at the server, a simple command being one that is sent in a single
    364      * line of text
    365      *
    366      * @param command the command to send to the server
    367      * @param sensitive whether the command should be redacted in logs (used for login)
    368      * @return a list of ImapResponses
    369      * @throws IOException
    370      * @throws MessagingException
    371      */
    372      List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
    373             throws IOException, MessagingException {
    374          // TODO: It may be nice to catch IOExceptions and close the connection here.
    375          // Currently, we expect callers to do that, but if they fail to we'll be in a broken state.
    376          sendCommand(command, sensitive);
    377          return getCommandResponses();
    378     }
    379 
    380      /**
    381       * Execute a complex command at the server, a complex command being one that must be sent in
    382       * multiple lines due to the use of string literals
    383       *
    384       * @param commands a list of strings that comprise the command to be sent to the server
    385       * @param sensitive whether the command should be redacted in logs (used for login)
    386       * @return a list of ImapResponses
    387       * @throws IOException
    388       * @throws MessagingException
    389       */
    390       List<ImapResponse> executeComplexCommand(List<String> commands, boolean sensitive)
    391             throws IOException, MessagingException {
    392           sendComplexCommand(commands, sensitive);
    393           return getCommandResponses();
    394       }
    395 
    396     /**
    397      * Query server for capabilities.
    398      */
    399     private ImapResponse queryCapabilities() throws IOException, MessagingException {
    400         ImapResponse capabilityResponse = null;
    401         for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
    402             if (r.is(0, ImapConstants.CAPABILITY)) {
    403                 capabilityResponse = r;
    404                 break;
    405             }
    406         }
    407         if (capabilityResponse == null) {
    408             throw new MessagingException("Invalid CAPABILITY response received");
    409         }
    410         return capabilityResponse;
    411     }
    412 
    413     /**
    414      * Sends client identification information to the IMAP server per RFC 2971. If
    415      * the server does not support the ID command, this will perform no operation.
    416      *
    417      * Interoperability hack:  Never send ID to *.secureserver.net, which sends back a
    418      * malformed response that our parser can't deal with.
    419      */
    420     private void doSendId(boolean hasIdCapability, String capabilities)
    421             throws MessagingException {
    422         if (!hasIdCapability) return;
    423 
    424         // Never send ID to *.secureserver.net
    425         String host = mTransport.getHost();
    426         if (host.toLowerCase().endsWith(".secureserver.net")) return;
    427 
    428         // Assign user-agent string (for RFC2971 ID command)
    429         String mUserAgent =
    430                 ImapStore.getImapId(mImapStore.getContext(), mImapStore.getUsername(), host,
    431                         capabilities);
    432 
    433         if (mUserAgent != null) {
    434             mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
    435         } else if (DEBUG_FORCE_SEND_ID) {
    436             mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
    437         }
    438         // else: mIdPhrase = null, no ID will be emitted
    439 
    440         // Send user-agent in an RFC2971 ID command
    441         if (mIdPhrase != null) {
    442             try {
    443                 executeSimpleCommand(mIdPhrase);
    444             } catch (ImapException ie) {
    445                 // Log for debugging, but this is not a fatal problem.
    446                 if (DebugUtils.DEBUG) {
    447                     LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
    448                 }
    449             } catch (IOException ioe) {
    450                 // Special case to handle malformed OK responses and ignore them.
    451                 // A true IOException will recur on the following login steps
    452                 // This can go away after the parser is fixed - see bug 2138981
    453             }
    454         }
    455     }
    456 
    457     /**
    458      * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
    459      * explicitly sets a namespace (using setup UI) or if the server does not support the
    460      * namespace command, this will perform no operation.
    461      */
    462     private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
    463         // user did not specify a hard-coded prefix; try to get it from the server
    464         if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) {
    465             List<ImapResponse> responseList = Collections.emptyList();
    466 
    467             try {
    468                 responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
    469             } catch (ImapException ie) {
    470                 // Log for debugging, but this is not a fatal problem.
    471                 if (DebugUtils.DEBUG) {
    472                     LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
    473                 }
    474             } catch (IOException ioe) {
    475                 // Special case to handle malformed OK responses and ignore them.
    476             }
    477 
    478             for (ImapResponse response: responseList) {
    479                 if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
    480                     ImapList namespaceList = response.getListOrEmpty(1);
    481                     ImapList namespace = namespaceList.getListOrEmpty(0);
    482                     String namespaceString = namespace.getStringOrEmpty(0).getString();
    483                     if (!TextUtils.isEmpty(namespaceString)) {
    484                         mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null));
    485                         mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString());
    486                     }
    487                 }
    488             }
    489         }
    490     }
    491 
    492     /**
    493      * Logs into the IMAP server
    494      */
    495     private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
    496         try {
    497             if (mImapStore.getUseOAuth()) {
    498                 // SASL authentication can take multiple steps. Currently the only SASL
    499                 // authentication supported is OAuth.
    500                 doSASLAuth();
    501             } else {
    502                 executeSimpleCommand(getLoginPhrase(), true);
    503             }
    504         } catch (ImapException ie) {
    505             if (DebugUtils.DEBUG) {
    506                 LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
    507             }
    508 
    509             final String status = ie.getStatus();
    510             final String code = ie.getResponseCode();
    511             final String alertText = ie.getAlertText();
    512 
    513             // if the response code indicates expired or bad credentials, throw a special exception
    514             if (ImapConstants.AUTHENTICATIONFAILED.equals(code) ||
    515                     ImapConstants.EXPIRED.equals(code) ||
    516                     (ImapConstants.NO.equals(status) && TextUtils.isEmpty(code))) {
    517                 throw new AuthenticationFailedException(alertText, ie);
    518             }
    519 
    520             throw new MessagingException(alertText, ie);
    521         }
    522     }
    523 
    524     /**
    525      * Performs an SASL authentication. Currently, the only type of SASL authentication supported
    526      * is OAuth.
    527      * @throws MessagingException
    528      * @throws IOException
    529      */
    530     private void doSASLAuth() throws MessagingException, IOException {
    531         LogUtils.d(Logging.LOG_TAG, "doSASLAuth");
    532         ImapResponse response = getOAuthResponse();
    533         if (!response.isOk()) {
    534             // Failed to authenticate. This may be just due to an expired token.
    535             LogUtils.d(Logging.LOG_TAG, "failed to authenticate, retrying");
    536             destroyResponses();
    537             // Clear the login phrase, this will force us to refresh the auth token.
    538             mLoginPhrase = null;
    539             // Close the transport so that we'll retry the authentication.
    540             if (mTransport != null) {
    541                 mTransport.close();
    542                 mTransport = null;
    543             }
    544             response = getOAuthResponse();
    545             if (!response.isOk()) {
    546                 LogUtils.d(Logging.LOG_TAG, "failed to authenticate, giving up");
    547                 destroyResponses();
    548                 throw new AuthenticationFailedException("OAuth failed after refresh");
    549             }
    550         }
    551     }
    552 
    553     private ImapResponse getOAuthResponse() throws IOException, MessagingException {
    554         ImapResponse response;
    555         sendCommandInternal(getLoginPhrase(), true);
    556         do {
    557             response = mParser.readResponse();
    558         } while (!response.isTagged() && !response.isContinuationRequest());
    559 
    560         if (response.isContinuationRequest()) {
    561             // SASL allows for a challenge/response type authentication, so if it doesn't yet have
    562             // enough info, it will send back a continuation request.
    563             // Currently, the only type of authentication we support is OAuth. The only case where
    564             // it will send a continuation request is when we fail to authenticate. We need to
    565             // reply with a CR/LF, and it will then return with a NO response.
    566             sendCommandInternal("", true);
    567             response = readResponse();
    568         }
    569 
    570         // if the response code indicates an error occurred within the server, indicate that
    571         final String responseCode = response.getResponseCodeOrEmpty().getString();
    572         if (ImapConstants.UNAVAILABLE.equals(responseCode)) {
    573             final String alert = response.getAlertTextOrEmpty().getString();
    574             throw new MessagingException(MessagingException.SERVER_ERROR, alert);
    575         }
    576 
    577         return response;
    578     }
    579 
    580     /**
    581      * Gets the path separator per the LIST command in RFC 3501. If the path separator
    582      * was obtained while obtaining the namespace or there is no prefix defined, this
    583      * will perform no operation.
    584      */
    585     private void doGetPathSeparator() throws MessagingException {
    586         // user did not specify a hard-coded prefix; try to get it from the server
    587         if (mImapStore.isUserPrefixSet()) {
    588             List<ImapResponse> responseList = Collections.emptyList();
    589 
    590             try {
    591                 responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
    592             } catch (ImapException ie) {
    593                 // Log for debugging, but this is not a fatal problem.
    594                 if (DebugUtils.DEBUG) {
    595                     LogUtils.d(Logging.LOG_TAG, ie, "ImapException");
    596                 }
    597             } catch (IOException ioe) {
    598                 // Special case to handle malformed OK responses and ignore them.
    599             }
    600 
    601             for (ImapResponse response: responseList) {
    602                 if (response.isDataResponse(0, ImapConstants.LIST)) {
    603                     mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString());
    604                 }
    605             }
    606         }
    607     }
    608 
    609     /**
    610      * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
    611      * to use TLS or the server does not support the TLS capability, this will perform
    612      * no operation.
    613      */
    614     private ImapResponse doStartTls(boolean hasStartTlsCapability)
    615             throws IOException, MessagingException {
    616         if (mTransport.canTryTlsSecurity()) {
    617             if (hasStartTlsCapability) {
    618                 // STARTTLS
    619                 executeSimpleCommand(ImapConstants.STARTTLS);
    620 
    621                 mTransport.reopenTls();
    622                 createParser();
    623                 // Per RFC requirement (3501-6.2.1) gather new capabilities
    624                 return(queryCapabilities());
    625             } else {
    626                 if (DebugUtils.DEBUG) {
    627                     LogUtils.d(Logging.LOG_TAG, "TLS not supported but required");
    628                 }
    629                 throw new MessagingException(MessagingException.TLS_REQUIRED);
    630             }
    631         }
    632         return null;
    633     }
    634 
    635     /** @see DiscourseLogger#logLastDiscourse() */
    636     void logLastDiscourse() {
    637         mDiscourse.logLastDiscourse();
    638     }
    639 }
    640