* Add IMAP ID command to all login sequences
* Send generic information for now
* Explicitly catch & discard parsing errors, since we really don't
  care if the command succeeds or not.
* Unit tests

Bug: 2332183
This commit is contained in:
Andrew Stadler 2010-01-25 18:40:45 -08:00
parent 57bdd9e580
commit 468371917e
2 changed files with 213 additions and 23 deletions

View File

@ -42,6 +42,7 @@ import com.android.email.mail.transport.MailTransport;
import com.beetstra.jutf7.CharsetProvider; import com.beetstra.jutf7.CharsetProvider;
import android.content.Context; import android.content.Context;
import android.os.Build;
import android.util.Config; import android.util.Config;
import android.util.Log; import android.util.Log;
@ -79,6 +80,9 @@ import javax.net.ssl.SSLException;
*/ */
public class ImapStore extends Store { public class ImapStore extends Store {
// Always check in FALSE
private static final boolean DEBUG_FORCE_SEND_ID = false;
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED }; private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED };
private Transport mRootTransport; private Transport mRootTransport;
@ -86,6 +90,8 @@ public class ImapStore extends Store {
private String mPassword; private String mPassword;
private String mLoginPhrase; private String mLoginPhrase;
private String mPathPrefix; private String mPathPrefix;
private String mIdPhrase = null;
private static String sImapId = null;
private LinkedList<ImapConnection> mConnections = private LinkedList<ImapConnection> mConnections =
new LinkedList<ImapConnection>(); new LinkedList<ImapConnection>();
@ -108,7 +114,7 @@ public class ImapStore extends Store {
*/ */
public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks) public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks)
throws MessagingException { throws MessagingException {
return new ImapStore(uri); return new ImapStore(context, uri);
} }
/** /**
@ -121,7 +127,7 @@ public class ImapStore extends Store {
* *
* @param uriString the Uri containing information to configure this store * @param uriString the Uri containing information to configure this store
*/ */
private ImapStore(String uriString) throws MessagingException { private ImapStore(Context context, String uriString) throws MessagingException {
URI uri; URI uri;
try { try {
uri = new URI(uriString); uri = new URI(uriString);
@ -166,6 +172,15 @@ public class ImapStore extends Store {
} }
mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501"); mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501");
// Assign user-agent string (for RFC2971 ID command)
String mUserAgent = getImapId(context);
if (mUserAgent != null) {
mIdPhrase = "ID (" + mUserAgent + ")";
} else if (DEBUG_FORCE_SEND_ID) {
mIdPhrase = "ID NIL";
}
// else: mIdPhrase = null, no ID will be emitted
} }
/** /**
@ -178,6 +193,73 @@ public class ImapStore extends Store {
mRootTransport = testTransport; mRootTransport = testTransport;
} }
/**
* Return, or create and return, an string suitable for use in an IMAP ID message.
* This is constructed similarly to the way the browser sets up its user-agent strings.
* See RFC 2971 for more details. The output of this command will be a series of key-value
* pairs delimited by spaces (there is no point in returning a structured result because
* this will be sent as-is to the IMAP server). No tokens, parenthesis or "ID" are included,
* because some connections may append additional values.
*
* The following IMAP ID keys may be included:
* name Android package name of the program
* os "android"
* os-version "version; model; build-id"
* vendor Vendor of the client/server
*
* @return a String for use in an IMAP ID message.
*/
public String getImapId(Context context) {
synchronized (Email.class) {
if (sImapId == null) {
// "name" "com.android.email"
StringBuffer sb = new StringBuffer("\"name\" \"");
sb.append(context.getPackageName());
sb.append("\"");
// "os" "android"
sb.append(" \"os\" \"android\"");
// "os-version" "version; model; build-id"
sb.append(" \"os-version\" \"");
final String version = Build.VERSION.RELEASE;
if (version.length() > 0) {
sb.append(version);
} else {
// default to "1.0"
sb.append("1.0");
}
// add the model (on release builds only)
if ("REL".equals(Build.VERSION.CODENAME)) {
final String model = Build.MODEL;
if (model.length() > 0) {
sb.append("; ");
sb.append(model);
}
}
// add the build ID or build #
final String id = Build.ID;
if (id.length() > 0) {
sb.append("; ");
sb.append(id);
}
sb.append("\"");
// "vendor" "the vendor"
final String vendor = Build.MANUFACTURER;
if (vendor.length() > 0) {
sb.append(" \"vendor\" \"");
sb.append(vendor);
sb.append("\"");
}
sImapId = sb.toString();
}
}
return sImapId;
}
@Override @Override
public Folder getFolder(String name) throws MessagingException { public Folder getFolder(String name) throws MessagingException {
ImapFolder folder; ImapFolder folder;
@ -191,7 +273,6 @@ public class ImapStore extends Store {
return folder; return folder;
} }
@Override @Override
public Folder[] getPersonalNamespaces() throws MessagingException { public Folder[] getPersonalNamespaces() throws MessagingException {
ImapConnection connection = getConnection(); ImapConnection connection = getConnection();
@ -1171,6 +1252,22 @@ public class ImapStore extends Store {
} }
} }
// 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 (Config.LOGD && 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 for details
}
}
try { try {
// TODO eventually we need to add additional authentication // TODO eventually we need to add additional authentication
// options such as SASL // options such as SASL

View File

@ -16,6 +16,7 @@
package com.android.email.mail.store; package com.android.email.mail.store;
import com.android.email.Email;
import com.android.email.mail.FetchProfile; import com.android.email.mail.FetchProfile;
import com.android.email.mail.Flag; import com.android.email.mail.Flag;
import com.android.email.mail.Folder; import com.android.email.mail.Folder;
@ -31,9 +32,11 @@ import com.android.email.mail.transport.MockTransport;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest; import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashMap;
import java.util.Locale; import java.util.Locale;
/** /**
@ -80,6 +83,92 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// TODO: inject specific facts in the initial folder SELECT and check them here // TODO: inject specific facts in the initial folder SELECT and check them here
} }
/**
* TODO: Test with SSL negotiation (faked)
* TODO: Test with SSL required but not supported
* TODO: Test with TLS negotiation (faked)
* TODO: Test with TLS required but not supported
* TODO: Test calling getMessageCount(), getMessages(), etc.
*/
/**
* Test the generation of the IMAP ID keys
*
* Since this is build-specific, we mostly just ensure that the correct strings
* are being generated, and non-empty, and (if possible) look for expected formatting.
*/
public void testImapId() {
String id = mStore.getImapId(getContext());
// Instead of a true tokenizer, we'll use double-quote as the split.
// We can's use " " because there may be spaces inside the values.
String[] elements = id.split("\"");
HashMap<String, String> map = new HashMap<String, String>();
for (int i = 0; i < elements.length; ) {
// Because we split at quotes, we expect to find:
// [i] = null
// [i+1] = key
// [i+2] = one or more spaces
// [i+3] = value
map.put(elements[i+1], elements[i+3]);
i += 4;
}
// Strings we'll expect to find:
// name Android package name of the program
// os "android"
// os-version "version; model; build-id"
// vendor Vendor of the client/server
String name = map.get("name");
assertEquals(getContext().getPackageName(), name);
String os = map.get("os");
assertEquals("android", os);
String osversion = map.get("os-version");
assertNotNull(osversion);
String vendor = map.get("vendor");
assertNotNull(vendor);
}
/**
* Test non-NIL server response to IMAP ID. We should simply ignore it.
*/
public void testServerId() throws MessagingException {
MockTransport mockTransport = openAndInjectMockTransport();
// try to open it
setupOpenFolder(mockTransport, new String[] {
"* ID (\"name\" \"Cyrus\" \"version\" \"1.5\"" +
" \"os\" \"sunos\" \"os-version\" \"5.5\"" +
" \"support-url\" \"mailto:cyrus-bugs+@andrew.cmu.edu\")",
"1 OK"});
mFolder.open(OpenMode.READ_WRITE, null);
}
/**
* Test OK response to IMAP ID with crummy text afterwards too.
*/
public void testImapIdOkParsing() throws MessagingException {
MockTransport mockTransport = openAndInjectMockTransport();
// try to open it
setupOpenFolder(mockTransport, new String[] {
"* ID NIL",
"1 OK [ID] bad-char-%"});
mFolder.open(OpenMode.READ_WRITE, null);
}
/**
* Test BAD response to IMAP ID - also with bad parser chars
*/
public void testImapIdBad() throws MessagingException {
MockTransport mockTransport = openAndInjectMockTransport();
// try to open it
setupOpenFolder(mockTransport, new String[] {
"1 BAD unknown command bad-char-%"});
mFolder.open(OpenMode.READ_WRITE, null);
}
/** /**
* Confirms that ImapList object correctly returns an appropriate Date object * Confirms that ImapList object correctly returns an appropriate Date object
* without throwning MessagingException when getKeyedDate() is called. * without throwning MessagingException when getKeyedDate() is called.
@ -112,14 +201,6 @@ public class ImapStoreUnitTests extends AndroidTestCase {
assertEquals(1230800400000L, result.getTime()); assertEquals(1230800400000L, result.getTime());
} }
/**
* TODO: Test with SSL negotiation (faked)
* TODO: Test with SSL required but not supported
* TODO: Test with TLS negotiation (faked)
* TODO: Test with TLS required but not supported
* TODO: Test calling getMessageCount(), getMessages(), etc.
*/
/** /**
* TODO: Test the operation of checkSettings() * TODO: Test the operation of checkSettings()
* TODO: Test small Store & Folder functions that manage folders & namespace * TODO: Test small Store & Folder functions that manage folders & namespace
@ -191,17 +272,29 @@ public class ImapStoreUnitTests extends AndroidTestCase {
* @param mockTransport the mock transport we're using * @param mockTransport the mock transport we're using
*/ */
private void setupOpenFolder(MockTransport mockTransport) { private void setupOpenFolder(MockTransport mockTransport) {
setupOpenFolder(mockTransport, new String[] {
"* ID NIL", "1 OK"});
}
/**
* Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open()
* Also allows setting a custom IMAP ID.
*
* @param mockTransport the mock transport we're using
*/
private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse) {
mockTransport.expect(null, "* OK Imap 2000 Ready To Assist You"); mockTransport.expect(null, "* OK Imap 2000 Ready To Assist You");
mockTransport.expect("1 LOGIN user \"password\"", mockTransport.expect("1 ID \\(.*\\)", imapIdResponse);
"1 OK user authenticated (Success)"); mockTransport.expect("2 LOGIN user \"password\"",
mockTransport.expect("2 SELECT \"INBOX\"", new String[] { "2 OK user authenticated (Success)");
mockTransport.expect("3 SELECT \"INBOX\"", new String[] {
"* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)", "* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
"* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]", "* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]",
"* 0 EXISTS", "* 0 EXISTS",
"* 0 RECENT", "* 0 RECENT",
"* OK [UNSEEN 0]", "* OK [UNSEEN 0]",
"* OK [UIDNEXT 1]", "* OK [UIDNEXT 1]",
"2 OK [READ-WRITE] INBOX selected. (Success)"}); "3 OK [READ-WRITE] INBOX selected. (Success)"});
} }
/** /**
@ -210,9 +303,9 @@ public class ImapStoreUnitTests extends AndroidTestCase {
public void testGetUnreadMessageCountWithQuotedString() throws Exception { public void testGetUnreadMessageCountWithQuotedString() throws Exception {
MockTransport mock = openAndInjectMockTransport(); MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock); setupOpenFolder(mock);
mock.expect("3 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] { mock.expect("4 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
"* STATUS \"INBOX\" (UNSEEN 2)", "* STATUS \"INBOX\" (UNSEEN 2)",
"3 OK STATUS completed"}); "4 OK STATUS completed"});
mFolder.open(OpenMode.READ_WRITE, null); mFolder.open(OpenMode.READ_WRITE, null);
int unreadCount = mFolder.getUnreadMessageCount(); int unreadCount = mFolder.getUnreadMessageCount();
assertEquals("getUnreadMessageCount with quoted string", 2, unreadCount); assertEquals("getUnreadMessageCount with quoted string", 2, unreadCount);
@ -224,10 +317,10 @@ public class ImapStoreUnitTests extends AndroidTestCase {
public void testGetUnreadMessageCountWithLiteralString() throws Exception { public void testGetUnreadMessageCountWithLiteralString() throws Exception {
MockTransport mock = openAndInjectMockTransport(); MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock); setupOpenFolder(mock);
mock.expect("3 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] { mock.expect("4 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
"* STATUS {5}", "* STATUS {5}",
"INBOX (UNSEEN 10)", "INBOX (UNSEEN 10)",
"3 OK STATUS completed"}); "4 OK STATUS completed"});
mFolder.open(OpenMode.READ_WRITE, null); mFolder.open(OpenMode.READ_WRITE, null);
int unreadCount = mFolder.getUnreadMessageCount(); int unreadCount = mFolder.getUnreadMessageCount();
assertEquals("getUnreadMessageCount with literal string", 10, unreadCount); assertEquals("getUnreadMessageCount with literal string", 10, unreadCount);
@ -246,9 +339,9 @@ public class ImapStoreUnitTests extends AndroidTestCase {
FetchProfile fp = new FetchProfile();fp.clear(); FetchProfile fp = new FetchProfile();fp.clear();
fp.add(FetchProfile.Item.STRUCTURE); fp.add(FetchProfile.Item.STRUCTURE);
Message message1 = mFolder.createMessage("1"); Message message1 = mFolder.createMessage("1");
mock.expect("3 UID FETCH 1 \\(UID BODYSTRUCTURE\\)", new String[] { mock.expect("4 UID FETCH 1 \\(UID BODYSTRUCTURE\\)", new String[] {
"* 1 FETCH (UID 1 BODYSTRUCTURE (TEXT PLAIN NIL NIL NIL 7BIT 0 0 NIL NIL NIL))", "* 1 FETCH (UID 1 BODYSTRUCTURE (TEXT PLAIN NIL NIL NIL 7BIT 0 0 NIL NIL NIL))",
"3 OK SUCCESS" "4 OK SUCCESS"
}); });
mFolder.fetch(new Message[] { message1 }, fp, null); mFolder.fetch(new Message[] { message1 }, fp, null);
@ -259,9 +352,9 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// Because this breaks our little parser, fetch() skips over empty parts. // Because this breaks our little parser, fetch() skips over empty parts.
// The rest of this test is confirming that this is the case. // The rest of this test is confirming that this is the case.
mock.expect("4 UID FETCH 1 \\(UID BODY.PEEK\\[TEXT\\]\\)", new String[] { mock.expect("5 UID FETCH 1 \\(UID BODY.PEEK\\[TEXT\\]\\)", new String[] {
"* 1 FETCH (UID 1 BODY[TEXT] NIL)", "* 1 FETCH (UID 1 BODY[TEXT] NIL)",
"4 OK SUCCESS" "5 OK SUCCESS"
}); });
ArrayList<Part> viewables = new ArrayList<Part>(); ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>(); ArrayList<Part> attachments = new ArrayList<Part>();