* 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 android.content.Context;
import android.os.Build;
import android.util.Config;
import android.util.Log;
@ -79,6 +80,9 @@ import javax.net.ssl.SSLException;
*/
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 Transport mRootTransport;
@ -86,6 +90,8 @@ public class ImapStore extends Store {
private String mPassword;
private String mLoginPhrase;
private String mPathPrefix;
private String mIdPhrase = null;
private static String sImapId = null;
private LinkedList<ImapConnection> mConnections =
new LinkedList<ImapConnection>();
@ -108,7 +114,7 @@ public class ImapStore extends Store {
*/
public static Store newInstance(String uri, Context context, PersistentDataCallbacks callbacks)
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
*/
private ImapStore(String uriString) throws MessagingException {
private ImapStore(Context context, String uriString) throws MessagingException {
URI uri;
try {
uri = new URI(uriString);
@ -166,6 +172,15 @@ public class ImapStore extends Store {
}
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;
}
/**
* 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
public Folder getFolder(String name) throws MessagingException {
ImapFolder folder;
@ -191,7 +273,6 @@ public class ImapStore extends Store {
return folder;
}
@Override
public Folder[] getPersonalNamespaces() throws MessagingException {
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 {
// TODO eventually we need to add additional authentication
// options such as SASL

View File

@ -16,6 +16,7 @@
package com.android.email.mail.store;
import com.android.email.Email;
import com.android.email.mail.FetchProfile;
import com.android.email.mail.Flag;
import com.android.email.mail.Folder;
@ -31,9 +32,11 @@ import com.android.email.mail.transport.MockTransport;
import android.test.AndroidTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import android.util.Log;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
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: 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
* without throwning MessagingException when getKeyedDate() is called.
@ -112,14 +201,6 @@ public class ImapStoreUnitTests extends AndroidTestCase {
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 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
*/
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("1 LOGIN user \"password\"",
"1 OK user authenticated (Success)");
mockTransport.expect("2 SELECT \"INBOX\"", new String[] {
mockTransport.expect("1 ID \\(.*\\)", imapIdResponse);
mockTransport.expect("2 LOGIN user \"password\"",
"2 OK user authenticated (Success)");
mockTransport.expect("3 SELECT \"INBOX\"", new String[] {
"* FLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen)",
"* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Draft \\Deleted \\Seen \\*)]",
"* 0 EXISTS",
"* 0 RECENT",
"* OK [UNSEEN 0]",
"* 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 {
MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock);
mock.expect("3 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
mock.expect("4 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
"* STATUS \"INBOX\" (UNSEEN 2)",
"3 OK STATUS completed"});
"4 OK STATUS completed"});
mFolder.open(OpenMode.READ_WRITE, null);
int unreadCount = mFolder.getUnreadMessageCount();
assertEquals("getUnreadMessageCount with quoted string", 2, unreadCount);
@ -224,10 +317,10 @@ public class ImapStoreUnitTests extends AndroidTestCase {
public void testGetUnreadMessageCountWithLiteralString() throws Exception {
MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock);
mock.expect("3 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
mock.expect("4 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
"* STATUS {5}",
"INBOX (UNSEEN 10)",
"3 OK STATUS completed"});
"4 OK STATUS completed"});
mFolder.open(OpenMode.READ_WRITE, null);
int unreadCount = mFolder.getUnreadMessageCount();
assertEquals("getUnreadMessageCount with literal string", 10, unreadCount);
@ -246,9 +339,9 @@ public class ImapStoreUnitTests extends AndroidTestCase {
FetchProfile fp = new FetchProfile();fp.clear();
fp.add(FetchProfile.Item.STRUCTURE);
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))",
"3 OK SUCCESS"
"4 OK SUCCESS"
});
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.
// 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)",
"4 OK SUCCESS"
"5 OK SUCCESS"
});
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();