Send CAPABILITY to all IMAP servers

* Send CAPABILITY to all servers, not just when we check TLS
* Feed capabilities into IMAP ID generation
* Unit tests updated

Bug: 2332183
This commit is contained in:
Andrew Stadler 2010-02-25 22:48:11 -08:00
parent 1270218f32
commit cb95fbe135
3 changed files with 84 additions and 40 deletions

View File

@ -92,6 +92,7 @@ public class ImapStore extends Store {
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.SEEN, Flag.FLAGGED };
private Context mContext;
private Transport mRootTransport;
private String mUsername;
private String mPassword;
@ -135,6 +136,7 @@ public class ImapStore extends Store {
* @param uriString the Uri containing information to configure this store
*/
private ImapStore(Context context, String uriString) throws MessagingException {
mContext = context;
URI uri;
try {
uri = new URI(uriString);
@ -179,15 +181,6 @@ public class ImapStore extends Store {
}
mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501");
// Assign user-agent string (for RFC2971 ID command)
String mUserAgent = getImapId(context, mUsername, mRootTransport.getHost());
if (mUserAgent != null) {
mIdPhrase = "ID (" + mUserAgent + ")";
} else if (DEBUG_FORCE_SEND_ID) {
mIdPhrase = "ID NIL";
}
// else: mIdPhrase = null, no ID will be emitted
}
/**
@ -221,9 +214,10 @@ public class ImapStore extends Store {
*
* @param userName the username of the account
* @param host the host (server) of the account
* @param capability the capabilities string from the server
* @return a String for use in an IMAP ID message.
*/
public String getImapId(Context context, String userName, String host) {
public String getImapId(Context context, String userName, String host, String capability) {
// The first section is global to all IMAP connections, and generates the fixed
// values in any IMAP ID message
synchronized (ImapStore.class) {
@ -245,7 +239,7 @@ public class ImapStore extends Store {
// Optionally add any vendor-supplied id keys
String vendorId =
VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, null);
VendorPolicyLoader.getInstance(context).getImapIdValues(userName, host, capability);
if (vendorId != null) {
id.append(' ');
id.append(vendorId);
@ -1321,13 +1315,15 @@ public class ImapStore extends Store {
// BANNER
mParser.readResponse();
// CAPABILITY
List<ImapResponse> response = executeSimpleCommand("CAPABILITY");
if (response.size() != 2) {
throw new MessagingException("Invalid CAPABILITY response received");
}
String capabilities = response.get(0).toString();
if (mTransport.canTryTlsSecurity()) {
// CAPABILITY
List<ImapResponse> responses = executeSimpleCommand("CAPABILITY");
if (responses.size() != 2) {
throw new MessagingException("Invalid CAPABILITY response received");
}
if (responses.get(0).contains("STARTTLS")) {
if (capabilities.contains("STARTTLS")) {
// STARTTLS
executeSimpleCommand("STARTTLS");
@ -1342,6 +1338,16 @@ public class ImapStore extends Store {
}
}
// Assign user-agent string (for RFC2971 ID command)
String mUserAgent = getImapId(mContext, mUsername, mRootTransport.getHost(),
capabilities);
if (mUserAgent != null) {
mIdPhrase = "ID (" + mUserAgent + ")";
} else if (DEBUG_FORCE_SEND_ID) {
mIdPhrase = "ID NIL";
}
// else: mIdPhrase = null, no ID will be emitted
// Send user-agent in an RFC2971 ID command
if (mIdPhrase != null) {
try {

View File

@ -51,6 +51,8 @@ public class ImapStoreUnitTests extends AndroidTestCase {
/* These values are provided by setUp() */
private ImapStore mStore = null;
private ImapStore.ImapFolder mFolder = null;
private int mNextTag;
/**
* Setup code. We generate a lightweight ImapStore and ImapStore.ImapFolder.
@ -105,7 +107,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// x-android-device-model Model (Optional, so not tested here)
// x-android-net-operator Carrier (Unreliable, so not tested here)
// AGUID A device+account UID
String id = mStore.getImapId(getContext(), "user-name", "host-name");
String id = mStore.getImapId(getContext(), "user-name", "host-name", "IMAP4rev1 STARTTLS");
HashMap<String, String> map = tokenizeImapId(id);
assertEquals(getContext().getPackageName(), map.get("name"));
assertEquals("android", map.get("os"));
@ -175,9 +177,9 @@ public class ImapStoreUnitTests extends AndroidTestCase {
ImapStore store2 = (ImapStore) ImapStore.newInstance("imap://user2:password@server:999",
getContext(), null);
String id1a = mStore.getImapId(getContext(), "user1", "host-name");
String id1b = mStore.getImapId(getContext(), "user1", "host-name");
String id2 = mStore.getImapId(getContext(), "user2", "host-name");
String id1a = mStore.getImapId(getContext(), "user1", "host-name", "IMAP4rev1");
String id1b = mStore.getImapId(getContext(), "user1", "host-name", "IMAP4rev1");
String id2 = mStore.getImapId(getContext(), "user2", "host-name", "IMAP4rev1");
String uid1a = tokenizeImapId(id1a).get("AGUID");
String uid1b = tokenizeImapId(id1b).get("AGUID");
@ -220,7 +222,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
"* ID (\"name\" \"Cyrus\" \"version\" \"1.5\"" +
" \"os\" \"sunos\" \"os-version\" \"5.5\"" +
" \"support-url\" \"mailto:cyrus-bugs+@andrew.cmu.edu\")",
"1 OK"});
"OK"});
mFolder.open(OpenMode.READ_WRITE, null);
}
@ -233,7 +235,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// try to open it
setupOpenFolder(mockTransport, new String[] {
"* ID NIL",
"1 OK [ID] bad-char-%"});
"OK [ID] bad-char-%"});
mFolder.open(OpenMode.READ_WRITE, null);
}
@ -245,7 +247,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// try to open it
setupOpenFolder(mockTransport, new String[] {
"1 BAD unknown command bad-char-%"});
"BAD unknown command bad-char-%"});
mFolder.open(OpenMode.READ_WRITE, null);
}
@ -342,6 +344,7 @@ public class ImapStoreUnitTests extends AndroidTestCase {
// Create mock transport and inject it into the ImapStore that's already set up
MockTransport mockTransport = new MockTransport();
mockTransport.setSecurity(Transport.CONNECTION_SECURITY_NONE, false);
mockTransport.setMockHost("mock.server.com");
mStore.setTransport(mockTransport);
return mockTransport;
}
@ -353,28 +356,55 @@ public class ImapStoreUnitTests extends AndroidTestCase {
*/
private void setupOpenFolder(MockTransport mockTransport) {
setupOpenFolder(mockTransport, new String[] {
"* ID NIL", "1 OK"});
"* ID NIL", "OK"});
}
/**
* Helper which stuffs the mock with enough strings to satisfy a call to ImapFolder.open()
* Also allows setting a custom IMAP ID.
*
* Also sets mNextTag, an int, which is useful if there are additional commands to inject.
*
* @param mockTransport the mock transport we're using
* @param imapIdResponse the expected series of responses to the IMAP ID command. Non-final
* lines should be tagged with *. The final response should be untagged (the correct
* tag will be added at runtime).
* @return the next tag# to use
*/
private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse) {
// Fix the tag # of the ID response
String last = imapIdResponse[imapIdResponse.length-1];
last = "2 " + last;
imapIdResponse[imapIdResponse.length-1] = last;
// inject boilerplate commands that match our typical login
mockTransport.expect(null, "* OK Imap 2000 Ready To Assist You");
mockTransport.expect("1 ID \\(.*\\)", imapIdResponse);
mockTransport.expect("2 LOGIN user \"password\"",
"2 OK user authenticated (Success)");
mockTransport.expect("3 SELECT \"INBOX\"", new String[] {
mockTransport.expect("1 CAPABILITY", new String[] {
"* CAPABILITY IMAP4rev1 STARTTLS AUTH=GSSAPI LOGINDISABLED",
"1 OK CAPABILITY completed"});
mockTransport.expect("2 ID \\(.*\\)", imapIdResponse);
mockTransport.expect("3 LOGIN user \"password\"",
"3 OK user authenticated (Success)");
mockTransport.expect("4 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]",
"3 OK [READ-WRITE] INBOX selected. (Success)"});
"4 OK [READ-WRITE] INBOX selected. (Success)"});
mNextTag = 5;
}
/**
* Return a tag for use in setting up expect strings. Typically this is called in pairs,
* first as getNextTag(false) when emitting the command, then as getNextTag(true) when
* emitting the final line of the expected response.
* @param advance true to increment mNextTag for the subsequence command
* @return a string containing the current tag
*/
public String getNextTag(boolean advance) {
if (advance) ++mNextTag;
return Integer.toString(mNextTag);
}
/**
@ -383,9 +413,9 @@ public class ImapStoreUnitTests extends AndroidTestCase {
public void testGetUnreadMessageCountWithQuotedString() throws Exception {
MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock);
mock.expect("4 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
mock.expect(getNextTag(false) + " STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
"* STATUS \"INBOX\" (UNSEEN 2)",
"4 OK STATUS completed"});
getNextTag(true) + " OK STATUS completed"});
mFolder.open(OpenMode.READ_WRITE, null);
int unreadCount = mFolder.getUnreadMessageCount();
assertEquals("getUnreadMessageCount with quoted string", 2, unreadCount);
@ -397,10 +427,10 @@ public class ImapStoreUnitTests extends AndroidTestCase {
public void testGetUnreadMessageCountWithLiteralString() throws Exception {
MockTransport mock = openAndInjectMockTransport();
setupOpenFolder(mock);
mock.expect("4 STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
mock.expect(getNextTag(false) + " STATUS \"INBOX\" \\(UNSEEN\\)", new String[] {
"* STATUS {5}",
"INBOX (UNSEEN 10)",
"4 OK STATUS completed"});
getNextTag(true) + " OK STATUS completed"});
mFolder.open(OpenMode.READ_WRITE, null);
int unreadCount = mFolder.getUnreadMessageCount();
assertEquals("getUnreadMessageCount with literal string", 10, unreadCount);
@ -419,9 +449,9 @@ public class ImapStoreUnitTests extends AndroidTestCase {
FetchProfile fp = new FetchProfile();fp.clear();
fp.add(FetchProfile.Item.STRUCTURE);
Message message1 = mFolder.createMessage("1");
mock.expect("4 UID FETCH 1 \\(UID BODYSTRUCTURE\\)", new String[] {
mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", new String[] {
"* 1 FETCH (UID 1 BODYSTRUCTURE (TEXT PLAIN NIL NIL NIL 7BIT 0 0 NIL NIL NIL))",
"4 OK SUCCESS"
getNextTag(true) + " OK SUCCESS"
});
mFolder.fetch(new Message[] { message1 }, fp, null);
@ -432,9 +462,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("5 UID FETCH 1 \\(UID BODY.PEEK\\[TEXT\\]\\)", new String[] {
mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[TEXT\\]\\)", new String[] {
"* 1 FETCH (UID 1 BODY[TEXT] NIL)",
"5 OK SUCCESS"
getNextTag(true) + " OK SUCCESS"
});
ArrayList<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();

View File

@ -45,6 +45,7 @@ public class MockTransport implements Transport {
private boolean mInputOpen;
private int mConnectionSecurity;
private boolean mTrustCertificates;
private String mHost;
private ArrayList<String> mQueuedInput = new ArrayList<String>();
@ -162,9 +163,16 @@ public class MockTransport implements Transport {
mPairs.clear();
}
/**
* This is a test function (not part of the interface) and is used to set up a result
* value for getHost(), if needed for the test.
*/
public void setMockHost(String host) {
mHost = host;
}
public String getHost() {
SmtpSenderUnitTests.fail("getHost() not implemented");
return null;
return mHost;
}
public InputStream getInputStream() {