blob: af8773bf98c160a0ef914a1b6b8ea756005615b4 [file] [log] [blame]
/*
* Copyright (C) 2015 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.phone.common.mail.store;
import android.util.ArraySet;
import android.util.Base64;
import com.android.phone.common.mail.AuthenticationFailedException;
import com.android.phone.common.mail.CertificateValidationException;
import com.android.phone.common.mail.MailTransport;
import com.android.phone.common.mail.MessagingException;
import com.android.phone.common.mail.store.ImapStore.ImapException;
import com.android.phone.common.mail.store.imap.DigestMd5Utils;
import com.android.phone.common.mail.store.imap.ImapConstants;
import com.android.phone.common.mail.store.imap.ImapResponse;
import com.android.phone.common.mail.store.imap.ImapResponseParser;
import com.android.phone.common.mail.store.imap.ImapUtility;
import com.android.phone.common.mail.utils.LogUtils;
import com.android.phone.vvm.omtp.OmtpEvents;
import com.android.phone.vvm.omtp.VvmLog;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.net.ssl.SSLException;
/**
* A cacheable class that stores the details for a single IMAP connection.
*/
public class ImapConnection {
private final String TAG = "ImapConnection";
private String mLoginPhrase;
private ImapStore mImapStore;
private MailTransport mTransport;
private ImapResponseParser mParser;
private Set<String> mCapabilities = new ArraySet<>();
static final String IMAP_REDACTED_LOG = "[IMAP command redacted]";
/**
* 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);
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.
*
* @return the login command string to sent to the IMAP server
*/
String getLoginPhrase() {
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;
}
public 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();
// The server should greet us with something like
// * OK IMAP4rev1 Server
// consume the response before doing anything else.
ImapResponse response = mParser.readResponse(false);
if (!response.isOk()) {
mImapStore.getImapHelper()
.handleEvent(OmtpEvents.DATA_INVALID_INITIAL_SERVER_RESPONSE);
throw new MessagingException(
MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR,
"Invalid server initial response");
}
queryCapability();
maybeDoStartTls();
// LOGIN
doLogin();
} catch (SSLException e) {
LogUtils.d(TAG, "SSLException ", e);
mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_SSL_EXCEPTION);
throw new CertificateValidationException(e.getMessage(), e);
} catch (IOException ioe) {
LogUtils.d(TAG, "IOException", ioe);
mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_IOE_ON_OPEN);
throw ioe;
} finally {
destroyResponses();
}
}
void logout() {
try {
sendCommand(ImapConstants.LOGOUT, false);
if (!mParser.readResponse(true).is(0, ImapConstants.BYE)) {
VvmLog.e(TAG, "Server did not respond LOGOUT with BYE");
}
if (!mParser.readResponse(false).isOk()) {
VvmLog.e(TAG, "Server did not respond OK after LOGOUT");
}
} catch (IOException | MessagingException e) {
VvmLog.e(TAG, "Error while logging out:" + e);
}
}
/**
* 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) {
logout();
mTransport.close();
mTransport = null;
}
destroyResponses();
mParser = null;
mImapStore = null;
}
/**
* Attempts to convert the connection into secure connection.
*/
private void maybeDoStartTls() throws IOException, MessagingException {
// STARTTLS is required in the OMTP standard but not every implementation support it.
// Make sure the server does have this capability
if (hasCapability(ImapConstants.CAPABILITY_STARTTLS)) {
executeSimpleCommand(ImapConstants.STARTTLS);
mTransport.reopenTls();
createParser();
// The cached capabilities should be refreshed after TLS is established.
queryCapability();
}
}
/**
* Logs into the IMAP server
*/
private void doLogin() throws IOException, MessagingException, AuthenticationFailedException {
try {
if (mCapabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) {
doDigestMd5Auth();
} else {
executeSimpleCommand(getLoginPhrase(), true);
}
} catch (ImapException ie) {
LogUtils.d(TAG, "ImapException", ie);
String status = ie.getStatus();
String statusMessage = ie.getStatusMessage();
String alertText = ie.getAlertText();
if (ImapConstants.NO.equals(status)) {
switch (statusMessage) {
case ImapConstants.NO_UNKNOWN_USER:
mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_USER);
break;
case ImapConstants.NO_UNKNOWN_CLIENT:
mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_DEVICE);
break;
case ImapConstants.NO_INVALID_PASSWORD:
mImapStore.getImapHelper()
.handleEvent(OmtpEvents.DATA_AUTH_INVALID_PASSWORD);
break;
case ImapConstants.NO_MAILBOX_NOT_INITIALIZED:
mImapStore.getImapHelper()
.handleEvent(OmtpEvents.DATA_AUTH_MAILBOX_NOT_INITIALIZED);
break;
case ImapConstants.NO_SERVICE_IS_NOT_PROVISIONED:
mImapStore.getImapHelper()
.handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_PROVISIONED);
break;
case ImapConstants.NO_SERVICE_IS_NOT_ACTIVATED:
mImapStore.getImapHelper()
.handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_ACTIVATED);
break;
case ImapConstants.NO_USER_IS_BLOCKED:
mImapStore.getImapHelper()
.handleEvent(OmtpEvents.DATA_AUTH_USER_IS_BLOCKED);
break;
case ImapConstants.NO_APPLICATION_ERROR:
mImapStore.getImapHelper()
.handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
default:
mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_BAD_IMAP_CREDENTIAL);
}
throw new AuthenticationFailedException(alertText, ie);
}
mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE);
throw new MessagingException(alertText, ie);
}
}
private void doDigestMd5Auth() throws IOException, MessagingException {
// Initiate the authentication.
// The server will issue us a challenge, asking to run MD5 on the nonce with our password
// and other data, including the cnonce we randomly generated.
//
// C: a AUTHENTICATE DIGEST-MD5
// S: (BASE64) realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth",
// algorithm=md5-sess,charset=utf-8
List<ImapResponse> responses = executeSimpleCommand(
ImapConstants.AUTHENTICATE + " " + ImapConstants.AUTH_DIGEST_MD5);
String decodedChallenge = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
Map<String, String> challenge = DigestMd5Utils.parseDigestMessage(decodedChallenge);
DigestMd5Utils.Data data = new DigestMd5Utils.Data(mImapStore, mTransport, challenge);
String response = data.createResponse();
// Respond to the challenge. If the server accepts it, it will reply a response-auth which
// is the MD5 of our password and the cnonce we've provided, to prove the server does know
// the password.
//
// C: (BASE64) charset=utf-8,username="chris",realm="elwood.innosoft.com",
// nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk",
// digest-uri="imap/elwood.innosoft.com",
// response=d388dad90d4bbd760a152321f2143af7,qop=auth
// S: (BASE64) rspauth=ea40f60335c427b5527b84dbabcdfffd
responses = executeContinuationResponse(encodeBase64(response), true);
// Verify response-auth.
// If failed verifyResponseAuth() will throw a MessagingException, terminating the
// connection
String decodedResponseAuth = decodeBase64(responses.get(0).getStringOrEmpty(0).getString());
data.verifyResponseAuth(decodedResponseAuth);
// Send a empty response to indicate we've accepted the response-auth
//
// C: (empty)
// S: a OK User logged in
executeContinuationResponse("", false);
}
private static String decodeBase64(String string) {
return new String(Base64.decode(string, Base64.DEFAULT));
}
private static String encodeBase64(String string) {
return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP);
}
private void queryCapability() throws IOException, MessagingException {
List<ImapResponse> responses = executeSimpleCommand(ImapConstants.CAPABILITY);
mCapabilities.clear();
Set<String> disabledCapabilities = mImapStore.getImapHelper().getConfig()
.getDisabledCapabilities();
for (ImapResponse response : responses) {
if (response.isTagged()) {
continue;
}
for (int i = 0; i < response.size(); i++) {
String capability = response.getStringOrEmpty(i).getString();
if (disabledCapabilities != null) {
if (!disabledCapabilities.contains(capability)) {
mCapabilities.add(capability);
}
} else {
mCapabilities.add(capability);
}
}
}
LogUtils.d(TAG, "Capabilities: " + mCapabilities.toString());
}
private boolean hasCapability(String capability) {
return mCapabilities.contains(capability);
}
/**
* 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());
}
public void destroyResponses() {
if (mParser != null) {
mParser.destroyResponses();
}
}
public ImapResponse readResponse() throws IOException, MessagingException {
return mParser.readResponse(false);
}
public List<ImapResponse> executeSimpleCommand(String command)
throws IOException, MessagingException{
return executeSimpleCommand(command, false);
}
/**
* 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).
* 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
*/
public 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();
}
public String sendCommand(String command, boolean sensitive)
throws IOException, MessagingException {
open();
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 : command));
return tag;
}
List<ImapResponse> executeContinuationResponse(String response, boolean sensitive)
throws IOException, MessagingException {
mTransport.writeLine(response, (sensitive ? IMAP_REDACTED_LOG : response));
return getCommandResponses();
}
/**
* 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(false);
responses.add(response);
} while (!(response.isTagged() || response.isContinuationRequest()));
if (!(response.isOk() || response.isContinuationRequest())) {
final String toString = response.toString();
final String status = response.getStatusOrEmpty().getString();
final String statusMessage = response.getStatusResponseTextOrEmpty().getString();
final String alert = response.getAlertTextOrEmpty().getString();
final String responseCode = response.getResponseCodeOrEmpty().getString();
destroyResponses();
throw new ImapException(toString, status, statusMessage, alert, responseCode);
}
return responses;
}
}