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:
parent
39745c3dc0
commit
284d8d7db5
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}.
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue