Fix NPE when switching IMAP into TLS

* Update MockTransport to allow TLS connections
* Test TLS connection in ImapStore unit tests
* The bugfix: Re-query capabilities after closing/reopening parser for TLS

(Note: Actually, this is required by the IMAP RFC 3501, 6.2.1)

Bug: 3315939
Change-Id: I51f838043e87750b5712a1bd2e4f9c821b58c808
diff --git a/src/com/android/email/mail/store/ImapStore.java b/src/com/android/email/mail/store/ImapStore.java
index c96cd53..fcaa4a0 100644
--- a/src/com/android/email/mail/store/ImapStore.java
+++ b/src/com/android/email/mail/store/ImapStore.java
@@ -232,7 +232,7 @@
      *
      * @param userName the username of the account
      * @param host the host (server) of the account
-     * @param capability the capabilities string from the server
+     * @param capabilityResponse the capabilities list from the server
      * @return a String for use in an IMAP ID message.
      */
     /* package */ static String getImapId(Context context, String userName, String host,
@@ -1407,17 +1407,9 @@
                 mParser.readResponse();
 
                 // CAPABILITY
-                ImapResponse capabilityResponse = null;
-                for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
-                    if (r.is(0, ImapConstants.CAPABILITY)) {
-                        capabilityResponse = r;
-                        break;
-                    }
-                }
-                if (capabilityResponse == null) {
-                    throw new MessagingException("Invalid CAPABILITY response received");
-                }
+                ImapResponse capabilityResponse = queryCapabilities();
 
+                // TLS
                 if (mTransport.canTryTlsSecurity()) {
                     if (capabilityResponse.contains(ImapConstants.STARTTLS)) {
                         // STARTTLS
@@ -1426,6 +1418,8 @@
                         mTransport.reopenTls();
                         mTransport.setSoTimeout(MailTransport.SOCKET_READ_TIMEOUT);
                         createParser();
+                        // Per RFC requirement (3501-6.2.1) gather new capabilities
+                        capabilityResponse = queryCapabilities();
                     } else {
                         if (Config.LOGD && Email.DEBUG) {
                             Log.d(Email.LOG_TAG, "TLS not supported but required");
@@ -1434,6 +1428,7 @@
                     }
                 }
 
+                // ID
                 // Assign user-agent string (for RFC2971 ID command)
                 String mUserAgent = getImapId(mContext, mUsername, mRootTransport.getHost(),
                         capabilityResponse);
@@ -1460,6 +1455,7 @@
                     }
                 }
 
+                // LOGIN
                 try {
                     // TODO eventually we need to add additional authentication
                     // options such as SASL
@@ -1565,6 +1561,23 @@
             return responses;
         }
 
