Implement IMAP prefix support

We support two different ways for an IMAP prefix to be specified:
  1. A text field on the IMAP configuration page. This is the most obvious to
     the end user. It is also an explicit, manual configuration.
  2. RFC2342 defines a NAMESPACE IMAP command to be able to query the prefix
     from the IMAP server. This is an automatic configuration without any
     user involvement (i.e. the UI will NOT change if a prefix is loaded in
     this way)

If the user goes to the trouble of specifying a prefix, we will always honour
it instead of the namespace returned by the IMAP server -- even if the user's
configuration is wrong.

bug 1592696

Change-Id: I6b94c7aaac538f6cd9dc4694b0f1634e8c956bc1
This commit is contained in:
Todd Kennedy 2011-02-10 13:47:35 -08:00
parent da71042569
commit 32311cce01
9 changed files with 412 additions and 124 deletions

View File

@ -258,7 +258,7 @@ public class MessagingController implements Runnable {
Store store = Store.getInstance(account.getStoreUri(mContext), mContext, null);
Folder[] remoteFolders = store.getPersonalNamespaces();
Folder[] remoteFolders = store.getAllFolders();
HashSet<String> remoteFolderNames = new HashSet<String>();
for (int i = 0, count = remoteFolders.length; i < count; i++) {

View File

@ -245,7 +245,7 @@ public abstract class Store {
public abstract Folder getFolder(String name) throws MessagingException;
public abstract Folder[] getPersonalNamespaces() throws MessagingException;
public abstract Folder[] getAllFolders() throws MessagingException;
public abstract Bundle checkSettings() throws MessagingException;

View File

@ -80,7 +80,7 @@ public class ExchangeStore extends Store {
}
@Override
public Folder[] getPersonalNamespaces() {
public Folder[] getAllFolders() {
return null;
}

View File

@ -57,7 +57,6 @@ import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Base64DataException;
import android.util.Config;
import android.util.Log;
import java.io.IOException;
@ -71,6 +70,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashSet;
@ -111,9 +111,10 @@ public class ImapStore extends Store {
private String mUsername;
private String mPassword;
private String mLoginPhrase;
private String mPathPrefix;
private String mIdPhrase = null;
private static String sImapId = null;
/*package*/ String mPathPrefix;
/*package*/ String mPathSeparator;
private final ConcurrentLinkedQueue<ImapConnection> mConnectionPool =
new ConcurrentLinkedQueue<ImapConnection>();
@ -240,11 +241,11 @@ public class ImapStore extends Store {
*
* @param userName the username of the account
* @param host the host (server) of the account
* @param capabilityResponse the capabilities list from the server
* @param capabilities a list of the capabilities from the server
* @return a String for use in an IMAP ID message.
*/
/* package */ static String getImapId(Context context, String userName, String host,
ImapResponse capabilityResponse) {
String capabilities) {
// The first section is global to all IMAP connections, and generates the fixed
// values in any IMAP ID message
synchronized (ImapStore.class) {
@ -266,7 +267,7 @@ public class ImapStore extends Store {
// Optionally add any vendor-supplied id keys
String vendorId = VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host,
capabilityResponse.flatten());
capabilities);
if (vendorId != null) {
id.append(' ');
id.append(vendorId);
@ -372,7 +373,7 @@ public class ImapStore extends Store {
@Override
public Folder getFolder(String name) throws MessagingException {
public Folder getFolder(String name) {
ImapFolder folder;
synchronized (mFolderCache) {
folder = mFolderCache.get(name);
@ -385,13 +386,18 @@ public class ImapStore extends Store {
}
@Override
public Folder[] getPersonalNamespaces() throws MessagingException {
public Folder[] getAllFolders() throws MessagingException {
ImapConnection connection = getConnection();
try {
ArrayList<Folder> folders = new ArrayList<Folder>();
List<ImapResponse> responses = connection.executeSimpleCommand(
String.format(ImapConstants.LIST + " \"\" \"%s*\"",
mPathPrefix == null ? "" : mPathPrefix));
// Establish a connection to the IMAP server; if necessary
// This ensures a valid prefix if the prefix is automatically set by the server
connection.executeSimpleCommand(ImapConstants.NOOP);
String imapCommand = ImapConstants.LIST + " \"\" \"*\"";
if (mPathPrefix != null) {
imapCommand = ImapConstants.LIST + " \"\" \"" + mPathPrefix + "*\"";
}
List<ImapResponse> responses = connection.executeSimpleCommand(imapCommand);
for (ImapResponse response : responses) {
// S: * LIST (\Noselect) "/" ~/Mail/foo
if (response.isDataResponse(0, ImapConstants.LIST)) {
@ -400,7 +406,7 @@ public class ImapStore extends Store {
// Get folder name.
ImapString encodedFolder = response.getStringOrEmpty(3);
if (encodedFolder.isEmpty()) continue;
String folder = decodeFolderName(encodedFolder.getString());
String folder = decodeFolderName(encodedFolder.getString(), mPathPrefix);
if (ImapConstants.INBOX.equalsIgnoreCase(folder)) {
continue;
}
@ -451,6 +457,19 @@ public class ImapStore extends Store {
return bundle;
}
/**
* Fixes the path prefix, if necessary. The path prefix must always end with the
* path separator.
*/
/*package*/ void ensurePrefixIsValid() {
// Make sure the path prefix ends with the path separator
if (!TextUtils.isEmpty(mPathPrefix) && !TextUtils.isEmpty(mPathSeparator)) {
if (!mPathPrefix.endsWith(mPathSeparator)) {
mPathPrefix = mPathPrefix + mPathSeparator;
}
}
}
/**
* Gets a connection if one is available from the pool, or creates a new one if not.
*/
@ -485,21 +504,37 @@ public class ImapStore extends Store {
}
}
/* package */ static String encodeFolderName(String name) {
/**
* Prepends the folder name with the given prefix and UTF-7 encodes it.
*/
/* package */ static String encodeFolderName(String name, String prefix) {
// do NOT add the prefix to the special name "INBOX"
if (ImapConstants.INBOX.equalsIgnoreCase(name)) return name;
// Prepend prefix
if (prefix != null) {
name = prefix + name;
}
// TODO bypass the conversion if name doesn't have special char.
ByteBuffer bb = MODIFIED_UTF_7_CHARSET.encode(name);
byte[] b = new byte[bb.limit()];
bb.get(b);
return Utility.fromAscii(b);
}
/* package */ static String decodeFolderName(String name) {
/**
* UTF-7 decodes the folder name and removes the given path prefix.
*/
/* package */ static String decodeFolderName(String name, String prefix) {
// TODO bypass the conversion if name doesn't have special char.
/*
* Convert the encoded name to US-ASCII, then pass it through the modified UTF-7
* decoder and return the Unicode String.
*/
return MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString();
String folder;
folder = MODIFIED_UTF_7_CHARSET.decode(ByteBuffer.wrap(Utility.toAscii(name))).toString();
if ((prefix != null) && folder.startsWith(prefix)) {
folder = folder.substring(prefix.length());
}
return folder;
}
/**
@ -526,7 +561,7 @@ public class ImapStore extends Store {
private OpenMode mMode;
private boolean mExists;
public ImapFolder(ImapStore store, String name) {
/*package*/ ImapFolder(ImapStore store, String name) {
mStore = store;
mName = name;
}
@ -574,7 +609,7 @@ public class ImapStore extends Store {
try {
List<ImapResponse> responses = mConnection.executeSimpleCommand(
String.format(ImapConstants.SELECT + " \"%s\"",
encodeFolderName(mName)));
encodeFolderName(mName, mStore.mPathPrefix)));
/*
* If the command succeeds we expect the folder has been opened read-write
* unless we are notified otherwise in the responses.
@ -628,7 +663,7 @@ public class ImapStore extends Store {
}
@Override
public OpenMode getMode() throws MessagingException {
public OpenMode getMode() {
return mMode;
}
@ -669,7 +704,7 @@ public class ImapStore extends Store {
try {
connection.executeSimpleCommand(String.format(
ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UIDVALIDITY + ")",
encodeFolderName(mName)));
encodeFolderName(mName, mStore.mPathPrefix)));
mExists = true;
return true;
@ -710,7 +745,7 @@ public class ImapStore extends Store {
}
try {
connection.executeSimpleCommand(String.format(ImapConstants.CREATE + " \"%s\"",
encodeFolderName(mName)));
encodeFolderName(mName, mStore.mPathPrefix)));
return true;
} catch (MessagingException me) {
@ -735,7 +770,7 @@ public class ImapStore extends Store {
mConnection.executeSimpleCommand(
String.format(ImapConstants.UID_COPY + " %s \"%s\"",
joinMessageUids(messages),
encodeFolderName(folder.getName())));
encodeFolderName(folder.getName(), mStore.mPathPrefix)));
} catch (IOException ioe) {
throw ioExceptionHandler(mConnection, ioe);
} finally {
@ -755,7 +790,7 @@ public class ImapStore extends Store {
int unreadMessageCount = 0;
List<ImapResponse> responses = mConnection.executeSimpleCommand(String.format(
ImapConstants.STATUS + " \"%s\" (" + ImapConstants.UNSEEN + ")",
encodeFolderName(mName)));
encodeFolderName(mName, mStore.mPathPrefix)));
// S: * STATUS mboxname (MESSAGES 231 UIDNEXT 44292)
for (ImapResponse response : responses) {
if (response.isDataResponse(0, ImapConstants.STATUS)) {
@ -772,7 +807,7 @@ public class ImapStore extends Store {
}
@Override
public void delete(boolean recurse) throws MessagingException {
public void delete(boolean recurse) {
throw new Error("ImapStore.delete() not yet implemented");
}
@ -846,8 +881,7 @@ public class ImapStore extends Store {
return getMessagesInternal(uids, listener);
}
public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener)
throws MessagingException {
public Message[] getMessagesInternal(String[] uids, MessageRetrievalListener listener) {
final ArrayList<Message> messages = new ArrayList<Message>(uids.length);
for (int i = 0; i < uids.length; i++) {
final String uid = uids[i];
@ -1061,7 +1095,7 @@ public class ImapStore extends Store {
}
@Override
public Flag[] getPermanentFlags() throws MessagingException {
public Flag[] getPermanentFlags() {
return PERMANENT_FLAGS;
}
@ -1295,7 +1329,7 @@ public class ImapStore extends Store {
mConnection.sendCommand(
String.format(ImapConstants.APPEND + " \"%s\" (%s) {%d}",
encodeFolderName(mName),
encodeFolderName(mName, mStore.mPathPrefix),
flagList,
out.getCount()), false);
ImapResponse response;
@ -1403,8 +1437,7 @@ public class ImapStore extends Store {
}
}
private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe)
throws MessagingException {
private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) {
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, "IO Exception detected: ", ioe);
}
@ -1426,7 +1459,7 @@ public class ImapStore extends Store {
}
@Override
public Message createMessage(String uid) throws MessagingException {
public Message createMessage(String uid) {
return new ImapMessage(uid, this);
}
}
@ -1462,70 +1495,39 @@ public class ImapStore extends Store {
mParser.readResponse();
// CAPABILITY
ImapResponse capabilityResponse = queryCapabilities();
ImapResponse capabilities = queryCapabilities();
boolean hasStartTlsCapability =
capabilities.contains(ImapConstants.STARTTLS);
// TLS
if (mTransport.canTryTlsSecurity()) {
if (capabilityResponse.contains(ImapConstants.STARTTLS)) {
// STARTTLS
executeSimpleCommand(ImapConstants.STARTTLS);
mTransport.reopenTls();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
createParser();
// Per RFC requirement (3501-6.2.1) gather new capabilities
capabilityResponse = queryCapabilities();
} else {
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
}
ImapResponse newCapabilities = doStartTls(hasStartTlsCapability);
if (newCapabilities != null) {
capabilities = newCapabilities;
}
// 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);
String capabilityString = capabilities.flatten();
// ID
if (capabilityResponse.contains(ImapConstants.ID)) {
// Assign user-agent string (for RFC2971 ID command)
String mUserAgent = getImapId(mContext, mUsername, mRootTransport.getHost(),
capabilityResponse);
if (mUserAgent != null) {
mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
} else if (DEBUG_FORCE_SEND_ID) {
mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
}
// else: mIdPhrase = null, no ID will be emitted
// Send user-agent in an RFC2971 ID command
if (mIdPhrase != null) {
try {
executeSimpleCommand(mIdPhrase);
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
// A true IOException will recur on the following login steps
// This can go away after the parser is fixed - see bug 2138981
}
}
}
doSendId(hasIdCapability, capabilityString);
// LOGIN
try {
// TODO eventually we need to add additional authentication
// options such as SASL
executeSimpleCommand(mLoginPhrase, true);
} catch (ImapException ie) {
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, ie.toString());
}
throw new AuthenticationFailedException(ie.getAlertText(), ie);
doLogin();
} catch (MessagingException me) {
throw new AuthenticationFailedException(null, me);
}
// NAMESPACE (only valid in the Authenticated state)
doGetNamespace(hasNamespaceCapability);
// Gets the path separator from the server
doGetPathSeparator();
ensurePrefixIsValid();
} catch (SSLException e) {
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, e.toString());
@ -1595,12 +1597,12 @@ public class ImapStore extends Store {
return tag;
}
public List<ImapResponse> executeSimpleCommand(String command) throws IOException,
/*package*/ List<ImapResponse> executeSimpleCommand(String command) throws IOException,
MessagingException {
return executeSimpleCommand(command, false);
}
public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
/*package*/ List<ImapResponse> executeSimpleCommand(String command, boolean sensitive)
throws IOException, MessagingException {
String tag = sendCommand(command, sensitive);
ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
@ -1635,14 +1637,161 @@ public class ImapStore extends Store {
return capabilityResponse;
}
/** @see ImapResponseParser#logLastDiscourse() */
/**
* Sends client identification information to the IMAP server per RFC 2971. If
* the server does not support the ID command, this will perform no operation.
*/
private void doSendId(boolean hasIdCapability, String capabilities)
throws MessagingException {
if (!hasIdCapability) return;
// Assign user-agent string (for RFC2971 ID command)
String mUserAgent =
getImapId(mContext, mUsername, mRootTransport.getHost(), capabilities);
if (mUserAgent != null) {
mIdPhrase = ImapConstants.ID + " (" + mUserAgent + ")";
} else if (DEBUG_FORCE_SEND_ID) {
mIdPhrase = ImapConstants.ID + " " + ImapConstants.NIL;
}
// else: mIdPhrase = null, no ID will be emitted
// Send user-agent in an RFC2971 ID command
if (mIdPhrase != null) {
try {
executeSimpleCommand(mIdPhrase);
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
// A true IOException will recur on the following login steps
// This can go away after the parser is fixed - see bug 2138981
}
}
}
/**
* Gets the user's Personal Namespace from the IMAP server per RFC 2342. If the user
* explicitly sets a namespace (using setup UI) or if the server does not support the
* namespace command, this will perform no operation.
*/
private void doGetNamespace(boolean hasNamespaceCapability) throws MessagingException {
// user did not specify a hard-coded prefix; try to get it from the server
if (hasNamespaceCapability && TextUtils.isEmpty(mPathPrefix)) {
List<ImapResponse> responseList = Collections.emptyList();
try {
responseList = executeSimpleCommand(ImapConstants.NAMESPACE);
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
}
for (ImapResponse response: responseList) {
if (response.isDataResponse(0, ImapConstants.NAMESPACE)) {
ImapList namespaceList = response.getListOrEmpty(1);
ImapList namespace = namespaceList.getListOrEmpty(0);
String namespaceString = namespace.getStringOrEmpty(0).getString();
if (!TextUtils.isEmpty(namespaceString)) {
mPathPrefix = decodeFolderName(namespaceString, null);
mPathSeparator = namespace.getStringOrEmpty(1).getString();
}
}
}
}
}
/**
* Logs into the IMAP server
*/
private void doLogin()
throws IOException, MessagingException, AuthenticationFailedException {
try {
// TODO eventually we need to add additional authentication
// options such as SASL
executeSimpleCommand(mLoginPhrase, true);
} catch (ImapException ie) {
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, ie.toString());
}
throw new AuthenticationFailedException(ie.getAlertText(), ie);
} catch (MessagingException me) {
throw new AuthenticationFailedException(null, me);
}
}
/**
* Gets the path separator per the LIST command in RFC 3501. If the path separator
* was obtained while obtaining the namespace or there is no prefix defined, this
* will perform no operation.
*/
private void doGetPathSeparator() throws MessagingException {
// user did not specify a hard-coded prefix; try to get it from the server
if (TextUtils.isEmpty(mPathSeparator) && !TextUtils.isEmpty(mPathPrefix)) {
List<ImapResponse> responseList = Collections.emptyList();
try {
responseList = executeSimpleCommand(ImapConstants.LIST + " \"\" \"\"");
} catch (ImapException ie) {
// Log for debugging, but this is not a fatal problem.
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, ie.toString());
}
} catch (IOException ioe) {
// Special case to handle malformed OK responses and ignore them.
}
for (ImapResponse response: responseList) {
if (response.isDataResponse(0, ImapConstants.LIST)) {
mPathSeparator = response.getStringOrEmpty(2).getString();
}
}
}
}
/**
* Starts a TLS session with the IMAP server per RFC 3501. If the user has not opted
* to use TLS or the server does not support the TLS capability, this will perform
* no operation.
*/
private ImapResponse doStartTls(boolean hasStartTlsCapability)
throws IOException, MessagingException {
if (mTransport.canTryTlsSecurity()) {
if (hasStartTlsCapability) {
// STARTTLS
executeSimpleCommand(ImapConstants.STARTTLS);
mTransport.reopenTls();
mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
createParser();
// Per RFC requirement (3501-6.2.1) gather new capabilities
return(queryCapabilities());
} else {
if (Email.DEBUG) {
Log.d(Email.LOG_TAG, "TLS not supported but required");
}
throw new MessagingException(MessagingException.TLS_REQUIRED);
}
}
return null;
}
/** @see DiscourseLogger#logLastDiscourse() */
public void logLastDiscourse() {
mDiscourse.logLastDiscourse();
}
}
static class ImapMessage extends MimeMessage {
ImapMessage(String uid, Folder folder) throws MessagingException {
ImapMessage(String uid, Folder folder) {
this.mUid = uid;
this.mFolder = folder;
}

View File

@ -157,7 +157,7 @@ public class Pop3Store extends Store {
}
@Override
public Folder[] getPersonalNamespaces() throws MessagingException {
public Folder[] getAllFolders() throws MessagingException {
return new Folder[] {
getFolder("INBOX"),
};

View File

@ -60,6 +60,7 @@ public final class ImapConstants {
public static final String LOGIN = "LOGIN";
public static final String LOGOUT = "LOGOUT";
public static final String LSUB = "LSUB";
public static final String NAMESPACE = "NAMESPACE";
public static final String NO = "NO";
public static final String NOOP = "NOOP";
public static final String OK = "OK";

View File

@ -24,7 +24,6 @@ import com.android.email.mail.transport.LoggingInputStream;
import com.android.emailcommon.mail.MessagingException;
import android.text.TextUtils;
import android.util.Config;
import android.util.Log;
import java.io.IOException;
@ -236,7 +235,7 @@ public class ImapResponseParser {
/**
* Read all bytes until \r\n.
*/
/* package */ String readUntilEol() throws IOException, MessagingException {
/* package */ String readUntilEol() throws IOException {
String ret = readUntil('\r');
expect('\n'); // TODO Should this really be error?
return ret;

View File

@ -191,7 +191,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// x-android-net-operator Carrier (Unreliable, so not tested here)
// AGUID A device+account UID
String id = ImapStore.getImapId(getContext(), "user-name", "host-name",
CAPABILITY_RESPONSE);
CAPABILITY_RESPONSE.flatten());
HashMap<String, String> map = tokenizeImapId(id);
assertEquals(getContext().getPackageName(), map.get("name"));
assertEquals("android", map.get("os"));
@ -242,7 +242,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// Invoke
String id = ImapStore.getImapId(getContext(), "user-name", "host-name",
ImapTestUtils.parseResponse("* CAPABILITY IMAP4rev1 XXX YYY Z"));
ImapTestUtils.parseResponse("* CAPABILITY IMAP4rev1 XXX YYY Z").flatten());
// Check the result
assertEquals("test-value", tokenizeImapId(id).get("test-key"));
@ -292,9 +292,10 @@ public class ImapStoreUnitTests extends AndroidTestCase {
ImapStore store2 = (ImapStore) ImapStore.newInstance("imap://user2:password@server:999",
getContext(), null);
String id1a = ImapStore.getImapId(getContext(), "user1", "host-name", CAPABILITY_RESPONSE);
String id1b = ImapStore.getImapId(getContext(), "user1", "host-name", CAPABILITY_RESPONSE);
String id2 = ImapStore.getImapId(getContext(), "user2", "host-name", CAPABILITY_RESPONSE);
String capabilities = CAPABILITY_RESPONSE.flatten();
String id1a = ImapStore.getImapId(getContext(), "user1", "host-name", capabilities);
String id1b = ImapStore.getImapId(getContext(), "user1", "host-name", capabilities);
String id2 = ImapStore.getImapId(getContext(), "user2", "host-name", capabilities);
String uid1a = tokenizeImapId(id1a).get("AGUID");
String uid1b = tokenizeImapId(id1b).get("AGUID");
@ -382,7 +383,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
/**
* Test small Folder functions that don't really do anything in Imap
*/
public void testSmallFolderFunctions() throws MessagingException {
public void testSmallFolderFunctions() {
// getPermanentFlags() returns { Flag.DELETED, Flag.SEEN, Flag.FLAGGED }
Flag[] flags = mFolder.getPermanentFlags();
assertEquals(3, flags.length);
@ -1096,10 +1097,11 @@ public class ImapStoreUnitTests extends AndroidTestCase {
assertEquals("initial uid", message.getUid());
}
public void testGetPersonalNamespaces() throws Exception {
public void testGetAllFolders() throws Exception {
MockTransport mock = openAndInjectMockTransport();
expectLogin(mock);
expectNoop(mock, true);
mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"",
new String[] {
"* lIST (\\HAsNoChildren) \"/\" \"inbox\"",
@ -1108,7 +1110,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
"* lIST (\\HAsNoChildren) \"/\" \"&ZeVnLIqe-\"", // Japanese folder name
getNextTag(true) + " oK SUCCESS"
});
Folder[] folders = mStore.getPersonalNamespaces();
Folder[] folders = mStore.getAllFolders();
ArrayList<String> list = new ArrayList<String>();
for (Folder f : folders) {
@ -1124,19 +1126,155 @@ public class ImapStoreUnitTests extends AndroidTestCase {
}
public void testEncodeFolderName() {
assertEquals("", ImapStore.encodeFolderName(""));
assertEquals("a", ImapStore.encodeFolderName("a"));
assertEquals("XYZ", ImapStore.encodeFolderName("XYZ"));
assertEquals("&ZeVnLIqe-", ImapStore.encodeFolderName("\u65E5\u672C\u8A9E"));
assertEquals("!&ZeVnLIqe-!", ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!"));
// null prefix
assertEquals("",
ImapStore.encodeFolderName("", null));
assertEquals("a",
ImapStore.encodeFolderName("a", null));
assertEquals("XYZ",
ImapStore.encodeFolderName("XYZ", null));
assertEquals("&ZeVnLIqe-",
ImapStore.encodeFolderName("\u65E5\u672C\u8A9E", null));
assertEquals("!&ZeVnLIqe-!",
ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!", null));
// empty prefix (same as a null prefix)
assertEquals("",
ImapStore.encodeFolderName("", ""));
assertEquals("a",
ImapStore.encodeFolderName("a", ""));
assertEquals("XYZ",
ImapStore.encodeFolderName("XYZ", ""));
assertEquals("&ZeVnLIqe-",
ImapStore.encodeFolderName("\u65E5\u672C\u8A9E", ""));
assertEquals("!&ZeVnLIqe-!",
ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!", ""));
// defined prefix
assertEquals("[Gmail]/",
ImapStore.encodeFolderName("", "[Gmail]/"));
assertEquals("[Gmail]/a",
ImapStore.encodeFolderName("a", "[Gmail]/"));
assertEquals("[Gmail]/XYZ",
ImapStore.encodeFolderName("XYZ", "[Gmail]/"));
assertEquals("[Gmail]/&ZeVnLIqe-",
ImapStore.encodeFolderName("\u65E5\u672C\u8A9E", "[Gmail]/"));
assertEquals("[Gmail]/!&ZeVnLIqe-!",
ImapStore.encodeFolderName("!\u65E5\u672C\u8A9E!", "[Gmail]/"));
// Add prefix to special mailbox "INBOX" [case insensitive), no affect
assertEquals("INBOX",
ImapStore.encodeFolderName("INBOX", "[Gmail]/"));
assertEquals("inbox",
ImapStore.encodeFolderName("inbox", "[Gmail]/"));
assertEquals("InBoX",
ImapStore.encodeFolderName("InBoX", "[Gmail]/"));
}
public void testDecodeFolderName() {
assertEquals("", ImapStore.decodeFolderName(""));
assertEquals("a", ImapStore.decodeFolderName("a"));
assertEquals("XYZ", ImapStore.decodeFolderName("XYZ"));
assertEquals("\u65E5\u672C\u8A9E", ImapStore.decodeFolderName("&ZeVnLIqe-"));
assertEquals("!\u65E5\u672C\u8A9E!", ImapStore.decodeFolderName("!&ZeVnLIqe-!"));
// null prefix
assertEquals("",
ImapStore.decodeFolderName("", null));
assertEquals("a",
ImapStore.decodeFolderName("a", null));
assertEquals("XYZ",
ImapStore.decodeFolderName("XYZ", null));
assertEquals("\u65E5\u672C\u8A9E",
ImapStore.decodeFolderName("&ZeVnLIqe-", null));
assertEquals("!\u65E5\u672C\u8A9E!",
ImapStore.decodeFolderName("!&ZeVnLIqe-!", null));
// empty prefix (same as a null prefix)
assertEquals("",
ImapStore.decodeFolderName("", ""));
assertEquals("a",
ImapStore.decodeFolderName("a", ""));
assertEquals("XYZ",
ImapStore.decodeFolderName("XYZ", ""));
assertEquals("\u65E5\u672C\u8A9E",
ImapStore.decodeFolderName("&ZeVnLIqe-", ""));
assertEquals("!\u65E5\u672C\u8A9E!",
ImapStore.decodeFolderName("!&ZeVnLIqe-!", ""));
// defined prefix; prefix found, prefix removed
assertEquals("",
ImapStore.decodeFolderName("[Gmail]/", "[Gmail]/"));
assertEquals("a",
ImapStore.decodeFolderName("[Gmail]/a", "[Gmail]/"));
assertEquals("XYZ",
ImapStore.decodeFolderName("[Gmail]/XYZ", "[Gmail]/"));
assertEquals("\u65E5\u672C\u8A9E",
ImapStore.decodeFolderName("[Gmail]/&ZeVnLIqe-", "[Gmail]/"));
assertEquals("!\u65E5\u672C\u8A9E!",
ImapStore.decodeFolderName("[Gmail]/!&ZeVnLIqe-!", "[Gmail]/"));
// defined prefix; prefix not found, no affect
assertEquals("INBOX/",
ImapStore.decodeFolderName("INBOX/", "[Gmail]/"));
assertEquals("INBOX/a",
ImapStore.decodeFolderName("INBOX/a", "[Gmail]/"));
assertEquals("INBOX/XYZ",
ImapStore.decodeFolderName("INBOX/XYZ", "[Gmail]/"));
assertEquals("INBOX/\u65E5\u672C\u8A9E",
ImapStore.decodeFolderName("INBOX/&ZeVnLIqe-", "[Gmail]/"));
assertEquals("INBOX/!\u65E5\u672C\u8A9E!",
ImapStore.decodeFolderName("INBOX/!&ZeVnLIqe-!", "[Gmail]/"));
}
public void testEnsurePrefixIsValid() {
// Test mPathSeparator == null
mStore.mPathSeparator = null;
mStore.mPathPrefix = null;
mStore.ensurePrefixIsValid();
assertNull(mStore.mPathPrefix);
mStore.mPathPrefix = "";
mStore.ensurePrefixIsValid();
assertEquals("", mStore.mPathPrefix);
mStore.mPathPrefix = "foo";
mStore.ensurePrefixIsValid();
assertEquals("foo", mStore.mPathPrefix);
mStore.mPathPrefix = "foo.";
mStore.ensurePrefixIsValid();
assertEquals("foo.", mStore.mPathPrefix);
// Test mPathSeparator == ""
mStore.mPathSeparator = "";
mStore.mPathPrefix = null;
mStore.ensurePrefixIsValid();
assertNull(mStore.mPathPrefix);
mStore.mPathPrefix = "";
mStore.ensurePrefixIsValid();
assertEquals("", mStore.mPathPrefix);
mStore.mPathPrefix = "foo";
mStore.ensurePrefixIsValid();
assertEquals("foo", mStore.mPathPrefix);
mStore.mPathPrefix = "foo.";
mStore.ensurePrefixIsValid();
assertEquals("foo.", mStore.mPathPrefix);
// Test mPathSeparator is non-empty
mStore.mPathSeparator = ".";
mStore.mPathPrefix = null;
mStore.ensurePrefixIsValid();
assertNull(mStore.mPathPrefix);
mStore.mPathPrefix = "";
mStore.ensurePrefixIsValid();
assertEquals("", mStore.mPathPrefix);
mStore.mPathPrefix = "foo";
mStore.ensurePrefixIsValid();
assertEquals("foo.", mStore.mPathPrefix);
// Trailing separator; path separator NOT appended
mStore.mPathPrefix = "foo.";
mStore.ensurePrefixIsValid();
assertEquals("foo.", mStore.mPathPrefix);
// Trailing punctuation has no affect; path separator still appended
mStore.mPathPrefix = "foo/";
mStore.ensurePrefixIsValid();
assertEquals("foo/.", mStore.mPathPrefix);
}
public void testOpen() throws Exception {
@ -1641,13 +1779,14 @@ public class ImapStoreUnitTests extends AndroidTestCase {
expectLogin(mock);
// List folders.
expectNoop(mock, true);
mock.expect(getNextTag(false) + " LIST \"\" \"\\*\"",
new String[] {
"* LIST () \"/\" \"" + FOLDER_1 + "\"",
"* LIST () \"/\" \"" + FOLDER_2 + "\"",
getNextTag(true) + " OK SUCCESS"
});
final Folder[] folders = mStore.getPersonalNamespaces();
"* LIST () \"/\" \"" + FOLDER_1 + "\"",
"* LIST () \"/\" \"" + FOLDER_2 + "\"",
getNextTag(true) + " OK SUCCESS"
});
final Folder[] folders = mStore.getAllFolders();
ArrayList<String> list = new ArrayList<String>();
for (Folder f : folders) {

View File

@ -241,7 +241,7 @@ public class Pop3StoreUnitTests extends AndroidTestCase {
public void testStoreFoldersFunctions() throws MessagingException {
// getPersonalNamespaces() always returns INBOX folder
Folder[] folders = mStore.getPersonalNamespaces();
Folder[] folders = mStore.getAllFolders();
assertEquals(1, folders.length);
assertSame(mFolder, folders[0]);
@ -319,7 +319,7 @@ public class Pop3StoreUnitTests extends AndroidTestCase {
* Lightweight test to confirm that POP3 hasn't implemented any folder roles yet.
*/
public void testNoFolderRolesYet() throws MessagingException {
Folder[] remoteFolders = mStore.getPersonalNamespaces();
Folder[] remoteFolders = mStore.getAllFolders();
for (Folder folder : remoteFolders) {
assertEquals(Folder.FolderRole.UNKNOWN, folder.getRole());
}