Add support for UIDPLUS capability

When copying messages between mailboxes using standard IMAP, we must perform
a QUERY or FETCH in order to determine the new message UID. However, if the
server supports the UIDPLUS capability, the server will return the new UID
as part of the response to the "UID COPY" command.

This is the first of a couple modifications. We still need to fallback to a
less efficient QUERY/FETCH if the server does not support UIDPLUS.

bug 4092301

Change-Id: I9279f7fd70daf85adba3b3e202c12d67ddf91f22
This commit is contained in:
Todd Kennedy 2011-03-23 14:36:16 -07:00
parent 39745c3dc0
commit 284d8d7db5
11 changed files with 530 additions and 101 deletions

View File

@ -43,8 +43,7 @@ public abstract class Folder {
/**
* Callback for each message retrieval.
*
* Not all {@link Folder} implementation won't call it.
* (Currently {@link com.android.email.mail.store.LocalStore.LocalFolder} won't.)
* Not all {@link Folder} implementations may invoke it.
*/
public interface MessageRetrievalListener {
public void messageRetrieved(Message message);
@ -79,9 +78,8 @@ public abstract class Folder {
public abstract boolean isOpen();
/**
* Get the mode the folder was opened with. This may be different than the mode the open
* Returns the mode the folder was opened with. This may be different than the mode the open
* was requested with.
* @return
*/
public abstract OpenMode getMode() throws MessagingException;
@ -95,7 +93,6 @@ public abstract class Folder {
/**
* Attempt to create the given folder remotely using the given type.
* @param type
* @return true if created, false if cannot create (e.g. server side)
*/
public abstract boolean create(FolderType type) throws MessagingException;
@ -103,7 +100,7 @@ public abstract class Folder {
public abstract boolean exists() throws MessagingException;
/**
* @return A count of the messages in the selected folder.
* Returns the number of messages in the selected folder.
*/
public abstract int getMessageCount() throws MessagingException;
@ -119,9 +116,6 @@ public abstract class Folder {
* each fetch completes. Messages are downloaded as (as) lightweight (as
* possible) objects to be filled in with later requests. In most cases this
* means that only the UID is downloaded.
*
* @param uids
* @param listener
*/
public abstract Message[] getMessages(MessageRetrievalListener listener)
throws MessagingException;
@ -146,6 +140,9 @@ public abstract class Folder {
public abstract void appendMessages(Message[] messages) throws MessagingException;
/**
* Copies the given messages to the destination folder.
*/
public abstract void copyMessages(Message[] msgs, Folder folder,
MessageUpdateCallbacks callbacks) throws MessagingException;

View File

@ -187,36 +187,6 @@ public class Utility {
}
}
/**
* Apply quoting rules per IMAP RFC,
* quoted = DQUOTE *QUOTED-CHAR DQUOTE
* QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
* quoted-specials = DQUOTE / "\"
*
* This is used primarily for IMAP login, but might be useful elsewhere.
*
* NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check
* for trouble chars before calling the replace functions.
*
* @param s The string to be quoted.
* @return A copy of the string, having undergone quoting as described above
*/
public static String imapQuoted(String s) {
// First, quote any backslashes by replacing \ with \\
// regex Pattern: \\ (Java string const = \\\\)
// Substitute: \\\\ (Java string const = \\\\\\\\)
String result = s.replaceAll("\\\\", "\\\\\\\\");
// Then, quote any double-quotes by replacing " with \"
// regex Pattern: " (Java string const = \")
// Substitute: \\" (Java string const = \\\\\")
result = result.replaceAll("\"", "\\\\\"");
// return string with quotes around it
return "\"" + result + "\"";
}
/**
* A fast version of URLDecoder.decode() that works only with UTF-8 and does only two
* allocations. This version is around 3x as fast as the standard one and I'm using it

View File

@ -28,12 +28,13 @@ import com.android.emailcommon.mail.AuthenticationFailedException;
import com.android.emailcommon.mail.FetchProfile;
import com.android.emailcommon.mail.Flag;
import com.android.emailcommon.mail.Folder;
import com.android.emailcommon.mail.Folder.FolderType;
import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.mail.Message;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.mail.Part;
import com.android.emailcommon.mail.Folder.FolderType;
import com.android.emailcommon.mail.Folder.MessageRetrievalListener;
import com.android.emailcommon.mail.Folder.OpenMode;
import com.android.emailcommon.provider.EmailContent;
import com.android.emailcommon.provider.EmailContent.Attachment;
import com.android.emailcommon.provider.EmailContent.AttachmentColumns;
@ -566,6 +567,7 @@ public class MessagingController implements Runnable {
int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1;
int remoteEnd = remoteMessageCount;
remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null);
// TODO Why are we running through the list twice? Combine w/ for loop below
for (Message message : remoteMessages) {
remoteUidMap.put(message.getUid(), message);
}
@ -1417,8 +1419,8 @@ public class MessagingController implements Runnable {
*/
private void processPendingDataChange(Store remoteStore, Mailbox mailbox, boolean changeRead,
boolean changeFlagged, boolean changeMailbox, EmailContent.Message oldMessage,
EmailContent.Message newMessage) throws MessagingException {
Mailbox newMailbox = null;;
final EmailContent.Message newMessage) throws MessagingException {
Mailbox newMailbox = null;
// 0. No remote update if the message is local-only
if (newMessage.mServerId == null || newMessage.mServerId.equals("")
@ -1478,15 +1480,23 @@ public class MessagingController implements Runnable {
return;
}
// Copy the message to its new folder
remoteFolder.copyMessages(messages, toFolder, null);
remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() {
@Override
public void onMessageUidChange(Message message, String newUid) {
ContentValues cv = new ContentValues();
cv.put(EmailContent.Message.SERVER_ID, newUid);
// We only have one message, so, any updates _must_ be for it. Otherwise,
// we'd have to cycle through to find the one with the same server ID.
mContext.getContentResolver().update(ContentUris.withAppendedId(
EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null);
}
@Override
public void onMessageNotFound(Message message) {
}
});
// Delete the message from the remote source folder
remoteMessage.setFlag(Flag.DELETED, true);
remoteFolder.expunge();
// Set the serverId to 0, since we don't know what the new server id will be
ContentValues cv = new ContentValues();
cv.put(EmailContent.Message.SERVER_ID, "0");
mContext.getContentResolver().update(ContentUris.withAppendedId(
EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null);
}
remoteFolder.close(false);
}

View File

@ -27,6 +27,7 @@ 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.ImapString;
import com.android.email.mail.store.imap.ImapUtility;
import com.android.email.mail.transport.CountingOutputStream;
import com.android.email.mail.transport.DiscourseLogger;
import com.android.email.mail.transport.EOLConvertingOutputStream;
@ -106,7 +107,6 @@ public class ImapStore extends Store {
private static final int COPY_BUFFER_SIZE = 16*1024;
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED };
private final Context mContext;
private Transport mRootTransport;
private String mUsername;
@ -198,7 +198,7 @@ public class ImapStore extends Store {
// build the LOGIN string once (instead of over-and-over again.)
// apply the quoting here around the built-up password
mLoginPhrase = ImapConstants.LOGIN + " " + mUsername + " "
+ Utility.imapQuoted(mPassword);
+ ImapUtility.imapQuoted(mPassword);
}
}
@ -768,10 +768,56 @@ public class ImapStore extends Store {
MessageUpdateCallbacks callbacks) throws MessagingException {
checkOpen();
try {
mConnection.executeSimpleCommand(
List<ImapResponse> responseList = mConnection.executeSimpleCommand(
String.format(ImapConstants.UID_COPY + " %s \"%s\"",
joinMessageUids(messages),
encodeFolderName(folder.getName(), mStore.mPathPrefix)));
if (!mConnection.isCapable(ImapConnection.CAPABILITY_UIDPLUS)) {
// TODO Implement alternate way to fetch UIDs (e.g. perform a query)
return;
}
// Build a message map for faster UID matching
HashMap<String, Message> messageMap = new HashMap<String, Message>();
for (Message m : messages) {
messageMap.put(m.getUid(), m);
}
// Process response to get the new UIDs
for (ImapResponse response : responseList) {
// All "BAD" responses are bad. Only "NO", tagged responses are bad.
if (response.isBad() || (response.isNo() && response.isTagged())) {
String responseText = response.getStatusResponseTextOrEmpty().getString();
throw new MessagingException(responseText);
}
// Skip untagged responses; they're just status
if (!response.isTagged()) {
continue;
}
// No callback provided to report of UID changes; nothing more to do here
// NOTE: We check this here to catch any server errors
if (callbacks == null) {
continue;
}
ImapList copyResponse = response.getListOrEmpty(1);
String responseCode = copyResponse.getStringOrEmpty(0).getString();
if (ImapConstants.COPYUID.equals(responseCode)) {
String origIdSet = copyResponse.getStringOrEmpty(2).getString();
String newIdSet = copyResponse.getStringOrEmpty(3).getString();
String[] origIdArray = ImapUtility.getImapSequenceValues(origIdSet);
String[] newIdArray = ImapUtility.getImapSequenceValues(newIdSet);
// There has to be a 1:1 mapping between old and new IDs
if (origIdArray.length != newIdArray.length) {
throw new MessagingException("Set length mis-match; orig IDs \"" +
origIdSet + "\" new IDs \"" + newIdSet + "\"");
}
for (int i = 0; i < origIdArray.length; i++) {
final String id = origIdArray[i];
final Message m = messageMap.get(id);
if (m != null) {
callbacks.onMessageUidChange(m, newIdArray[i]);
}
}
}
}
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
} finally {
@ -1469,6 +1515,17 @@ public class ImapStore extends Store {
* A cacheable class that stores the details for a single IMAP connection.
*/
class ImapConnection {
/** 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;
private static final String IMAP_DEDACTED_LOG = "[IMAP command redacted]";
private Transport mTransport;
private ImapResponseParser mParser;
@ -1510,20 +1567,17 @@ public class ImapStore extends Store {
// 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.
boolean hasIdCapability =
capabilities.contains(ImapConstants.ID);
boolean hasNamespaceCapability =
capabilities.contains(ImapConstants.NAMESPACE);
setCapabilities(capabilities);
String capabilityString = capabilities.flatten();
// ID
doSendId(hasIdCapability, capabilityString);
doSendId(isCapable(CAPABILITY_ID), capabilityString);
// LOGIN
doLogin();
// NAMESPACE (only valid in the Authenticated state)
doGetNamespace(hasNamespaceCapability);
doGetNamespace(isCapable(CAPABILITY_NAMESPACE));
// Gets the path separator from the server
doGetPathSeparator();
@ -1554,6 +1608,33 @@ public class ImapStore extends Store {
}
}
/**
* 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;
}
}
/**
* Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and
* set it to {@link #mParser}.

View File

@ -40,6 +40,7 @@ public final class ImapConstants {
public static final String CHECK = "CHECK";
public static final String CLOSE = "CLOSE";
public static final String COPY = "COPY";
public static final String COPYUID = "COPYUID";
public static final String CREATE = "CREATE";
public static final String DELETE = "DELETE";
public static final String EXAMINE = "EXAMINE";
@ -85,6 +86,7 @@ public final class ImapConstants {
public static final String UID_SEARCH = "UID SEARCH";
public static final String UID_STORE = "UID STORE";
public static final String UIDNEXT = "UIDNEXT";
public static final String UIDPLUS = "UIDPLUS";
public static final String UIDVALIDITY = "UIDVALIDITY";
public static final String UNSEEN = "UNSEEN";
public static final String UNSUBSCRIBE = "UNSUBSCRIBE";

View File

@ -62,6 +62,20 @@ public class ImapResponse extends ImapList {
return is(0, ImapConstants.OK);
}
/**
* @return whether it's an BAD response.
*/
public boolean isBad() {
return is(0, ImapConstants.BAD);
}
/**
* @return whether it's an NO response.
*/
public boolean isNo() {
return is(0, ImapConstants.NO);
}
/**
* @return whether it's an {@code responseType} data response. (i.e. not tagged).
* @param index where {@code responseType} should appear. e.g. 1 for "FETCH"

View File

@ -0,0 +1,127 @@
/*
* 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.imap;
import com.android.emailcommon.Logging;
import android.util.Log;
import java.util.ArrayList;
/**
* Utility methods for use with IMAP.
*/
public class ImapUtility {
/**
* Apply quoting rules per IMAP RFC,
* quoted = DQUOTE *QUOTED-CHAR DQUOTE
* QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
* quoted-specials = DQUOTE / "\"
*
* This is used primarily for IMAP login, but might be useful elsewhere.
*
* NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check
* for trouble chars before calling the replace functions.
*
* @param s The string to be quoted.
* @return A copy of the string, having undergone quoting as described above
*/
public static String imapQuoted(String s) {
// First, quote any backslashes by replacing \ with \\
// regex Pattern: \\ (Java string const = \\\\)
// Substitute: \\\\ (Java string const = \\\\\\\\)
String result = s.replaceAll("\\\\", "\\\\\\\\");
// Then, quote any double-quotes by replacing " with \"
// regex Pattern: " (Java string const = \")
// Substitute: \\" (Java string const = \\\\\")
result = result.replaceAll("\"", "\\\\\"");
// return string with quotes around it
return "\"" + result + "\"";
}
/**
* Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a
* list of individual numbers. If the set is invalid, an empty array is returned.
* <pre>
* sequence-number = nz-number / "*"
* sequence-range = sequence-number ":" sequence-number
* sequence-set = (sequence-number / sequence-range) *("," sequence-set)
* </pre>
*/
public static String[] getImapSequenceValues(String set) {
ArrayList<String> list = new ArrayList<String>();
if (set != null) {
String[] setItems = set.split(",");
for (String item : setItems) {
if (item.indexOf(':') == -1) {
// simple item
try {
Integer.parseInt(item); // Don't need the value; just ensure it's valid
list.add(item);
} catch (NumberFormatException e) {
Log.d(Logging.LOG_TAG, "Invalid UID value", e);
}
} else {
// range
for (String rangeItem : getImapRangeValues(item)) {
list.add(rangeItem);
}
}
}
}
String[] stringList = new String[list.size()];
return list.toArray(stringList);
}
/**
* Expand the given number range into a list of individual numbers. If the range is not valid,
* an empty array is returned.
* <pre>
* sequence-number = nz-number / "*"
* sequence-range = sequence-number ":" sequence-number
* sequence-set = (sequence-number / sequence-range) *("," sequence-set)
* </pre>
*/
public static String[] getImapRangeValues(String range) {
ArrayList<String> list = new ArrayList<String>();
try {
if (range != null) {
int colonPos = range.indexOf(':');
if (colonPos > 0) {
int first = Integer.parseInt(range.substring(0, colonPos));
int second = Integer.parseInt(range.substring(colonPos + 1));
if (first < second) {
for (int i = first; i <= second; i++) {
list.add(Integer.toString(i));
}
} else {
for (int i = first; i >= second; i--) {
list.add(Integer.toString(i));
}
}
}
}
} catch (NumberFormatException e) {
Log.d(Logging.LOG_TAG, "Invalid range value", e);
}
String[] stringList = new String[list.size()];
return list.toArray(stringList);
}
}

View File

@ -87,6 +87,10 @@ public class ImapStoreUnitTests extends AndroidTestCase {
private ImapStore.ImapFolder mFolder = null;
private int mNextTag;
// Fields specific to the CopyMessages tests
private MockTransport mCopyMock;
private Folder mCopyToFolder;
private Message[] mCopyMessages;
/**
* Setup code. We generate a lightweight ImapStore and ImapStore.ImapFolder.
@ -133,7 +137,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
*/
public void testLoginFailure() throws Exception {
MockTransport mockTransport = openAndInjectMockTransport();
expectLogin(mockTransport, false, false, new String[] {"* iD nIL", "oK"},
expectLogin(mockTransport, false, false, false, new String[] {"* iD nIL", "oK"},
"nO authentication failed");
try {
@ -152,7 +156,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
false);
// try to open it, with STARTTLS
expectLogin(mockTransport, true, false,
expectLogin(mockTransport, true, false, false,
new String[] {"* iD nIL", "oK"}, "oK user authenticated (Success)");
mockTransport.expect(
getNextTag(false) + " SELECT \"" + FOLDER_ENCODED + "\"", new String[] {
@ -417,7 +421,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// Respond to the initial connection
mockTransport.expect(null, "* oK Imap 2000 Ready To Assist You");
// Return "ID" in the capability
expectCapability(mockTransport, true);
expectCapability(mockTransport, true, false);
// No TLS
// No ID (the special case for this server)
// LOGIN
@ -515,7 +519,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
*/
private void setupOpenFolder(MockTransport mockTransport, String readWriteMode) {
setupOpenFolder(mockTransport, new String[] {
"* iD nIL", "oK"}, readWriteMode);
"* iD nIL", "oK"}, readWriteMode, false);
}
/**
@ -532,7 +536,12 @@ public class ImapStoreUnitTests extends AndroidTestCase {
*/
private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse,
String readWriteMode) {
expectLogin(mockTransport, imapIdResponse);
setupOpenFolder(mockTransport, imapIdResponse, readWriteMode, false);
}
private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse,
String readWriteMode, boolean withUidPlus) {
expectLogin(mockTransport, imapIdResponse, withUidPlus);
expectSelect(mockTransport, readWriteMode);
}
@ -555,20 +564,21 @@ public class ImapStoreUnitTests extends AndroidTestCase {
}
private void expectLogin(MockTransport mockTransport) {
expectLogin(mockTransport, new String[] {"* iD nIL", "oK"});
expectLogin(mockTransport, new String[] {"* iD nIL", "oK"}, false);
}
private void expectLogin(MockTransport mockTransport, String[] imapIdResponse) {
expectLogin(mockTransport, false, (imapIdResponse != null), imapIdResponse,
private void expectLogin(MockTransport mockTransport, String[] imapIdResponse,
boolean withUidPlus) {
expectLogin(mockTransport, false, (imapIdResponse != null), withUidPlus, imapIdResponse,
"oK user authenticated (Success)");
}
private void expectLogin(MockTransport mockTransport, boolean startTls, boolean withId,
String[] imapIdResponse, String loginResponse) {
boolean withUidPlus, String[] imapIdResponse, String loginResponse) {
// inject boilerplate commands that match our typical login
mockTransport.expect(null, "* oK Imap 2000 Ready To Assist You");
expectCapability(mockTransport, withId);
expectCapability(mockTransport, withId, withUidPlus);
// TLS (if expected)
if (startTls) {
@ -576,7 +586,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
getNextTag(true) + " Ok starting TLS");
mockTransport.expectStartTls();
// After switching to TLS the client must re-query for capability
expectCapability(mockTransport, withId);
expectCapability(mockTransport, withId, withUidPlus);
}
// ID
@ -595,10 +605,11 @@ public class ImapStoreUnitTests extends AndroidTestCase {
getNextTag(true) + " " + loginResponse);
}
private void expectCapability(MockTransport mockTransport, boolean withId) {
String capabilityList = withId
? "* cAPABILITY iMAP4rev1 sTARTTLS aUTH=gSSAPI lOGINDISABLED iD"
: "* cAPABILITY iMAP4rev1 sTARTTLS aUTH=gSSAPI lOGINDISABLED";
private void expectCapability(MockTransport mockTransport, boolean withId,
boolean withUidPlus) {
String capabilityList = "* cAPABILITY iMAP4rev1 sTARTTLS aUTH=gSSAPI lOGINDISABLED";
capabilityList += withId ? " iD" : "";
capabilityList += withUidPlus ? " UiDPlUs" : "";
mockTransport.expect(getNextTag(false) + " CAPABILITY", new String[] {
capabilityList,
@ -1453,25 +1464,116 @@ public class ImapStoreUnitTests extends AndroidTestCase {
assertFalse(folder.create(FolderType.HOLDS_MESSAGES));
}
public void testCopy() throws Exception {
MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock);
private void setupCopyMessages(boolean withUidPlus) throws Exception {
mCopyMock = openAndInjectMockTransport();
setupOpenFolder(mCopyMock, new String[] {"* iD nIL", "oK"}, "rEAD-wRITE", withUidPlus);
mFolder.open(OpenMode.READ_WRITE, null);
Folder folderTo = mStore.getFolder("\u65E5\u672C\u8A9E");
Message[] messages = new Message[] {
mCopyToFolder = mStore.getFolder("\u65E5\u672C\u8A9E");
mCopyMessages = new Message[] {
mFolder.createMessage("11"),
mFolder.createMessage("12"),
};
}
mock.expect(getNextTag(false) + " UID COPY 11\\,12 \\\"&ZeVnLIqe-\\\"",
private String getCopyMessagesPattern() {
return getNextTag(false) + " UID COPY 11\\,12 \\\"&ZeVnLIqe-\\\"";
}
private static class CopyMessagesCallback implements Folder.MessageUpdateCallbacks {
int messageNotFoundCalled;
int messageUidChangeCalled;
@Override
public void onMessageNotFound(Message message) {
++messageNotFoundCalled;
}
@Override
public void onMessageUidChange(Message message, String newUid) {
++messageUidChangeCalled;
}
}
// TODO Test additional degenerate cases; src msg not found, ...
// Golden case; successful copy with UIDCOPY result
public void testCopyMessages1() throws Exception {
setupCopyMessages(true);
mCopyMock.expect(getCopyMessagesPattern(),
new String[] {
getNextTag(true) + " oK copy completed"
"* Ok COPY in progress",
getNextTag(true) + " oK [COPYUID 777 11,12 45,46] UID COPY completed"
});
mFolder.copyMessages(messages, folderTo, null);
CopyMessagesCallback cb = new CopyMessagesCallback();
mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);
// TODO: Test NO response. (src message not found)
assertEquals(0, cb.messageNotFoundCalled);
assertEquals(2, cb.messageUidChangeCalled);
}
// Degenerate case; NO, un-tagged response works
public void testCopyMessages2() throws Exception {
setupCopyMessages(true);
mCopyMock.expect(getCopyMessagesPattern(),
new String[] {
"* No Some error occured during the copy",
getNextTag(true) + " oK [COPYUID 777 11,12 45,46] UID COPY completed"
});
CopyMessagesCallback cb = new CopyMessagesCallback();
mFolder.copyMessages(mCopyMessages, mCopyToFolder, cb);
assertEquals(0, cb.messageNotFoundCalled);
assertEquals(2, cb.messageUidChangeCalled);
}
// Degenerate case; NO, tagged response throws MessagingException
public void testCopyMessages3() throws Exception {
try {
setupCopyMessages(false);
mCopyMock.expect(getCopyMessagesPattern(),
new String[] {
getNextTag(true) + " No copy did not finish"
});
mFolder.copyMessages(mCopyMessages, mCopyToFolder, null);
fail("MessagingException expected.");
} catch (MessagingException expected) {
}
}
// Degenerate case; BAD, un-tagged response throws MessagingException
public void testCopyMessages4() throws Exception {
try {
setupCopyMessages(true);
mCopyMock.expect(getCopyMessagesPattern(),
new String[] {
"* BAD failed for some reason",
getNextTag(true) + " Ok copy completed"
});
mFolder.copyMessages(mCopyMessages, mCopyToFolder, null);
fail("MessagingException expected.");
} catch (MessagingException expected) {
}
}
// Degenerate case; BAD, tagged response throws MessagingException
public void testCopyMessages5() throws Exception {
try {
setupCopyMessages(false);
mCopyMock.expect(getCopyMessagesPattern(),
new String[] {
getNextTag(true) + " BaD copy completed"
});
mFolder.copyMessages(mCopyMessages, mCopyToFolder, null);
fail("MessagingException expected.");
} catch (MessagingException expected) {
}
}
public void testGetUnreadMessageCount() throws Exception {
@ -1755,7 +1857,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
expectLogin(mock);
mStore.checkSettings();
expectLogin(mock, false, false,
expectLogin(mock, false, false, false,
new String[] {"* iD nIL", "oK"}, "nO authentication failed");
try {
mStore.checkSettings();

View File

@ -0,0 +1,140 @@
/*
* 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.imap;
import com.android.email.mail.store.imap.ImapUtility;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
import libcore.util.EmptyArray;
public class ImapUtilityTests extends AndroidTestCase {
/**
* Tests of the IMAP quoting rules function.
*/
public void testImapQuote() {
// Simple strings should come through with simple quotes
assertEquals("\"abcd\"", ImapUtility.imapQuoted("abcd"));
// Quoting internal double quotes with \
assertEquals("\"ab\\\"cd\"", ImapUtility.imapQuoted("ab\"cd"));
// Quoting internal \ with \\
assertEquals("\"ab\\\\cd\"", ImapUtility.imapQuoted("ab\\cd"));
}
/**
* Test getting elements of an IMAP sequence set.
*/
public void testGetImapSequenceValues() {
String[] expected;
String[] actual;
// Test valid sets
expected = new String[] {"1"};
actual = ImapUtility.getImapSequenceValues("1");
MoreAsserts.assertEquals(expected, actual);
expected = new String[] {"1", "3", "2"};
actual = ImapUtility.getImapSequenceValues("1,3,2");
MoreAsserts.assertEquals(expected, actual);
expected = new String[] {"4", "5", "6"};
actual = ImapUtility.getImapSequenceValues("4:6");
MoreAsserts.assertEquals(expected, actual);
expected = new String[] {"9", "8", "7"};
actual = ImapUtility.getImapSequenceValues("9:7");
MoreAsserts.assertEquals(expected, actual);
expected = new String[] {"1", "2", "3", "4", "9", "8", "7"};
actual = ImapUtility.getImapSequenceValues("1,2:4,9:7");
MoreAsserts.assertEquals(expected, actual);
// Test partially invalid sets
expected = new String[] { "1", "5" };
actual = ImapUtility.getImapSequenceValues("1,x,5");
MoreAsserts.assertEquals(expected, actual);
expected = new String[] { "1", "2", "3" };
actual = ImapUtility.getImapSequenceValues("a:d,1:3");
MoreAsserts.assertEquals(expected, actual);
// Test invalid sets
expected = EmptyArray.STRING;
actual = ImapUtility.getImapSequenceValues("");
MoreAsserts.assertEquals(expected, actual);
expected = EmptyArray.STRING;
actual = ImapUtility.getImapSequenceValues(null);
MoreAsserts.assertEquals(expected, actual);
expected = EmptyArray.STRING;
actual = ImapUtility.getImapSequenceValues("a");
MoreAsserts.assertEquals(expected, actual);
expected = EmptyArray.STRING;
actual = ImapUtility.getImapSequenceValues("1:x");
MoreAsserts.assertEquals(expected, actual);
}
/**
* Test getting elements of an IMAP range.
*/
public void testGetImapRangeValues() {
String[] expected;
String[] actual;
// Test valid ranges
expected = new String[] {"1", "2", "3"};
actual = ImapUtility.getImapRangeValues("1:3");
MoreAsserts.assertEquals(expected, actual);
expected = new String[] {"16", "15", "14"};
actual = ImapUtility.getImapRangeValues("16:14");
MoreAsserts.assertEquals(expected, actual);
// Test in-valid ranges
expected = EmptyArray.STRING;
actual = ImapUtility.getImapRangeValues("");
MoreAsserts.assertEquals(expected, actual);
expected = EmptyArray.STRING;
actual = ImapUtility.getImapRangeValues(null);
MoreAsserts.assertEquals(expected, actual);
expected = EmptyArray.STRING;
actual = ImapUtility.getImapRangeValues("a");
MoreAsserts.assertEquals(expected, actual);
expected = EmptyArray.STRING;
actual = ImapUtility.getImapRangeValues("6");
MoreAsserts.assertEquals(expected, actual);
expected = EmptyArray.STRING;
actual = ImapUtility.getImapRangeValues("1:3,6");
MoreAsserts.assertEquals(expected, actual);
expected = EmptyArray.STRING;
actual = ImapUtility.getImapRangeValues("1:x");
MoreAsserts.assertEquals(expected, actual);
expected = EmptyArray.STRING;
actual = ImapUtility.getImapRangeValues("1:*");
MoreAsserts.assertEquals(expected, actual);
}
}

View File

@ -32,7 +32,7 @@ public class MockFolder extends Folder {
}
@Override
public void copyMessages(Message[] msgs, Folder folder,
public void copyMessages(Message[] msgs, Folder folder,
MessageUpdateCallbacks callbacks) {
}

View File

@ -56,6 +56,8 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import libcore.util.EmptyArray;
/**
* This is a series of unit tests for the Utility class. These tests must be locally
* complete - no server(s) required.
@ -65,22 +67,6 @@ import java.util.Set;
*/
@SmallTest
public class UtilityUnitTests extends AndroidTestCase {
/**
* Tests of the IMAP quoting rules function.
*/
public void testImapQuote() {
// Simple strings should come through with simple quotes
assertEquals("\"abcd\"", Utility.imapQuoted("abcd"));
// Quoting internal double quotes with \
assertEquals("\"ab\\\"cd\"", Utility.imapQuoted("ab\"cd"));
// Quoting internal \ with \\
assertEquals("\"ab\\\\cd\"", Utility.imapQuoted("ab\\cd"));
}
/**
* Tests of the syncronization of array and types of the display folder names
*/