+        /**
+         * Query server for capabilities.
+         */
+        private ImapResponse queryCapabilities() throws IOException, MessagingException {
+            ImapResponse capabilityResponse = null;
+            for (ImapResponse r : executeSimpleCommand(ImapConstants.CAPABILITY)) {
+                if (r.is(0, ImapConstants.CAPABILITY)) {
+                    capabilityResponse = r;
+                    break;
+                }
+            }
+            if (capabilityResponse == null) {
+                throw new MessagingException("Invalid CAPABILITY response received");
+            }
+            return capabilityResponse;
+        }
+
         /** @see ImapResponseParser#logLastDiscourse() */
         public void logLastDiscourse() {
             mDiscourse.logLastDiscourse();
diff --git a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java
index 867e86a..1bbc82f 100644
--- a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java
+++ b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java
@@ -26,13 +26,13 @@
 import com.android.email.mail.FetchProfile;
 import com.android.email.mail.Flag;
 import com.android.email.mail.Folder;
+import com.android.email.mail.Folder.FolderType;
+import com.android.email.mail.Folder.OpenMode;
 import com.android.email.mail.Message;
+import com.android.email.mail.Message.RecipientType;
 import com.android.email.mail.MessagingException;
 import com.android.email.mail.Part;
 import com.android.email.mail.Transport;
-import com.android.email.mail.Folder.FolderType;
-import com.android.email.mail.Folder.OpenMode;
-import com.android.email.mail.Message.RecipientType;
 import com.android.email.mail.internet.MimeBodyPart;
 import com.android.email.mail.internet.MimeMultipart;
 import com.android.email.mail.internet.MimeUtility;
@@ -50,7 +50,6 @@
 import android.test.MoreAsserts;
 import android.test.suitebuilder.annotation.SmallTest;
 
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.regex.Pattern;
@@ -80,7 +79,7 @@
      */
     private final static String FOLDER_ENCODED = "&ZeU-";
 
-    private static ImapResponse CAPABILITY_RESPONSE = ImapTestUtils.parseResponse(
+    private final static ImapResponse CAPABILITY_RESPONSE = ImapTestUtils.parseResponse(
             "* CAPABILITY IMAP4rev1 STARTTLS");
 
     /* These values are provided by setUp() */
@@ -129,9 +128,13 @@
         // TODO: inject specific facts in the initial folder SELECT and check them here
     }
 
+    /**
+     * Test simple login with failed authentication
+     */
     public void testLoginFailure() throws Exception {
         MockTransport mockTransport = openAndInjectMockTransport();
-        expectLogin(mockTransport, new String[] {"* iD nIL", "oK"}, "nO authentication failed");
+        expectLogin(mockTransport, false, new String[] {"* iD nIL", "oK"},
+                "nO authentication failed");
 
         try {
             mStore.getConnection().open();
@@ -141,9 +144,34 @@
     }
 
     /**
+     * Test simple TLS open
+     */
+    public void testTlsOpen() throws MessagingException {
+
+        MockTransport mockTransport = openAndInjectMockTransport(Transport.CONNECTION_SECURITY_TLS,
+                false);
+
+        // try to open it, with STARTTLS
+        expectLogin(mockTransport, true,
+                new String[] {"* iD nIL", "oK"}, "oK user authenticated (Success)");
+        mockTransport.expect(
+                getNextTag(false) + " SELECT \"" + FOLDER_ENCODED + "\"", 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]",
+                getNextTag(true) + " oK [" + "rEAD-wRITE" + "] " +
+                        FOLDER_ENCODED + " selected. (Success)"});
+
+        mFolder.open(OpenMode.READ_WRITE, null);
+        assertTrue(mockTransport.isTlsStarted());
+    }
+
+    /**
      * 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
      */
 
@@ -390,9 +418,17 @@
      * Set up a basic MockTransport. open it, and inject it into mStore
      */
     private MockTransport openAndInjectMockTransport() {
+        return openAndInjectMockTransport(Transport.CONNECTION_SECURITY_NONE, false);
+    }
+
+    /**
+     * Set up a MockTransport with security settings
+     */
+    private MockTransport openAndInjectMockTransport(int connectionSecurity,
+            boolean trustAllCertificates) {
         // 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.setSecurity(connectionSecurity, trustAllCertificates);
         mockTransport.setMockHost("mock.server.com");
         mStore.setTransport(mockTransport);
         return mockTransport;
@@ -427,8 +463,7 @@
      * @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).
-     * @param "READ-WRITE" or "READ-ONLY"
-     * @return the next tag# to use
+     * @param readWriteMode "READ-WRITE" or "READ-ONLY"
      */
     private void setupOpenFolder(MockTransport mockTransport, String[] imapIdResponse,
             String readWriteMode) {
@@ -450,18 +485,26 @@
     }
 
     private void expectLogin(MockTransport mockTransport, String[] imapIdResponse) {
-        expectLogin(mockTransport, imapIdResponse, "oK user authenticated (Success)");
+        expectLogin(mockTransport, false, imapIdResponse, "oK user authenticated (Success)");
     }
 
