Merge "Support IMAP search in UTF-8, in addition to US-ASCII"

This commit is contained in:
Marc Blank 2011-08-18 13:02:48 -07:00 committed by Android (Google) Code Review
commit 28a48b6001
2 changed files with 144 additions and 41 deletions

View File

@ -16,6 +16,9 @@
package com.android.email.mail.store;
import android.text.TextUtils;
import android.util.Log;
import com.android.email.Email;
import com.android.email.mail.Transport;
import com.android.email.mail.store.ImapStore.ImapException;
@ -31,9 +34,6 @@ import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.CertificateValidationException;
import com.android.emailcommon.mail.MessagingException;
import android.text.TextUtils;
import android.util.Log;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
@ -248,14 +248,53 @@ class ImapConnection {
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> executeSimpleCommand(String command, boolean sensitive)
throws IOException, MessagingException {
String tag = sendCommand(command, sensitive);
/**
* 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 {
ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
ImapResponse response;
do {
@ -271,6 +310,38 @@ class ImapConnection {
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 {
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.
*/

View File

@ -366,34 +366,37 @@ class ImapFolder extends Folder {
throw new Error("ImapStore.delete() not yet implemented");
}
/* package */ String[] searchForUids(String searchCriteria)
throws MessagingException {
String[] getSearchUids(List<ImapResponse> responses) {
// S: * SEARCH 2 3 6
final ArrayList<String> uids = new ArrayList<String>();
for (ImapResponse response : responses) {
if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
continue;
}
// Found SEARCH response data
for (int i = 1; i < response.size(); i++) {
ImapString s = response.getStringOrEmpty(i);
if (s.isString()) {
uids.add(s.getString());
}
}
}
return uids.toArray(Utility.EMPTY_STRINGS);
}
@VisibleForTesting
String[] searchForUids(String searchCriteria) throws MessagingException {
checkOpen();
List<ImapResponse> responses;
try {
try {
responses = mConnection.executeSimpleCommand(
ImapConstants.UID_SEARCH + " " + searchCriteria);
String command = ImapConstants.UID_SEARCH + " " + searchCriteria;
return getSearchUids(mConnection.executeSimpleCommand(command));
} catch (ImapException e) {
Log.d(Logging.LOG_TAG, "ImapException in search: " + searchCriteria);
return Utility.EMPTY_STRINGS; // not found;
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
// S: * SEARCH 2 3 6
final ArrayList<String> uids = new ArrayList<String>();
for (ImapResponse response : responses) {
if (!response.isDataResponse(0, ImapConstants.SEARCH)) {
continue;
}
// Found SEARCH response data
for (int i = 1; i < response.size(); i++) {
ImapString s = response.getStringOrEmpty(i);
if (s.isString()) {
uids.add(s.getString());
}
}
}
return uids.toArray(Utility.EMPTY_STRINGS);
} finally {
destroyResponses();
}
@ -413,29 +416,58 @@ class ImapFolder extends Folder {
return null;
}
@VisibleForTesting
protected static boolean isAsciiString(String str) {
int len = str.length();
for (int i = 0; i < len; i++) {
char c = str.charAt(i);
if (c >= 128) return false;
}
return true;
}
/**
* Retrieve messages based on search parameters. We search FROM, TO, CC, SUBJECT, and BODY
* We send: SEARCH OR FROM "foo" (OR TO "foo" (OR CC "foo" (OR SUBJECT "foo" BODY "foo")))
* TODO: Properly quote the filter
* We send: SEARCH OR FROM "foo" (OR TO "foo" (OR CC "foo" (OR SUBJECT "foo" BODY "foo"))), but
* with the additional CHARSET argument and sending "foo" as a literal (e.g. {3}<CRLF>foo}
*/
@Override
@VisibleForTesting
public Message[] getMessages(SearchParams params, MessageRetrievalListener listener)
throws MessagingException {
List<String> commands = new ArrayList<String>();
String filter = params.mFilter;
StringBuilder sb = new StringBuilder();
sb.append("OR FROM \"");
sb.append(filter);
sb.append("\" (OR TO \"");
sb.append(filter);
sb.append("\" (OR CC \"");
sb.append(filter);
sb.append("\" (OR SUBJECT \"");
sb.append(filter);
sb.append("\" BODY \"");
sb.append(filter);
sb.append("\")))");
return getMessagesInternal(searchForUids(sb.toString()), listener);
// All servers MUST accept US-ASCII, so we'll send this as the CHARSET unless we're really
// dealing with a string that contains non-ascii characters
String charset = "US-ASCII";
if (!isAsciiString(filter)) {
charset = "UTF-8";
}
// This is the length of the string in octets (bytes), formatted as a string literal {n}
String octetLength = "{" + filter.getBytes().length + "}";
// Break the command up into pieces ending with the string literal length
commands.add(ImapConstants.UID_SEARCH + " CHARSET " + charset + " OR FROM " + octetLength);
commands.add(filter + " (OR TO " + octetLength);
commands.add(filter + " (OR CC " + octetLength);
commands.add(filter + " (OR SUBJECT " + octetLength);
commands.add(filter + " BODY " + octetLength);
commands.add(filter + ")))");
return getMessagesInternal(complexSearchForUids(commands), listener);
}
/* package */ String[] complexSearchForUids(List<String> commands) throws MessagingException {
checkOpen();
try {
try {
return getSearchUids(mConnection.executeComplexCommand(commands, false));
} catch (ImapException e) {
return Utility.EMPTY_STRINGS; // not found;
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
}
} finally {
destroyResponses();
}
}
@Override