1/*
2 * Copyright (C) 2008 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.email.mail.store;
18
19import android.content.Context;
20import android.test.AndroidTestCase;
21import android.test.suitebuilder.annotation.SmallTest;
22import android.test.suitebuilder.annotation.Suppress;
23
24import com.android.email.DBTestHelper;
25import com.android.email.mail.transport.MockTransport;
26import com.android.email.provider.ProviderTestUtils;
27import com.android.emailcommon.TempDirectory;
28import com.android.emailcommon.internet.MimeMessage;
29import com.android.emailcommon.mail.Address;
30import com.android.emailcommon.mail.FetchProfile;
31import com.android.emailcommon.mail.Flag;
32import com.android.emailcommon.mail.Folder;
33import com.android.emailcommon.mail.Folder.FolderType;
34import com.android.emailcommon.mail.Folder.OpenMode;
35import com.android.emailcommon.mail.Message;
36import com.android.emailcommon.mail.Message.RecipientType;
37import com.android.emailcommon.mail.MessagingException;
38import com.android.emailcommon.provider.Account;
39import com.android.emailcommon.provider.HostAuth;
40
41/**
42 * This is a series of unit tests for the POP3 Store class.  These tests must be locally
43 * complete - no server(s) required.
44 */
45@Suppress
46@SmallTest
47public class Pop3StoreUnitTests extends AndroidTestCase {
48    final String UNIQUE_ID_1 = "20080909002219r1800rrjo9e00";
49
50    final static int PER_MESSAGE_SIZE = 100;
51
52    /* These values are provided by setUp() */
53    private Pop3Store mStore = null;
54    private Pop3Store.Pop3Folder mFolder = null;
55
56    private Context mMockContext;
57    private HostAuth mHostAuth;
58
59    /**
60     * Setup code.  We generate a lightweight Pop3Store and Pop3Store.Pop3Folder.
61     */
62    @Override
63    protected void setUp() throws Exception {
64        super.setUp();
65        mMockContext = DBTestHelper.ProviderContextSetupHelper.getProviderContext(
66                getContext());
67
68        // Use the target's (i.e. the Email application) context
69        TempDirectory.setTempDirectory(mMockContext);
70
71        // These are needed so we can get at the inner classes
72        mHostAuth = new HostAuth();
73        Account testAccount = ProviderTestUtils.setupAccount("acct1", false, mMockContext);
74
75        mHostAuth.setLogin("user", "password");
76        mHostAuth.setConnection("pop3", "server", 999);
77        testAccount.mHostAuthRecv = mHostAuth;
78        testAccount.save(mMockContext);
79        mStore = (Pop3Store) Pop3Store.newInstance(testAccount, mMockContext);
80        mFolder = (Pop3Store.Pop3Folder) mStore.getFolder("INBOX");
81    }
82
83    /**
84     * Test various sunny-day operations of UIDL parser for multi-line responses
85     */
86    public void testUIDLParserMulti() {
87
88        // multi-line mode
89        Pop3Store.Pop3Folder.UidlParser parser = mFolder.new UidlParser();
90
91        // Test basic in-list UIDL
92        parser.parseMultiLine("101 " + UNIQUE_ID_1);
93        assertEquals(101, parser.mMessageNumber);
94        assertEquals(UNIQUE_ID_1, parser.mUniqueId);
95        assertFalse(parser.mEndOfMessage);
96        assertFalse(parser.mErr);
97
98        //  Test end-of-list
99        parser.parseMultiLine(".");
100        assertTrue(parser.mEndOfMessage);
101        assertFalse(parser.mErr);
102    }
103
104    /**
105     * Test various sunny-day operations of UIDL parser for single-line responses
106     */
107    public void testUIDLParserSingle() {
108
109        // single-line mode
110        Pop3Store.Pop3Folder.UidlParser parser = mFolder.new UidlParser();
111
112        // Test single-message OK response
113        parser.parseSingleLine("+OK 101 " + UNIQUE_ID_1);
114        assertEquals(101, parser.mMessageNumber);
115        assertEquals(UNIQUE_ID_1, parser.mUniqueId);
116        assertTrue(parser.mEndOfMessage);
117
118        // Test single-message ERR response
119        parser.parseSingleLine("-ERR what???");
120        assertTrue(parser.mErr);
121    }
122
123    /**
124     * Test various rainy-day operations of the UIDL parser for multi-line responses
125     * TODO other malformed responses
126     */
127    public void testUIDLParserMultiFail() {
128        // multi-line mode
129        Pop3Store.Pop3Folder.UidlParser parser = mFolder.new UidlParser();
130
131        // Test with null input
132        boolean result;
133        result = parser.parseMultiLine(null);
134        assertFalse(result);
135
136        // Test with empty input
137        result = parser.parseMultiLine("");
138        assertFalse(result);
139    }
140
141    /**
142     * Test various rainy-day operations of the UIDL parser for single-line responses
143     * TODO other malformed responses
144     */
145    public void testUIDLParserSingleFail() {
146        // single-line mode
147        Pop3Store.Pop3Folder.UidlParser parser = mFolder.new UidlParser();
148
149        // Test with null input
150        boolean result;
151        result = parser.parseSingleLine(null);
152        assertFalse(result);
153
154        // Test with empty input
155        result = parser.parseSingleLine("");
156        assertFalse(result);
157    }
158
159    /**
160     * Tests that variants on the RFC-specified formatting of UIDL work properly.
161     */
162    public void testUIDLComcastVariant() {
163
164        // multi-line mode
165        Pop3Store.Pop3Folder.UidlParser parser = mFolder.new UidlParser();
166
167        // Comcast servers send multiple spaces in their darn UIDL strings.
168        parser.parseMultiLine("101   " + UNIQUE_ID_1);
169        assertEquals(101, parser.mMessageNumber);
170        assertEquals(UNIQUE_ID_1, parser.mUniqueId);
171        assertFalse(parser.mEndOfMessage);
172        assertFalse(parser.mErr);
173    }
174
175    /**
176     * Confirms simple non-SSL non-TLS login
177     */
178    public void testSimpleLogin() throws MessagingException {
179
180        MockTransport mockTransport = openAndInjectMockTransport();
181
182        // try to open it
183        setupOpenFolder(mockTransport, 0, null);
184        mFolder.open(OpenMode.READ_ONLY);
185    }
186
187    /**
188     * TODO: Test with SSL negotiation (faked)
189     * TODO: Test with SSL required but not supported
190     * TODO: Test with TLS negotiation (faked)
191     * TODO: Test with TLS required but not supported
192     * TODO: Test calling getMessageCount(), getMessages(), etc.
193     */
194
195    /**
196     * Test the operation of checkSettings(), which requires (a) a good open and (b) UIDL support.
197     */
198    public void testCheckSettings() throws MessagingException {
199
200        MockTransport mockTransport = openAndInjectMockTransport();
201
202        // scenario 1:  CAPA returns -ERR, so we try UIDL explicitly
203        setupOpenFolder(mockTransport, 0, null);
204        setupUidlSequence(mockTransport, 1);
205        mockTransport.expect("QUIT", "");
206        mStore.checkSettings();
207
208        // scenario 2:  CAPA indicates UIDL, so we don't try UIDL
209        setupOpenFolder(mockTransport, 0, "UIDL");
210        mockTransport.expect("QUIT", "");
211        mStore.checkSettings();
212
213        // scenario 3:  CAPA returns -ERR, and UIDL fails
214        try {
215            setupOpenFolder(mockTransport, 0, null);
216            mockTransport.expect("UIDL", "-ERR unsupported");
217            mockTransport.expect("QUIT", "");
218            mStore.checkSettings();
219            fail("MessagingException was expected due to UIDL unsupported.");
220        } catch (MessagingException me) {
221            // this is expected, so eat it
222        }
223    }
224
225    /**
226     * Test a strange case that causes open to proceed without mCapabilities
227     *  open - fail with "-" error code
228     *  then check capabilities
229     */
230    public void testCheckSettingsCapabilities() throws MessagingException {
231
232        MockTransport mockTransport = openAndInjectMockTransport();
233
234        // First, preload an open that fails for some reason
235        mockTransport.expect(null, "-ERR from the Mock Transport.");
236
237        // And watch it fail
238        try {
239            Pop3Store.Pop3Folder folder = mStore.new Pop3Folder("INBOX");
240            folder.open(OpenMode.READ_WRITE);
241            fail("Should have thrown exception");
242        } catch (MessagingException me) {
243            // Expected - continue.
244        }
245
246        // Now try again (assuming a slightly different connection setup - successful)
247        // Note, checkSettings is going to try to close the connection again, so we expect
248        // one extra QUIT before we spin it up again
249        mockTransport.expect("QUIT", "");
250        mockTransport.expectClose();
251        setupOpenFolder(mockTransport, 0, "UIDL");
252        mockTransport.expect("QUIT", "");
253        mStore.checkSettings();
254    }
255
256    /**
257     * Test small Store & Folder functions that manage folders & namespace
258     */
259    public void testStoreFoldersFunctions() {
260
261        // getPersonalNamespaces() always returns INBOX folder
262        Folder[] folders = mStore.updateFolders();
263        assertEquals(1, folders.length);
264        assertSame(mFolder, folders[0]);
265
266        // getName() returns the name we were created with.  If "inbox", converts to INBOX
267        assertEquals("INBOX", mFolder.getName());
268        Pop3Store.Pop3Folder folderMixedCaseInbox = mStore.new Pop3Folder("iNbOx");
269        assertEquals("INBOX", folderMixedCaseInbox.getName());
270        Pop3Store.Pop3Folder folderNotInbox = mStore.new Pop3Folder("NOT-INBOX");
271        assertEquals("NOT-INBOX", folderNotInbox.getName());
272
273        // exists() true if name is INBOX
274        assertTrue(mFolder.exists());
275        assertTrue(folderMixedCaseInbox.exists());
276        assertFalse(folderNotInbox.exists());
277    }
278
279    /**
280     * Test small Folder functions that don't really do anything in Pop3
281     */
282    public void testSmallFolderFunctions() {
283
284        // getMode() returns OpenMode.READ_WRITE
285        assertEquals(OpenMode.READ_WRITE, mFolder.getMode());
286
287        // canCreate() && create() return false
288        assertFalse(mFolder.canCreate(FolderType.HOLDS_FOLDERS));
289        assertFalse(mFolder.canCreate(FolderType.HOLDS_MESSAGES));
290        assertFalse(mFolder.create(FolderType.HOLDS_FOLDERS));
291        assertFalse(mFolder.create(FolderType.HOLDS_MESSAGES));
292
293        // getUnreadMessageCount() always returns -1
294        assertEquals(-1, mFolder.getUnreadMessageCount());
295
296        // getPermanentFlags() returns { Flag.DELETED }
297        Flag[] flags = mFolder.getPermanentFlags();
298        assertEquals(1, flags.length);
299        assertEquals(Flag.DELETED, flags[0]);
300
301        // appendMessages(Message[] messages) does nothing
302        mFolder.appendMessages(null);
303
304        // delete(boolean recurse) does nothing
305        // TODO - it should!
306        mFolder.delete(false);
307
308        // expunge() returns null
309        assertNull(mFolder.expunge());
310
311        // copyMessages() is unsupported
312        try {
313            mFolder.copyMessages(null, null, null);
314            fail("Exception not thrown by copyMessages()");
315        } catch (UnsupportedOperationException e) {
316            // expected - succeed
317        }
318    }
319
320    /**
321     * Lightweight test to confirm that POP3 hasn't implemented any folder roles yet.
322     */
323    public void testNoFolderRolesYet() {
324        Folder[] remoteFolders = mStore.updateFolders();
325        for (Folder folder : remoteFolders) {
326            assertEquals(Folder.FolderRole.UNKNOWN, folder.getRole());
327        }
328    }
329
330    /**
331     * Lightweight test to confirm that POP3 is requesting sent-message-upload.
332     */
333    public void testSentUploadRequested() {
334        assertTrue(mStore.requireCopyMessageToSentFolder());
335    }
336
337    /**
338     * Test the process of opening and indexing a mailbox with one unread message in it.
339     *
340     * TODO should create an instrumented listener to confirm all expected callbacks.  Then use
341     * it everywhere we could have passed a message listener.
342     */
343    public void testOneUnread() throws MessagingException {
344
345        MockTransport mockTransport = openAndInjectMockTransport();
346
347        checkOneUnread(mockTransport);
348    }
349
350    /**
351     * Test the process of opening and getting message by uid.
352     */
353    public void testGetMessageByUid() throws MessagingException {
354
355        MockTransport mockTransport = openAndInjectMockTransport();
356
357        setupOpenFolder(mockTransport, 2, null);
358        mFolder.open(OpenMode.READ_WRITE);
359        // check message count
360        assertEquals(2, mFolder.getMessageCount());
361
362        // setup 2 messages
363        setupUidlSequence(mockTransport, 2);
364        String uid1 = getSingleMessageUID(1);
365        String uid2 = getSingleMessageUID(2);
366        String uid3 = getSingleMessageUID(3);
367
368        Message msg1 = mFolder.getMessage(uid1);
369        assertTrue("message with uid1", msg1 != null);
370
371        // uid3 does not exist
372        Message msg3 = mFolder.getMessage(uid3);
373        assertTrue("message with uid3", msg3 == null);
374
375        Message msg2 = mFolder.getMessage(uid2);
376        assertTrue("message with uid2", msg2 != null);
377    }
378
379    /**
380     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
381     * things should happen:  We should see an intermediate failure that makes sense, and the next
382     * operation should reopen properly.
383     *
384     * There are multiple versions of this test because we are simulating the steps of
385     * MessagingController.synchronizeMailboxSyncronous() and we will inject the failure a bit
386     * further along in each case, to test various recovery points.
387     *
388     * This test confirms that Pop3Store needs to call close() in the IOExceptionHandler in
389     * Pop3Folder.getMessages(), due to a closure before the UIDL command completes.
390     */
391    public void testCatchClosed1a() throws MessagingException {
392
393        MockTransport mockTransport = openAndInjectMockTransport();
394
395        openFolderWithMessage(mockTransport);
396
397        // cause the next sequence to fail on the readLine() calls
398        mockTransport.closeInputStream();
399
400        // index the message(s) - it should fail, because our stream is broken
401        try {
402            setupUidlSequence(mockTransport, 1);
403            Message[] messages = mFolder.getMessages(1, 1, null);
404            assertEquals(1, messages.length);
405            assertEquals(getSingleMessageUID(1), messages[0].getUid());
406            fail("Broken stream should cause getMessages() to throw.");
407        } catch(MessagingException me) {
408            // success
409        }
410
411        // At this point the UI would display connection error, which is fine.  Now, the real
412        // test is, can we recover?  So I'll just repeat the above steps, without the failure.
413        // NOTE: everything from here down is copied from testOneUnread() and should be consolidated
414
415        // confirm that we're closed at this point
416        assertFalse("folder should be 'closed' after an IOError", mFolder.isOpen());
417
418        // and confirm that the next connection will be OK
419        checkOneUnread(mockTransport);
420    }
421
422    /**
423     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
424     * things should happen:  We should see an intermediate failure that makes sense, and the next
425     * operation should reopen properly.
426     *
427     * There are multiple versions of this test because we are simulating the steps of
428     * MessagingController.synchronizeMailboxSyncronous() and we will inject the failure a bit
429     * further along in each case, to test various recovery points.
430     *
431     * This test confirms that Pop3Store needs to call close() in the IOExceptionHandler in
432     * Pop3Folder.getMessages(), due to non-numeric data in a multi-line UIDL.
433     */
434    public void testCatchClosed1b() throws MessagingException {
435
436        MockTransport mockTransport = openAndInjectMockTransport();
437
438        openFolderWithMessage(mockTransport);
439
440        // index the message(s) - it should fail, because our stream is broken
441        try {
442            // setupUidlSequence(mockTransport, 1);
443            mockTransport.expect("UIDL", "+OK sending UIDL list");
444            mockTransport.expect(null, "bad-data" + " " + "THE-UIDL");
445            mockTransport.expect(null, ".");
446
447            Message[] messages = mFolder.getMessages(1, 1, null);
448            fail("Bad UIDL should cause getMessages() to throw.");
449        } catch(MessagingException me) {
450            // success
451        }
452
453        // At this point the UI would display connection error, which is fine.  Now, the real
454        // test is, can we recover?  So I'll just repeat the above steps, without the failure.
455        // NOTE: everything from here down is copied from testOneUnread() and should be consolidated
456
457        // confirm that we're closed at this point
458        assertFalse("folder should be 'closed' after an IOError", mFolder.isOpen());
459
460        // and confirm that the next connection will be OK
461        checkOneUnread(mockTransport);
462    }
463
464    /**
465     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
466     * things should happen:  We should see an intermediate failure that makes sense, and the next
467     * operation should reopen properly.
468     *
469     * There are multiple versions of this test because we are simulating the steps of
470     * MessagingController.synchronizeMailboxSyncronous() and we will inject the failure a bit
471     * further along in each case, to test various recovery points.
472     *
473     * This test confirms that Pop3Store needs to call close() in the IOExceptionHandler in
474     * Pop3Folder.getMessages(), due to non-numeric data in a single-line UIDL.
475     */
476    public void testCatchClosed1c() throws MessagingException {
477
478        MockTransport mockTransport = openAndInjectMockTransport();
479
480        // openFolderWithMessage(mockTransport);
481        setupOpenFolder(mockTransport, 6000, null);
482        mFolder.open(OpenMode.READ_ONLY);
483        assertEquals(6000, mFolder.getMessageCount());
484
485        // index the message(s) - it should fail, because our stream is broken
486        try {
487            // setupUidlSequence(mockTransport, 1);
488            mockTransport.expect("UIDL 1", "+OK " + "bad-data" + " " + "THE-UIDL");
489
490            Message[] messages = mFolder.getMessages(1, 1, null);
491            fail("Bad UIDL should cause getMessages() to throw.");
492        } catch(MessagingException me) {
493            // success
494        }
495
496        // At this point the UI would display connection error, which is fine.  Now, the real
497        // test is, can we recover?  So I'll just repeat the above steps, without the failure.
498        // NOTE: everything from here down is copied from testOneUnread() and should be consolidated
499
500        // confirm that we're closed at this point
501        assertFalse("folder should be 'closed' after an IOError", mFolder.isOpen());
502
503        // and confirm that the next connection will be OK
504        checkOneUnread(mockTransport);
505    }
506
507    /**
508     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
509     * things should happen:  We should see an intermediate failure that makes sense, and the next
510     * operation should reopen properly.
511     *
512     * There are multiple versions of this test because we are simulating the steps of
513     * MessagingController.synchronizeMailboxSyncronous() and we will inject the failure a bit
514     * further along in each case, to test various recovery points.
515     *
516     * This test confirms that Pop3Store needs to call close() in the first IOExceptionHandler in
517     * Pop3Folder.fetch(), for a failure in the call to indexUids().
518     */
519    public void testCatchClosed2() throws MessagingException {
520
521        MockTransport mockTransport = openAndInjectMockTransport();
522
523        openFolderWithMessage(mockTransport);
524
525        // index the message(s)
526        setupUidlSequence(mockTransport, 1);
527        Message[] messages = mFolder.getMessages(1, 1, null);
528        assertEquals(1, messages.length);
529        assertEquals(getSingleMessageUID(1), messages[0].getUid());
530
531        // cause the next sequence to fail on the readLine() calls
532        mockTransport.closeInputStream();
533
534        try {
535            // try the basic fetch of flags & envelope
536            setupListSequence(mockTransport, 1);
537            FetchProfile fp = new FetchProfile();
538            fp.add(FetchProfile.Item.FLAGS);
539            fp.add(FetchProfile.Item.ENVELOPE);
540            mFolder.fetch(messages, fp, null);
541            assertEquals(PER_MESSAGE_SIZE, messages[0].getSize());
542            fail("Broken stream should cause fetch() to throw.");
543        }
544        catch(MessagingException me) {
545            // success
546        }
547
548        // At this point the UI would display connection error, which is fine.  Now, the real
549        // test is, can we recover?  So I'll just repeat the above steps, without the failure.
550        // NOTE: everything from here down is copied from testOneUnread() and should be consolidated
551
552        // confirm that we're closed at this point
553        assertFalse("folder should be 'closed' after an IOError", mFolder.isOpen());
554
555        // and confirm that the next connection will be OK
556        checkOneUnread(mockTransport);
557    }
558
559    /**
560     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
561     * things should happen:  We should see an intermediate failure that makes sense, and the next
562     * operation should reopen properly.
563     *
564     * There are multiple versions of this test because we have to check additional places where
565     * Pop3Store and/or Pop3Folder should be dealing with IOErrors.
566     *
567     * This test confirms that Pop3Store needs to call close() in the first IOExceptionHandler in
568     * Pop3Folder.fetch(), for a failure in the call to fetchEnvelope().
569     */
570    public void testCatchClosed2a() throws MessagingException {
571
572        MockTransport mockTransport = openAndInjectMockTransport();
573
574        openFolderWithMessage(mockTransport);
575
576        // index the message(s)
577        setupUidlSequence(mockTransport, 1);
578        Message[] messages = mFolder.getMessages(1, 1, null);
579        assertEquals(1, messages.length);
580        assertEquals(getSingleMessageUID(1), messages[0].getUid());
581
582        // try the basic fetch of flags & envelope, but the LIST command fails
583        setupBrokenListSequence(mockTransport, 1);
584        try {
585            FetchProfile fp = new FetchProfile();
586            fp.add(FetchProfile.Item.FLAGS);
587            fp.add(FetchProfile.Item.ENVELOPE);
588            mFolder.fetch(messages, fp, null);
589            assertEquals(PER_MESSAGE_SIZE, messages[0].getSize());
590            fail("Broken stream should cause fetch() to throw.");
591        } catch(MessagingException me) {
592            // success
593        }
594
595        // At this point the UI would display connection error, which is fine.  Now, the real
596        // test is, can we recover?  So I'll just repeat the above steps, without the failure.
597        // NOTE: everything from here down is copied from testOneUnread() and should be consolidated
598
599        // confirm that we're closed at this point
600        assertFalse("folder should be 'closed' after an IOError", mFolder.isOpen());
601
602        // and confirm that the next connection will be OK
603        checkOneUnread(mockTransport);
604    }
605
606    /**
607     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
608     * things should happen:  We should see an intermediate failure that makes sense, and the next
609     * operation should reopen properly.
610     *
611     * There are multiple versions of this test because we are simulating the steps of
612     * MessagingController.synchronizeMailboxSyncronous() and we will inject the failure a bit
613     * further along in each case, to test various recovery points.
614     *
615     * This test confirms that Pop3Store needs to call close() in the second IOExceptionHandler in
616     * Pop3Folder.fetch().
617     */
618    public void testCatchClosed3() throws MessagingException {
619
620        MockTransport mockTransport = openAndInjectMockTransport();
621
622        openFolderWithMessage(mockTransport);
623
624        // index the message(s)
625        setupUidlSequence(mockTransport, 1);
626        Message[] messages = mFolder.getMessages(1, 1, null);
627        assertEquals(1, messages.length);
628        assertEquals(getSingleMessageUID(1), messages[0].getUid());
629
630        // try the basic fetch of flags & envelope
631        setupListSequence(mockTransport, 1);
632        FetchProfile fp = new FetchProfile();
633        fp.add(FetchProfile.Item.FLAGS);
634        fp.add(FetchProfile.Item.ENVELOPE);
635        mFolder.fetch(messages, fp, null);
636        assertEquals(PER_MESSAGE_SIZE, messages[0].getSize());
637
638        // cause the next sequence to fail on the readLine() calls
639        mockTransport.closeInputStream();
640
641        try {
642            // now try fetching the message
643            setupSingleMessage(mockTransport, 1, false);
644            fp = new FetchProfile();
645            fp.add(FetchProfile.Item.BODY);
646            mFolder.fetch(messages, fp, null);
647            checkFetchedMessage(messages[0], 1, false);
648            fail("Broken stream should cause fetch() to throw.");
649        }
650        catch(MessagingException me) {
651            // success
652        }
653
654        // At this point the UI would display connection error, which is fine.  Now, the real
655        // test is, can we recover?  So I'll just repeat the above steps, without the failure.
656        // NOTE: everything from here down is copied from testOneUnread() and should be consolidated
657
658        // confirm that we're closed at this point
659        assertFalse("folder should be 'closed' after an IOError", mFolder.isOpen());
660
661        // and confirm that the next connection will be OK
662        checkOneUnread(mockTransport);
663    }
664
665    /**
666     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
667     * things should happen:  We should see an intermediate failure that makes sense, and the next
668     * operation should reopen properly.
669     *
670     * There are multiple versions of this test because we have to check additional places where
671     * Pop3Store and/or Pop3Folder should be dealing with IOErrors.
672     *
673     * This test confirms that Pop3Store needs to call close() in the IOExceptionHandler in
674     * Pop3Folder.setFlags().
675     */
676    public void testCatchClosed4() throws MessagingException {
677
678        MockTransport mockTransport = openAndInjectMockTransport();
679
680        openFolderWithMessage(mockTransport);
681
682        // index the message(s)
683        setupUidlSequence(mockTransport, 1);
684        Message[] messages = mFolder.getMessages(1, 1, null);
685        assertEquals(1, messages.length);
686        assertEquals(getSingleMessageUID(1), messages[0].getUid());
687
688        // cause the next sequence to fail on the readLine() calls
689        mockTransport.closeInputStream();
690
691        // delete 'em all - should fail because of broken stream
692        try {
693            mockTransport.expect("DELE 1", "+OK message deleted");
694            mFolder.setFlags(messages, new Flag[] { Flag.DELETED }, true);
695            fail("Broken stream should cause fetch() to throw.");
696        }
697        catch(MessagingException me) {
698            // success
699        }
700
701        // At this point the UI would display connection error, which is fine.  Now, the real
702        // test is, can we recover?  So I'll just repeat the above steps, without the failure.
703        // NOTE: everything from here down is copied from testOneUnread() and should be consolidated
704
705        // confirm that we're closed at this point
706        assertFalse("folder should be 'closed' after an IOError", mFolder.isOpen());
707
708        // and confirm that the next connection will be OK
709        checkOneUnread(mockTransport);
710    }
711
712    /**
713     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
714     * things should happen:  We should see an intermediate failure that makes sense, and the next
715     * operation should reopen properly.
716     *
717     * There are multiple versions of this test because we have to check additional places where
718     * Pop3Store and/or Pop3Folder should be dealing with IOErrors.
719     *
720     * This test confirms that Pop3Store needs to call close() in the first IOExceptionHandler in
721     * Pop3Folder.open().
722     */
723    public void testCatchClosed5() {
724        // TODO cannot write this test until we can inject stream closures mid-sequence
725    }
726
727    /**
728     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
729     * things should happen:  We should see an intermediate failure that makes sense, and the next
730     * operation should reopen properly.
731     *
732     * There are multiple versions of this test because we have to check additional places where
733     * Pop3Store and/or Pop3Folder should be dealing with IOErrors.
734     *
735     * This test confirms that Pop3Store needs to call close() in the second IOExceptionHandler in
736     * Pop3Folder.open() (when it calls STAT and the response is empty of garbagey).
737     */
738    public void testCatchClosed6a() throws MessagingException {
739
740        MockTransport mockTransport = openAndInjectMockTransport();
741
742        // like openFolderWithMessage(mockTransport) but with a broken STAT report (empty response)
743        setupOpenFolder(mockTransport, -1, null);
744        try {
745            mFolder.open(OpenMode.READ_ONLY);
746            fail("Broken STAT should cause open() to throw.");
747        } catch(MessagingException me) {
748            // success
749        }
750
751        // At this point the UI would display connection error, which is fine.  Now, the real
752        // test is, can we recover?  So I'll try a new connection, without the failure.
753
754        // confirm that we're closed at this point
755        assertFalse("folder should be 'closed' after an IOError", mFolder.isOpen());
756
757        // and confirm that the next connection will be OK
758        checkOneUnread(mockTransport);
759    }
760
761    /**
762     * Test the scenario where the transport is "open" but not really (e.g. server closed).  Two
763     * things should happen:  We should see an intermediate failure that makes sense, and the next
764     * operation should reopen properly.
765     *
766     * There are multiple versions of this test because we have to check additional places where
767     * Pop3Store and/or Pop3Folder should be dealing with IOErrors.
768     *
769     * This test confirms that Pop3Store needs to call close() in the second IOExceptionHandler in
770     * Pop3Folder.open() (when it calls STAT, and there is no response at all).
771     */
772    public void testCatchClosed6b() {
773        // TODO cannot write this test until we can inject stream closures mid-sequence
774    }
775
776    /**
777     * Given an initialized mock transport, open it and attempt to "read" one unread message from
778     * it.  This can be used as a basic test of functionality and it should be possible to call this
779     * repeatedly (if you close the folder between calls).
780     *
781     * @param mockTransport the mock transport we're using
782     */
783    private void checkOneUnread(MockTransport mockTransport) throws MessagingException {
784        openFolderWithMessage(mockTransport);
785
786        // index the message(s)
787        setupUidlSequence(mockTransport, 1);
788        Message[] messages = mFolder.getMessages(1, 1, null);
789        assertEquals(1, messages.length);
790        assertEquals(getSingleMessageUID(1), messages[0].getUid());
791
792        // try the basic fetch of flags & envelope
793        setupListSequence(mockTransport, 1);
794        FetchProfile fp = new FetchProfile();
795        fp.add(FetchProfile.Item.FLAGS);
796        fp.add(FetchProfile.Item.ENVELOPE);
797        mFolder.fetch(messages, fp, null);
798        assertEquals(PER_MESSAGE_SIZE, messages[0].getSize());
799
800        // A side effect of how messages work is that if you get fields that are empty,
801        // then empty arrays are written back into the parsed header fields (e.g. mTo, mFrom).  The
802        // standard message parser needs to clear these before parsing.  Make sure that this
803        // is happening.  (This doesn't affect IMAP, which reads the headers directly via
804        // IMAP evelopes.)
805        MimeMessage message = (MimeMessage) messages[0];
806        message.getRecipients(RecipientType.TO);
807        message.getRecipients(RecipientType.CC);
808        message.getRecipients(RecipientType.BCC);
809
810        // now try fetching the message
811        setupSingleMessage(mockTransport, 1, false);
812        fp = new FetchProfile();
813        fp.add(FetchProfile.Item.BODY);
814        mFolder.fetch(messages, fp, null);
815        checkFetchedMessage(messages[0], 1, false);
816    }
817
818    /**
819     * A group of tests to confirm that we're properly juggling the RETR and TOP commands.
820     * Some servers (hello, live.com) support TOP but don't support CAPA.  So we ignore CAPA
821     * and just try TOP.
822     */
823    public void testRetrVariants() throws MessagingException {
824        MockTransport mockTransport = openAndInjectMockTransport();
825        openFolderWithMessage(mockTransport);
826
827        // index the message(s)
828        setupUidlSequence(mockTransport, 2);
829        Message[] messages = mFolder.getMessages(1, 2, null);
830        assertEquals(2, messages.length);
831
832        // basic fetch of flags & envelope
833        setupListSequence(mockTransport, 2);
834        FetchProfile fp = new FetchProfile();
835        fp.add(FetchProfile.Item.FLAGS);
836        fp.add(FetchProfile.Item.ENVELOPE);
837        mFolder.fetch(messages, fp, null);
838
839        // A side effect of how messages work is that if you get fields that are empty,
840        // then empty arrays are written back into the parsed header fields (e.g. mTo, mFrom).  The
841        // standard message parser needs to clear these before parsing.  Make sure that this
842        // is happening.  (This doesn't affect IMAP, which reads the headers directly via
843        // IMAP envelopes.)
844        for (Message message : messages) {
845            message.getRecipients(RecipientType.TO);
846            message.getRecipients(RecipientType.CC);
847            message.getRecipients(RecipientType.BCC);
848        }
849
850        // In the cases below, we fetch BODY_SANE which tries to load the first chunk of the
851        // message (not the entire thing) in order to quickly access the headers.
852        // In the first test, TOP succeeds
853        Message[] singleMessage = new Message[] { messages[0] };
854        setupSingleMessageTop(mockTransport, 1, true, true);        // try TOP & succeed
855        fp = new FetchProfile();
856        fp.add(FetchProfile.Item.BODY_SANE);
857        mFolder.fetch(singleMessage, fp, null);
858        checkFetchedMessage(singleMessage[0], 1, false);
859
860        // In the 2nd test, TOP fails, so we should fall back to RETR
861        singleMessage[0] = messages[1];
862        setupSingleMessageTop(mockTransport, 2, true, false);        // try TOP & fail
863        fp = new FetchProfile();
864        fp.add(FetchProfile.Item.BODY_SANE);
865        mFolder.fetch(singleMessage, fp, null);
866        checkFetchedMessage(singleMessage[0], 2, false);
867    }
868
869    /**
870     * Set up a basic MockTransport. open it, and inject it into mStore
871     */
872    private MockTransport openAndInjectMockTransport() {
873        // Create mock transport and inject it into the POP3Store that's already set up
874        MockTransport mockTransport = new MockTransport(mContext, mHostAuth);
875        mockTransport.setSecurity(HostAuth.FLAG_NONE, false);
876        mStore.setTransport(mockTransport);
877        return mockTransport;
878    }
879
880    /**
881     * Open a folder that's preloaded with one unread message.
882     *
883     * @param mockTransport the mock transport we're using
884     */
885    private void openFolderWithMessage(MockTransport mockTransport) throws MessagingException {
886        // try to open it
887        setupOpenFolder(mockTransport, 1, null);
888        mFolder.open(OpenMode.READ_ONLY);
889
890        // check message count
891        assertEquals(1, mFolder.getMessageCount());
892    }
893
894    /**
895     * Look at a fetched message and confirm that it is complete.
896     *
897     * TODO this needs to be more dynamic, not just hardcoded for empty message #1.
898     *
899     * @param message the fetched message to be checked
900     * @param msgNum the message number
901     */
902    private void checkFetchedMessage(Message message, int msgNum, boolean body)
903            throws MessagingException {
904        // check To:
905        Address[] to = message.getRecipients(RecipientType.TO);
906        assertNotNull(to);
907        assertEquals(1, to.length);
908        assertEquals("Smith@Registry.Org", to[0].getAddress());
909        assertNull(to[0].getPersonal());
910
911        // check From:
912        Address[] from = message.getFrom();
913        assertNotNull(from);
914        assertEquals(1, from.length);
915        assertEquals("Jones@Registry.Org", from[0].getAddress());
916        assertNull(from[0].getPersonal());
917
918        // check Cc:
919        Address[] cc = message.getRecipients(RecipientType.CC);
920        assertNotNull(cc);
921        assertEquals(1, cc.length);
922        assertEquals("Chris@Registry.Org", cc[0].getAddress());
923        assertNull(cc[0].getPersonal());
924
925        // check Reply-To:
926        Address[] replyto = message.getReplyTo();
927        assertNotNull(replyto);
928        assertEquals(1, replyto.length);
929        assertEquals("Roger@Registry.Org", replyto[0].getAddress());
930        assertNull(replyto[0].getPersonal());
931
932        // TODO date
933
934        // TODO check body (if applicable)
935    }
936
937    /**
938     * Helper which stuffs the mock with enough strings to satisfy a call to Pop3Folder.open()
939     *
940     * @param mockTransport the mock transport we're using
941     * @param statCount the number of messages to indicate in the STAT, or -1 for broken STAT
942     * @param capabilities if non-null, comma-separated list of capabilities
943     */
944    private void setupOpenFolder(MockTransport mockTransport, int statCount, String capabilities) {
945        mockTransport.expect(null, "+OK Hello there from the Mock Transport.");
946        if (capabilities == null) {
947            mockTransport.expect("CAPA", "-ERR unimplemented");
948        } else {
949            mockTransport.expect("CAPA", "+OK capabilities follow");
950            mockTransport.expect(null, capabilities.split(","));        // one capability per line
951            mockTransport.expect(null, ".");                            // terminated by "."
952        }
953        mockTransport.expect("USER user", "+OK User name accepted");
954        mockTransport.expect("PASS password", "+OK Logged in");
955        if (statCount == -1) {
956            mockTransport.expect("STAT", "");
957        } else {
958            String stat = "+OK " + Integer.toString(statCount) + " "
959                    + Integer.toString(PER_MESSAGE_SIZE * statCount);
960            mockTransport.expect("STAT", stat);
961        }
962    }
963
964    /**
965     * Setup expects for a UIDL on a mailbox with 0 or more messages in it.
966     * @param transport The mock transport to preload
967     * @param numMessages The number of messages to return from UIDL.
968     */
969    private static void setupUidlSequence(MockTransport transport, int numMessages) {
970        transport.expect("UIDL", "+OK sending UIDL list");
971        for (int msgNum = 1; msgNum <= numMessages; ++msgNum) {
972            transport.expect(null, Integer.toString(msgNum) + " " + getSingleMessageUID(msgNum));
973        }
974        transport.expect(null, ".");
975    }
976
977    /**
978     * Setup expects for a LIST on a mailbox with 0 or more messages in it.
979     * @param transport The mock transport to preload
980     * @param numMessages The number of messages to return from LIST.
981     */
982    private static void setupListSequence(MockTransport transport, int numMessages) {
983        transport.expect("LIST", "+OK sending scan listing");
984        for (int msgNum = 1; msgNum <= numMessages; ++msgNum) {
985            transport.expect(null, Integer.toString(msgNum) + " " +
986                    Integer.toString(PER_MESSAGE_SIZE * msgNum));
987        }
988        transport.expect(null, ".");
989    }
990
991    /**
992     * Setup expects for a LIST on a mailbox with 0 or more messages in it, except that
993     * this time the pipe fails, and we return empty lines.
994     * @param transport The mock transport to preload
995     * @param numMessages The number of messages to return from LIST.
996     */
997    private static void setupBrokenListSequence(MockTransport transport, int numMessages) {
998        transport.expect("LIST", "");
999        for (int msgNum = 1; msgNum <= numMessages; ++msgNum) {
1000            transport.expect(null, "");
1001        }
1002        transport.expect(null, "");
1003    }
1004
1005    /**
1006     * Setup a single message to be retrieved.
1007     *
1008     * Per RFC822 here is a minimal message header:
1009     *     Date:     26 Aug 76 1429 EDT
1010     *     From:     Jones@Registry.Org
1011     *     To:       Smith@Registry.Org
1012     *
1013     * We'll add the following fields to support additional tests:
1014     *     Cc:       Chris@Registry.Org
1015     *     Reply-To: Roger@Registry.Org
1016     *
1017     * @param transport the mock transport to preload
1018     * @param msgNum the message number to expect and return
1019     * @param body if true, a non-empty body will be added
1020     */
1021    private static void setupSingleMessage(MockTransport transport, int msgNum, boolean body) {
1022        setupSingleMessageTop(transport, msgNum, false, false);
1023    }
1024
1025    /**
1026     * Setup a single message to be retrieved (headers only).
1027     * This is very similar to setupSingleMessage() but is intended to test the BODY_SANE
1028     * fetch mode.
1029     * @param transport the mock transport
1030     * @param msgNum the message number to expect and return
1031     * @param topTry if true, the "client" is going to attempt the TOP command
1032     * @param topSupported if true, the "server" supports the TOP command
1033     */
1034    private static void setupSingleMessageTop(MockTransport transport, int msgNum,
1035            boolean topTry, boolean topSupported) {
1036        String msgNumString = Integer.toString(msgNum);
1037        String topCommand = "TOP " + msgNumString + " 673";
1038        String retrCommand = "RETR " + msgNumString;
1039
1040        if (topTry) {
1041            if (topSupported) {
1042                transport.expect(topCommand, "+OK message follows");
1043            } else {
1044                transport.expect(topCommand, "-ERR unsupported command");
1045                transport.expect(retrCommand, "+OK message follows");
1046            }
1047        } else {
1048            transport.expect(retrCommand, "+OK message follows");
1049        }
1050
1051        transport.expect(null, "Date: 26 Aug 76 1429 EDT");
1052        transport.expect(null, "From: Jones@Registry.Org");
1053        transport.expect(null, "To:   Smith@Registry.Org");
1054        transport.expect(null, "CC:   Chris@Registry.Org");
1055        transport.expect(null, "Reply-To: Roger@Registry.Org");
1056        transport.expect(null, "");
1057        transport.expect(null, ".");
1058    }
1059
1060    /**
1061     * Generates a simple unique code for each message.  Repeatable.
1062     * @param msgNum The message number
1063     * @return a string that can be used as the UID
1064     */
1065    private static String getSingleMessageUID(int msgNum) {
1066        final String UID_HEAD = "ABCDEF-";
1067        final String UID_TAIL = "";
1068        return UID_HEAD + Integer.toString(msgNum) + UID_TAIL;
1069    }
1070}
1071