diff --git a/emailcommon/src/com/android/emailcommon/mail/Folder.java b/emailcommon/src/com/android/emailcommon/mail/Folder.java index ea7fc5984..e3062c497 100644 --- a/emailcommon/src/com/android/emailcommon/mail/Folder.java +++ b/emailcommon/src/com/android/emailcommon/mail/Folder.java @@ -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; diff --git a/emailcommon/src/com/android/emailcommon/utility/Utility.java b/emailcommon/src/com/android/emailcommon/utility/Utility.java index ce2b0bb0a..155df57ab 100644 --- a/emailcommon/src/com/android/emailcommon/utility/Utility.java +++ b/emailcommon/src/com/android/emailcommon/utility/Utility.java @@ -187,36 +187,6 @@ public class Utility { } } - /** - * Apply quoting rules per IMAP RFC, - * quoted = DQUOTE *QUOTED-CHAR DQUOTE - * QUOTED-CHAR = / "\" 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 diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index 55b9b55a7..86b63aaa0 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -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); } diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java index 53078b248..2596d5b7f 100644 --- a/src/com/android/email/mail/store/ImapStore.java +++ b/src/com/android/email/mail/store/ImapStore.java @@ -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 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 messageMap = new HashMap(); + 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}. diff --git a/src/com/android/email/mail/store/imap/ImapConstants.java b/src/com/android/email/mail/store/imap/ImapConstants.java index 9e882c71e..eee2ac44e 100644 --- a/src/com/android/email/mail/store/imap/ImapConstants.java +++ b/src/com/android/email/mail/store/imap/ImapConstants.java @@ -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"; diff --git a/src/com/android/email/mail/store/imap/ImapResponse.java b/src/com/android/email/mail/store/imap/ImapResponse.java index 35d93deb5..05bf594e6 100644 --- a/src/com/android/email/mail/store/imap/ImapResponse.java +++ b/src/com/android/email/mail/store/imap/ImapResponse.java @@ -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" diff --git a/src/com/android/email/mail/store/imap/ImapUtility.java b/src/com/android/email/mail/store/imap/ImapUtility.java new file mode 100644 index 000000000..dc7e98e96 --- /dev/null +++ b/src/com/android/email/mail/store/imap/ImapUtility.java @@ -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 = / "\" 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. + *
+     * sequence-number = nz-number / "*"
+     * sequence-range  = sequence-number ":" sequence-number
+     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+     * 
+ */ + public static String[] getImapSequenceValues(String set) { + ArrayList list = new ArrayList(); + 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. + *
+     * sequence-number = nz-number / "*"
+     * sequence-range  = sequence-number ":" sequence-number
+     * sequence-set    = (sequence-number / sequence-range) *("," sequence-set)
+     * 
+ */ + public static String[] getImapRangeValues(String range) { + ArrayList list = new ArrayList(); + 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); + } +} diff --git a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java index e8d52302a..136ea4b68 100644 --- a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java +++ b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java @@ -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(); diff --git a/tests/src/com/android/email/mail/store/imap/ImapUtilityTests.java b/tests/src/com/android/email/mail/store/imap/ImapUtilityTests.java new file mode 100644 index 000000000..1396ae6c6 --- /dev/null +++ b/tests/src/com/android/email/mail/store/imap/ImapUtilityTests.java @@ -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); + } +} diff --git a/tests/src/com/android/emailcommon/mail/MockFolder.java b/tests/src/com/android/emailcommon/mail/MockFolder.java index 56cac0b54..9992d760a 100644 --- a/tests/src/com/android/emailcommon/mail/MockFolder.java +++ b/tests/src/com/android/emailcommon/mail/MockFolder.java @@ -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) { } diff --git a/tests/src/com/android/emailcommon/utility/UtilityUnitTests.java b/tests/src/com/android/emailcommon/utility/UtilityUnitTests.java index d970ce8f1..a5aa6b32f 100644 --- a/tests/src/com/android/emailcommon/utility/UtilityUnitTests.java +++ b/tests/src/com/android/emailcommon/utility/UtilityUnitTests.java @@ -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 */