IMAP ID
* 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:
parent
57bdd9e580
commit
468371917e
@ -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
|
||||||
|
@ -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>();
|
||||||
|
Loading…
Reference in New Issue
Block a user