-    private void expectLogin(MockTransport mockTransport, String[] imapIdResponse,
+    private void expectLogin(MockTransport mockTransport, boolean startTls, String[] imapIdResponse,
             String loginResponse) {
         // inject boilerplate commands that match our typical login
         mockTransport.expect(null, "* oK Imap 2000 Ready To Assist You");
 
-        mockTransport.expect(getNextTag(false) + " CAPABILITY", new String[] {
-                "* cAPABILITY iMAP4rev1 sTARTTLS aUTH=gSSAPI lOGINDISABLED",
-                getNextTag(true) + " oK CAPABILITY completed"});
+        expectCapability(mockTransport);
 
+        // TLS (if expected)
+        if (startTls) {
+            mockTransport.expect(getNextTag(false) + " STARTTLS",
+                getNextTag(true) + " Ok starting TLS");
+            mockTransport.expectStartTls();
+            // After switching to TLS the client must re-query for capability
+            expectCapability(mockTransport);
+        }
+
+        // ID
         String expectedNextTag = getNextTag(false);
         // Fix the tag # of the ID response
         String last = imapIdResponse[imapIdResponse.length-1];
@@ -471,10 +514,17 @@
 
         getNextTag(true); // Advance the tag for ID response.
 
+        // LOGIN
         mockTransport.expect(getNextTag(false) + " LOGIN user \"password\"",
                 getNextTag(true) + " " + loginResponse);
     }
 
+    private void expectCapability(MockTransport mockTransport) {
+        mockTransport.expect(getNextTag(false) + " CAPABILITY", new String[] {
+            "* cAPABILITY iMAP4rev1 sTARTTLS aUTH=gSSAPI lOGINDISABLED",
+            getNextTag(true) + " oK CAPABILITY completed"});
+    }
+
     private void expectNoop(MockTransport mockTransport, boolean ok) {
         String response = ok ? " oK success" : " nO timeout";
         mockTransport.expect(getNextTag(false) + " NOOP",
@@ -1420,7 +1470,7 @@
     }
 
     /**
-     * Test for {@link ImapStore#getConnection} and {@link ImapStore#keepConnectionForReuse}
+     * Test for {@link ImapStore#getConnection}
      */
     public void testGetConnection() throws Exception {
         MockTransport mock = openAndInjectMockTransport();
@@ -1488,7 +1538,7 @@
         expectLogin(mock);
         mStore.checkSettings();
 
-        expectLogin(mock, new String[] {"* iD nIL", "oK"}, "nO authentication failed");
+        expectLogin(mock, false, new String[] {"* iD nIL", "oK"}, "nO authentication failed");
         try {
             mStore.checkSettings();
             fail();
diff --git a/tests/src/com/android/email/mail/transport/MockTransport.java b/tests/src/com/android/email/mail/transport/MockTransport.java
index 81bfb02..9b9a2b0 100644
--- a/tests/src/com/android/email/mail/transport/MockTransport.java
+++ b/tests/src/com/android/email/mail/transport/MockTransport.java
@@ -44,8 +44,7 @@
 
     private static final String SPECIAL_RESPONSE_IOEXCEPTION = "!!!IOEXCEPTION!!!";
 
-    private boolean mSslAllowed = false;
-    private boolean mTlsAllowed = false;
+    private boolean mTlsStarted = false;
 
     private boolean mOpen;
     private boolean mInputOpen;
@@ -60,6 +59,7 @@
         public static final int ACTION_INJECT_TEXT = 0;
         public static final int ACTION_CLIENT_CLOSE = 1;
         public static final int ACTION_IO_EXCEPTION = 2;
+        public static final int ACTION_START_TLS = 3;
 
         int mAction;
         String mPattern;
@@ -86,6 +86,8 @@
                     return "Expect the client to close";
                 case ACTION_IO_EXCEPTION:
                     return "Expect IOException";
+                case ACTION_START_TLS:
+                    return "Expect StartTls";
                 default:
                     return "(Hmm.  Unknown action.)";
             }
@@ -142,6 +144,10 @@
         mPairs.add(new Transaction(Transaction.ACTION_IO_EXCEPTION));
     }
 
+    public void expectStartTls() {
+        mPairs.add(new Transaction(Transaction.ACTION_START_TLS));
+    }
+
     private void sendResponse(Transaction pair) {
         switch (pair.mAction) {
             case Transaction.ACTION_INJECT_TEXT:
@@ -170,6 +176,13 @@
     }
 
     /**
+     * Check that TLS was started
+     */
+    public boolean isTlsStarted() {
+        return mTlsStarted;
+    }
+
+    /**
      * This simulates a condition where the server has closed its side, causing
      * reads to fail.
      */
@@ -289,8 +302,9 @@
 
     public void reopenTls() /* throws MessagingException */ {
         SmtpSenderUnitTests.assertTrue(mOpen);
-        SmtpSenderUnitTests.assertTrue(mTlsAllowed);
-        SmtpSenderUnitTests.fail("reopenTls() not implemented");
+        Transaction expect = mPairs.remove(0);
+        SmtpSenderUnitTests.assertTrue(expect.mAction == Transaction.ACTION_START_TLS);
+        mTlsStarted = true;
     }
 
     public void setSecurity(int connectionSecurity, boolean trustAllCertificates) {