diff options
Diffstat (limited to 'src/com/android/email/mail/store/ImapConnection.java')
-rw-r--r-- | src/com/android/email/mail/store/ImapConnection.java | 636 |
1 files changed, 0 insertions, 636 deletions
diff --git a/src/com/android/email/mail/store/ImapConnection.java b/src/com/android/email/mail/store/ImapConnection.java deleted file mode 100644 index 3e71774fb..000000000 --- a/src/com/android/email/mail/store/ImapConnection.java +++ /dev/null @@ -1,636 +0,0 @@ -/* - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.email.mail.store; - -import android.text.TextUtils; -import android.util.Base64; - -import com.android.email.DebugUtils; -import com.android.email.mail.internet.AuthenticationCache; -import com.android.email.mail.store.ImapStore.ImapException; -import com.android.email.mail.store.imap.ImapConstants; -import com.android.email.mail.store.imap.ImapList; -import com.android.email.mail.store.imap.ImapResponse; -import com.android.email.mail.store.imap.ImapResponseParser; -import com.android.email.mail.store.imap.ImapUtility; -import com.android.email.mail.transport.DiscourseLogger; -import com.android.email.mail.transport.MailTransport; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.AuthenticationFailedException; -import com.android.emailcommon.mail.CertificateValidationException; -import com.android.emailcommon.mail.MessagingException; -import com.android.mail.utils.LogUtils; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.net.ssl.SSLException; - -/** - * A cacheable class that stores the details for a single IMAP connection. - */ -class ImapConnection { - // Always check in FALSE - private static final boolean DEBUG_FORCE_SEND_ID = false; - - /** ID capability per RFC 2971*/ - public static final int CAPABILITY_ID = 1 << 0; - /** NAMESPACE capability per RFC 2342 */ - public static final int CAPABILITY_NAMESPACE = 1 << 1; - /** STARTTLS capability per RFC 3501 */ - public static final int CAPABILITY_STARTTLS = 1 << 2; - /** UIDPLUS capability per RFC 4315 */ - public static final int CAPABILITY_UIDPLUS = 1 << 3; - - /** The capabilities supported; a set of CAPABILITY_* values. */ - private int mCapabilities; - static final String IMAP_REDACTED_LOG = "[IMAP command redacted]"; - MailTransport mTransport; - private ImapResponseParser mParser; - private ImapStore mImapStore; - private String mLoginPhrase; - private String mAccessToken; - private String mIdPhrase = null; - - /** # of command/response lines to log upon crash. */ - private static final int DISCOURSE_LOGGER_SIZE = 64; - private final DiscourseLogger mDiscourse = new DiscourseLogger(DISCOURSE_LOGGER_SIZE); - /** - * Next tag to use. All connections associated to the same ImapStore instance share the same - * counter to make tests simpler. - * (Some of the tests involve multiple connections but only have a single counter to track the - * tag.) - */ - private final AtomicInteger mNextCommandTag = new AtomicInteger(0); - - // Keep others from instantiating directly - ImapConnection(ImapStore store) { - setStore(store); - } - - void setStore(ImapStore store) { - // TODO: maybe we should throw an exception if the connection is not closed here, - // if it's not currently closed, then we won't reopen it, so if the credentials have - // changed, the connection will not be reestablished. - mImapStore = store; - mLoginPhrase = null; - } - - /** - * Generates and returns the phrase to be used for authentication. This will be a LOGIN with - * username and password, or an OAUTH authentication string, with username and access token. - * Currently, these are the only two auth mechanisms supported. - * - * @throws IOException - * @throws AuthenticationFailedException - * @return the login command string to sent to the IMAP server - */ - String getLoginPhrase() throws MessagingException, IOException { - // build the LOGIN string once (instead of over-and-over again.) - if (mImapStore.getUseOAuth()) { - // We'll recreate the login phrase if it's null, or if the access token - // has changed. - final String accessToken = AuthenticationCache.getInstance().retrieveAccessToken( - mImapStore.getContext(), mImapStore.getAccount()); - if (mLoginPhrase == null || !TextUtils.equals(mAccessToken, accessToken)) { - mAccessToken = accessToken; - final String oauthCode = "user=" + mImapStore.getUsername() + '\001' + - "auth=Bearer " + mAccessToken + '\001' + '\001'; - mLoginPhrase = ImapConstants.AUTHENTICATE + " " + ImapConstants.XOAUTH2 + " " + - Base64.encodeToString(oauthCode.getBytes(), Base64.NO_WRAP); - } - } else { - if (mLoginPhrase == null) { - if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) { - // build the LOGIN string once (instead of over-and-over again.) - // apply the quoting here around the built-up password - mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " " - + ImapUtility.imapQuoted(mImapStore.getPassword()); - } - } - } - return mLoginPhrase; - } - - void open() throws IOException, MessagingException { - if (mTransport != null && mTransport.isOpen()) { - return; - } - - try { - // copy configuration into a clean transport, if necessary - if (mTransport == null) { - mTransport = mImapStore.cloneTransport(); - } - - mTransport.open(); - - createParser(); - - // BANNER - mParser.readResponse(); - - // CAPABILITY - ImapResponse capabilities = queryCapabilities(); - - boolean hasStartTlsCapability = - capabilities.contains(ImapConstants.STARTTLS); - - // TLS - ImapResponse newCapabilities = doStartTls(hasStartTlsCapability); - if (newCapabilities != null) { - capabilities = newCapabilities; - } - - // NOTE: An IMAP response MUST be processed before issuing any new IMAP - // requests. Subsequent requests may destroy previous response data. As - // such, we save away capability information here for future use. - setCapabilities(capabilities); - String capabilityString = capabilities.flatten(); - - // ID - doSendId(isCapable(CAPABILITY_ID), capabilityString); - - // LOGIN - doLogin(); - - // NAMESPACE (only valid in the Authenticated state) - doGetNamespace(isCapable(CAPABILITY_NAMESPACE)); - - // Gets the path separator from the server - doGetPathSeparator(); - - mImapStore.ensurePrefixIsValid(); - } catch (SSLException e) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, e, "SSLException"); - } - throw new CertificateValidationException(e.getMessage(), e); - } catch (IOException ioe) { - // NOTE: Unlike similar code in POP3, I'm going to rethrow as-is. There is a lot - // of other code here that catches IOException and I don't want to break it. - // This catch is only here to enhance logging of connection-time issues. - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ioe, "IOException"); - } - throw ioe; - } finally { - destroyResponses(); - } - } - - /** - * Closes the connection and releases all resources. This connection can not be used again - * until {@link #setStore(ImapStore)} is called. - */ - void close() { - if (mTransport != null) { - mTransport.close(); - mTransport = null; - } - destroyResponses(); - mParser = null; - mImapStore = null; - } - - /** - * Returns whether or not the specified capability is supported by the server. - */ - private boolean isCapable(int capability) { - return (mCapabilities & capability) != 0; - } - - /** - * Sets the capability flags according to the response provided by the server. - * Note: We only set the capability flags that we are interested in. There are many IMAP - * capabilities that we do not track. - */ - private void setCapabilities(ImapResponse capabilities) { - if (capabilities.contains(ImapConstants.ID)) { - mCapabilities |= CAPABILITY_ID; - } - if (capabilities.contains(ImapConstants.NAMESPACE)) { - mCapabilities |= CAPABILITY_NAMESPACE; - } - if (capabilities.contains(ImapConstants.UIDPLUS)) { - mCapabilities |= CAPABILITY_UIDPLUS; - } - if (capabilities.contains(ImapConstants.STARTTLS)) { - mCapabilities |= CAPABILITY_STARTTLS; - } - } - - /** - * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and - * set it to {@link #mParser}. - * - * If we already have an {@link ImapResponseParser}, we - * {@link #destroyResponses()} and throw it away. - */ - private void createParser() { - destroyResponses(); - mParser = new ImapResponseParser(mTransport.getInputStream(), mDiscourse); - } - - void destroyResponses() { - if (mParser != null) { - mParser.destroyResponses(); - } - } - - boolean isTransportOpenForTest() { - return mTransport != null && mTransport.isOpen(); - } - - ImapResponse readResponse() throws IOException, MessagingException { - return mParser.readResponse(); - } - - /** - * Send a single command to the server. The command will be preceded by an IMAP command - * tag and followed by \r\n (caller need not supply them). - * - * @param command The command to send to the server - * @param sensitive If true, the command will not be logged - * @return Returns the command tag that was sent - */ - String sendCommand(String command, boolean sensitive) - throws MessagingException, IOException { - LogUtils.d(Logging.LOG_TAG, "sendCommand %s", (sensitive ? IMAP_REDACTED_LOG : command)); - open(); - return sendCommandInternal(command, sensitive); - } - - String sendCommandInternal(String command, boolean sensitive) - throws MessagingException, IOException { - if (mTransport == null) { - throw new IOException("Null transport"); - } - String tag = Integer.toString(mNextCommandTag.incrementAndGet()); - String commandToSend = tag + " " + command; - mTransport.writeLine(commandToSend, sensitive ? IMAP_REDACTED_LOG : null); - mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend); - return tag; - } - - /** - * Send a single, complex command to the server. The command will be preceded by an IMAP - * command tag and followed by \r\n (caller need not supply them). After each piece of the - * command, a response will be read which MUST be a continuation request. - * - * @param commands An array of Strings comprising the command to be sent to the server - * @return Returns the command tag that was sent - */ - String sendComplexCommand(List<String> commands, boolean sensitive) throws MessagingException, - IOException { - open(); - String tag = Integer.toString(mNextCommandTag.incrementAndGet()); - int len = commands.size(); - for (int i = 0; i < len; i++) { - String commandToSend = commands.get(i); - // The first part of the command gets the tag - if (i == 0) { - commandToSend = tag + " " + commandToSend; - } else { - // Otherwise, read the response from the previous part of the command - ImapResponse response = readResponse(); - // If it isn't a continuation request, that's an error - if (!response.isContinuationRequest()) { - throw new MessagingException("Expected continuation request"); - } - } - // Send the command - mTransport.writeLine(commandToSend, null); - mDiscourse.addSentCommand(sensitive ? IMAP_REDACTED_LOG : commandToSend); - } - return tag; - } - - List<ImapResponse> executeSimpleCommand(String command) throws IOException, MessagingException { - return executeSimpleCommand(command, false); - } - - /** - * Read and return all of the responses from the most recent command sent to the server - * - * @return a list of ImapResponses - * @throws IOException - * @throws MessagingException - */ - List<ImapResponse> getCommandResponses() throws IOException, MessagingException { - final List<ImapResponse> responses = new ArrayList<ImapResponse>(); - ImapResponse response; - do { - response = mParser.readResponse(); - responses.add(response); - } while (!response.isTagged()); - - if (!response.isOk()) { - final String toString = response.toString(); - final String alert = response.getAlertTextOrEmpty().getString(); - final String responseCode = response.getResponseCodeOrEmpty().getString(); - destroyResponses(); - - // if the response code indicates an error occurred within the server, indicate that - if (ImapConstants.UNAVAILABLE.equals(responseCode)) { - throw new MessagingException(MessagingException.SERVER_ERROR, alert); - } - - throw new ImapException(toString, alert, responseCode); - } - return responses; - } - - /** - * Execute a simple command at the server, a simple command being one that is sent in a single - * line of text - * - * @param command the command to send to the server - * @param sensitive whether the command should be redacted in logs (used for login) - * @return a list of ImapResponses - * @throws IOException - * @throws MessagingException - */ - List<ImapResponse> executeSimpleCommand(String command, boolean sensitive) - throws IOException, MessagingException { - // TODO: It may be nice to catch IOExceptions and close the connection here. - // Currently, we expect callers to do that, but if they fail to we'll be in a broken state. - sendCommand(command, sensitive); - return getCommandResponses(); - } - - /** - * Execute a complex command at the server, a complex command being one that must be sent in - * multiple lines due to the use of string literals - * - * @param commands a list of strings that comprise the command to be sent to the server - * @param sensitive whether the command should be redacted in logs (used for login) - * @return a list of ImapResponses - * @throws IOException - * @throws MessagingException - */ - List<ImapResponse> executeComplexCommand(List<String> commands, boolean sensitive) - throws IOException, MessagingException { - sendComplexCommand(commands, sensitive); - return getCommandResponses(); - } - - /** - * Query server for capabilities. - */ - private ImapResponse queryCapabilities() throws IOException, MessagingException { - ImapResponse capabilityResponse = null; - for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) { - if (r.is(0, ImapConstants.CAPABILITY)) { - capabilityResponse = r; - break; - } - } - if (capabilityResponse == null) { - throw new MessagingException("Invalid CAPABILITY response received"); - } - return capabilityResponse; - } - - /** - * Sends client identification information to the IMAP server per RFC 2971. If - * the server does not support the ID command, this will perform no operation. - * - * Interoperability hack: Never send ID to *.secureserver.net, which sends back a - * malformed response that our parser can't deal with. - */ - private void doSendId(boolean hasIdCapability, String capabilities) - throws MessagingException { - if (!hasIdCapability) return; - - // Never send ID to *.secureserver.net - String host = mTransport.getHost(); - if (host.toLowerCase().endsWith(".secureserver.net")) return; - - // Assign user-agent string (for RFC2971 ID command) - String mUserAgent = - ImapStore.getImapId(mImapStore.getContext(), mImapStore.getUsername(), host, - capabilities); - - if (mUserAgent != null) { - mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")"; - } else if (DEBUG_FORCE_SEND_ID) { - mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL; - } - // else: mIdPhrase = null, no ID will be emitted - - // Send user-agent in an RFC2971 ID command - if (mIdPhrase != null) { - try { - executeSimpleCommand(mIdPhrase); - } catch (ImapException ie) { - // Log for debugging, but this is not a fatal problem. - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ie, "ImapException"); - } - } catch (IOException ioe) { - // Special case to handle malformed OK responses and ignore them. - // A true IOException will recur on the following login steps - // This can go away after the parser is fixed - see bug 2138981 - } - } - } - - /** - * Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user - * explicitly sets a namespace (using setup UI) or if the server does not support the - * namespace command, this will perform no operation. - */ - private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException { - // user did not specify a hard-coded prefix; try to get it from the server - if (hasNamespaceCapability && !mImapStore.isUserPrefixSet()) { - List<ImapResponse> responseList = Collections.emptyList(); - - try { - responseList = executeSimpleCommand(ImapConstants.NAMESPACE); - } catch (ImapException ie) { - // Log for debugging, but this is not a fatal problem. - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ie, "ImapException"); - } - } catch (IOException ioe) { - // Special case to handle malformed OK responses and ignore them. - } - - for (ImapResponse response: responseList) { - if (response.isDataResponse(0, ImapConstants.NAMESPACE)) { - ImapList namespaceList = response.getListOrEmpty(1); - ImapList namespace = namespaceList.getListOrEmpty(0); - String namespaceString = namespace.getStringOrEmpty(0).getString(); - if (!TextUtils.isEmpty(namespaceString)) { - mImapStore.setPathPrefix(ImapStore.decodeFolderName(namespaceString, null)); - mImapStore.setPathSeparator(namespace.getStringOrEmpty(1).getString()); - } - } - } - } - } - - /** - * Logs into the IMAP server - */ - private void doLogin() throws IOException, MessagingException, AuthenticationFailedException { - try { - if (mImapStore.getUseOAuth()) { - // SASL authentication can take multiple steps. Currently the only SASL - // authentication supported is OAuth. - doSASLAuth(); - } else { - executeSimpleCommand(getLoginPhrase(), true); - } - } catch (ImapException ie) { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ie, "ImapException"); - } - - final String code = ie.getResponseCode(); - final String alertText = ie.getAlertText(); - - // if the response code indicates expired or bad credentials, throw a special exception - if (ImapConstants.AUTHENTICATIONFAILED.equals(code) || - ImapConstants.EXPIRED.equals(code)) { - throw new AuthenticationFailedException(alertText, ie); - } - - throw new MessagingException(alertText, ie); - } - } - - /** - * Performs an SASL authentication. Currently, the only type of SASL authentication supported - * is OAuth. - * @throws MessagingException - * @throws IOException - */ - private void doSASLAuth() throws MessagingException, IOException { - LogUtils.d(Logging.LOG_TAG, "doSASLAuth"); - ImapResponse response = getOAuthResponse(); - if (!response.isOk()) { - // Failed to authenticate. This may be just due to an expired token. - LogUtils.d(Logging.LOG_TAG, "failed to authenticate, retrying"); - destroyResponses(); - // Clear the login phrase, this will force us to refresh the auth token. - mLoginPhrase = null; - // Close the transport so that we'll retry the authentication. - if (mTransport != null) { - mTransport.close(); - mTransport = null; - } - response = getOAuthResponse(); - if (!response.isOk()) { - LogUtils.d(Logging.LOG_TAG, "failed to authenticate, giving up"); - destroyResponses(); - throw new AuthenticationFailedException("OAuth failed after refresh"); - } - } - } - - private ImapResponse getOAuthResponse() throws IOException, MessagingException { - ImapResponse response; - sendCommandInternal(getLoginPhrase(), true); - do { - response = mParser.readResponse(); - } while (!response.isTagged() && !response.isContinuationRequest()); - - if (response.isContinuationRequest()) { - // SASL allows for a challenge/response type authentication, so if it doesn't yet have - // enough info, it will send back a continuation request. - // Currently, the only type of authentication we support is OAuth. The only case where - // it will send a continuation request is when we fail to authenticate. We need to - // reply with a CR/LF, and it will then return with a NO response. - sendCommandInternal("", true); - response = readResponse(); - } - - // if the response code indicates an error occurred within the server, indicate that - final String responseCode = response.getResponseCodeOrEmpty().getString(); - if (ImapConstants.UNAVAILABLE.equals(responseCode)) { - final String alert = response.getAlertTextOrEmpty().getString(); - throw new MessagingException(MessagingException.SERVER_ERROR, alert); - } - - return response; - } - - /** - * Gets the path separator per the LIST command in RFC 3501. If the path separator - * was obtained while obtaining the namespace or there is no prefix defined, this - * will perform no operation. - */ - private void doGetPathSeparator() throws MessagingException { - // user did not specify a hard-coded prefix; try to get it from the server - if (mImapStore.isUserPrefixSet()) { - List<ImapResponse> responseList = Collections.emptyList(); - - try { - responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\""); - } catch (ImapException ie) { - // Log for debugging, but this is not a fatal problem. - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, ie, "ImapException"); - } - } catch (IOException ioe) { - // Special case to handle malformed OK responses and ignore them. - } - - for (ImapResponse response: responseList) { - if (response.isDataResponse(0, ImapConstants.LIST)) { - mImapStore.setPathSeparator(response.getStringOrEmpty(2).getString()); - } - } - } - } - - /** - * Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted - * to use TLS or the server does not support the TLS capability, this will perform - * no operation. - */ - private ImapResponse doStartTls(boolean hasStartTlsCapability) - throws IOException, MessagingException { - if (mTransport.canTryTlsSecurity()) { - if (hasStartTlsCapability) { - // STARTTLS - executeSimpleCommand(ImapConstants.STARTTLS); - - mTransport.reopenTls(); - createParser(); - // Per RFC requirement (3501-6.2.1) gather new capabilities - return(queryCapabilities()); - } else { - if (DebugUtils.DEBUG) { - LogUtils.d(Logging.LOG_TAG, "TLS not supported but required"); - } - throw new MessagingException(MessagingException.TLS_REQUIRED); - } - } - return null; - } - - /** @see DiscourseLogger#logLastDiscourse() */ - void logLastDiscourse() { - mDiscourse.logLastDiscourse(); - } -} |