Support IMAP search in UTF-8, in addition to US-ASCII
* The existing IMAP search code is well-known to be primitive and largely broken. In particular, it fails with any non-ASCII character and with a variety of symbols (e.g. quotes, slashes, etc.) Basically, it's an accident waiting to happen, returning empty data sets even when the query might reasonably be expected (or known) to return valid data. * The current CL uses the IMAP literal string format to represent the query text; this string can be sent either in ascii or in UTF-8, and since it is sent as an octet (byte) count followed by 8-bit data, it also solves any quoting issues that might come up. So, we kill two birds with one stone. * The bug in question was punted to a subsequent MR; however, I think it would be a mistake to ship the code without this CL, which has been tested against the three common IMAP servers that we aim to support. Bug: 4690713 Change-Id: Iaa542bfc56737871f3cbc9c83f0e768415a7f2b6
This commit is contained in:
parent
8790f09a99
commit
20bf1f632f
@ -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.
|
||||
*/
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user