709 lines
28 KiB
Java
709 lines
28 KiB
Java
/*
|
|
* 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.net.SocketTimeoutException;
|
|
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;
|
|
|
|
// RFC 2177 defines that IDLE connections must be refreshed at least every 29 minutes
|
|
public static final int PING_IDLE_TIMEOUT = 29 * 60 * 1000;
|
|
|
|
// Special timeout for DONE operations
|
|
public static final int DONE_TIMEOUT = 5 * 1000;
|
|
|
|
// Time to wait between the first idle message and triggering the changes
|
|
private static final int IDLE_OP_READ_TIMEOUT = 500;
|
|
|
|
/** 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;
|
|
/** IDLE capability per RFC 2177 */
|
|
public static final int CAPABILITY_IDLE = 1 << 4;
|
|
|
|
/** 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;
|
|
|
|
private boolean mIdling = false;
|
|
|
|
/** # 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;
|
|
}
|
|
|
|
int getReadTimeout() throws IOException {
|
|
if (mTransport == null) {
|
|
return MailTransport.SOCKET_READ_TIMEOUT;
|
|
}
|
|
return mTransport.getReadTimeout();
|
|
}
|
|
|
|
void setReadTimeout(int timeout) throws IOException {
|
|
if (mTransport != null) {
|
|
mTransport.setReadTimeout(timeout);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns whether or not the specified capability is supported by the server.
|
|
*/
|
|
public 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;
|
|
}
|
|
if (capabilities.contains(ImapConstants.IDLE)) {
|
|
mCapabilities |= CAPABILITY_IDLE;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {
|
|
// Don't allow any command other than DONE when idling
|
|
if (mIdling && !command.equals(ImapConstants.DONE)) {
|
|
return null;
|
|
}
|
|
mIdling = command.equals(ImapConstants.IDLE);
|
|
|
|
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());
|
|
final String commandToSend;
|
|
if (command.equals(ImapConstants.DONE)) {
|
|
// Do not send a tag for DONE command
|
|
commandToSend = command;
|
|
} else {
|
|
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);
|
|
}
|
|
|
|
List<ImapResponse> executeIdleCommand() throws IOException, MessagingException {
|
|
mParser.expectIdlingResponse();
|
|
return executeSimpleCommand(ImapConstants.IDLE, 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 = null;
|
|
boolean idling = false;
|
|
boolean throwSocketTimeoutEx = true;
|
|
int lastSocketTimeout = getReadTimeout();
|
|
try {
|
|
do {
|
|
response = mParser.readResponse();
|
|
if (idling) {
|
|
setReadTimeout(IDLE_OP_READ_TIMEOUT);
|
|
throwSocketTimeoutEx = false;
|
|
}
|
|
responses.add(response);
|
|
if (response.isIdling()) {
|
|
idling = true;
|
|
}
|
|
} while (idling || !response.isTagged());
|
|
} catch (SocketTimeoutException ex) {
|
|
if (throwSocketTimeoutEx) {
|
|
throw ex;
|
|
}
|
|
} finally {
|
|
mParser.resetIdlingStatus();
|
|
if (lastSocketTimeout != getReadTimeout()) {
|
|
setReadTimeout(lastSocketTimeout);
|
|
}
|
|
}
|
|
|
|
// When idling, any response is valid; otherwise it must be OK
|
|
if (!response.isOk() && !idling) {
|
|
final String toString = response.toString();
|
|
final String status = response.getStatusOrEmpty().getString();
|
|
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, status, 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 status = ie.getStatus();
|
|
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) ||
|
|
(ImapConstants.NO.equals(status) && TextUtils.isEmpty(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();
|
|
}
|
|
}
|