From 7d3519151a34792956cfc2b63bd2735fd0202d54 Mon Sep 17 00:00:00 2001 From: Makoto Onuki Date: Mon, 17 May 2010 15:44:36 -0700 Subject: [PATCH] Tests for IMAP FETCH Adding regression test for the new IMAP parser. Change-Id: Iac7f5c022e44ca5f06f735e145af15cc459eb61f --- proguard.flags | 6 +- .../email/mail/internet/MimeMultipart.java | 4 + .../email/mail/store/ImapStoreUnitTests.java | 224 ++++++++++++++++++ .../email/mail/transport/MockTransport.java | 52 ++-- 4 files changed, 259 insertions(+), 27 deletions(-) diff --git a/proguard.flags b/proguard.flags index bbffc8a54..f3b7e57d2 100644 --- a/proguard.flags +++ b/proguard.flags @@ -23,9 +23,9 @@ -keep class * extends org.apache.james.mime4j.util.TempStorage - # Keep names that are used only by unit tests +# Any methods whose name is '*ForTest' are preserved. -keep class ** { *** *ForTest(...); } @@ -172,3 +172,7 @@ -keep class org.apache.james.mime4j.message.Message { *; } + +-keepclasseswithmembers class org.apache.commons.io.IOUtils { + *** toByteArray(...); +} diff --git a/src/com/android/email/mail/internet/MimeMultipart.java b/src/com/android/email/mail/internet/MimeMultipart.java index 629d56df5..3673e0978 100644 --- a/src/com/android/email/mail/internet/MimeMultipart.java +++ b/src/com/android/email/mail/internet/MimeMultipart.java @@ -104,4 +104,8 @@ public class MimeMultipart extends Multipart { public InputStream getInputStream() throws MessagingException { return null; } + + public String getSubTypeForTest() { + return mSubType; + } } diff --git a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java index d95108dad..640209f77 100644 --- a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java +++ b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java @@ -17,7 +17,9 @@ package com.android.email.mail.store; import com.android.email.Email; +import com.android.email.Utility; import com.android.email.mail.Address; +import com.android.email.mail.Body; import com.android.email.mail.FetchProfile; import com.android.email.mail.Flag; import com.android.email.mail.Folder; @@ -28,11 +30,15 @@ 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; import com.android.email.mail.internet.TextBody; import com.android.email.mail.store.ImapStore.ImapMessage; import com.android.email.mail.transport.MockTransport; +import org.apache.commons.io.IOUtils; + import android.test.AndroidTestCase; import android.test.MoreAsserts; import android.test.suitebuilder.annotation.SmallTest; @@ -438,6 +444,224 @@ public class ImapStoreUnitTests extends AndroidTestCase { assertEquals("getUnreadMessageCount with literal string", 10, unreadCount); } + public void testFetchFlagEnvelope() throws MessagingException { + final MockTransport mock = openAndInjectMockTransport(); + setupOpenFolder(mock); + mFolder.open(OpenMode.READ_WRITE, null); + final Message message = mFolder.createMessage("1"); + + final FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.FLAGS); + fp.add(FetchProfile.Item.ENVELOPE); + mock.expect(getNextTag(false) + + " UID FETCH 1 \\(UID FLAGS INTERNALDATE RFC822\\.SIZE BODY\\.PEEK\\[HEADER.FIELDS" + + " \\(date subject from content-type to cc message-id\\)\\]\\)", + new String[] { + "* 9 FETCH (UID 1 RFC822.SIZE 120626 INTERNALDATE \"17-May-2010 22:00:15 +0000\"" + + "FLAGS (\\Seen) BODY[HEADER.FIELDS (date subject from content-type to cc" + + " message-id)]" + + " {279}", + "From: Xxxxxx Yyyyy ", + "Date: Mon, 17 May 2010 14:59:52 -0700", + "Message-ID: ", + "Subject: ssubject", + "To: android.test01@android.com", + "Content-Type: multipart/mixed; boundary=a00000000000000000000000000b", + "", + ")", + getNextTag(true) + " OK SUCCESS" + }); + mFolder.fetch(new Message[] { message }, fp, null); + + assertEquals("android.test01@android.com", message.getHeader("to")[0]); + assertEquals("Xxxxxx Yyyyy ", message.getHeader("from")[0]); + assertEquals("multipart/mixed; boundary=a00000000000000000000000000b", + message.getHeader("Content-Type")[0]); + assertTrue(message.isSet(Flag.SEEN)); + } + + public void testFetchBodyStructure() throws MessagingException { + final MockTransport mock = openAndInjectMockTransport(); + setupOpenFolder(mock); + mFolder.open(OpenMode.READ_WRITE, null); + final Message message = mFolder.createMessage("1"); + + final FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.STRUCTURE); + mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", + new String[] { + "* 9 FETCH (UID 1 BODYSTRUCTURE ((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\")" + + " CID NIL \"7BIT\" 18 3 NIL NIL NIL)" + + "(\"IMAGE\" \"PNG\"" + + " (\"NAME\" \"device.png\") NIL NIL \"BASE64\" 117840 NIL (\"ATTACHMENT\"" + + "(\"FILENAME\" \"device.png\")) NIL)" + + "(\"TEXT\" \"HTML\"" + + " () NIL NIL \"7BIT\" 100 NIL NIL (\"ATTACHMENT\"" + + "(\"FILENAME\" \"attachment.html\" \"SIZE\" 555)) NIL)" + + "\"MIXED\" (\"BOUNDARY\" \"00032556278a7005e40486d159ca\") NIL NIL))", + getNextTag(true) + " OK SUCCESS" + }); + mFolder.fetch(new Message[] { message }, fp, null); + + // Check mime structure... + Body body = message.getBody(); + assertTrue(body instanceof MimeMultipart); + MimeMultipart mimeMultipart = (MimeMultipart) body; + assertEquals(3, mimeMultipart.getCount()); + assertEquals("mixed", mimeMultipart.getSubTypeForTest()); + + Part part0 = mimeMultipart.getBodyPart(0); + Part part1 = mimeMultipart.getBodyPart(1); + Part part2 = mimeMultipart.getBodyPart(2); + assertTrue(part0 instanceof MimeBodyPart); + assertTrue(part1 instanceof MimeBodyPart); + assertTrue(part2 instanceof MimeBodyPart); + + MimeBodyPart mimePart0 = (MimeBodyPart) part0; // text/plain + MimeBodyPart mimePart1 = (MimeBodyPart) part1; // image/png + MimeBodyPart mimePart2 = (MimeBodyPart) part2; // text/html + + MoreAsserts.assertEquals( + new String[] {"text/plain;\n CHARSET=\"ISO-8859-1\""}, + part0.getHeader("Content-Type") + ); + MoreAsserts.assertEquals( + new String[] {"image/png;\n NAME=\"device.png\""}, + part1.getHeader("Content-Type") + ); + MoreAsserts.assertEquals( + new String[] {"text/html"}, + part2.getHeader("Content-Type") + ); + + MoreAsserts.assertEquals( + new String[] {"CID"}, + part0.getHeader("Content-ID") + ); + assertNull( + part1.getHeader("Content-ID") + ); + assertNull( + part2.getHeader("Content-ID") + ); + + MoreAsserts.assertEquals( + new String[] {"7BIT"}, + part0.getHeader("Content-Transfer-Encoding") + ); + MoreAsserts.assertEquals( + new String[] {"BASE64"}, + part1.getHeader("Content-Transfer-Encoding") + ); + MoreAsserts.assertEquals( + new String[] {"7BIT"}, + part2.getHeader("Content-Transfer-Encoding") + ); + + MoreAsserts.assertEquals( + new String[] {";\n size=18"}, // TODO Is that right? + part0.getHeader("Content-Disposition") + ); + MoreAsserts.assertEquals( + new String[] {"attachment;\n filename=\"device.png\";\n size=117840"}, + part1.getHeader("Content-Disposition") + ); + MoreAsserts.assertEquals( + new String[] {"attachment;\n filename=\"attachment.html\";\n size=\"555\""}, + part2.getHeader("Content-Disposition") + ); + + // TODO Test for quote: If a filename contains ", it should become %22, + // which isn't implemented. + } + + public void testFetchBodySane() throws MessagingException { + final MockTransport mock = openAndInjectMockTransport(); + setupOpenFolder(mock); + mFolder.open(OpenMode.READ_WRITE, null); + final Message message = mFolder.createMessage("1"); + + final FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY_SANE); + mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[\\]<0.51200>\\)", + new String[] { + "* 9 FETCH (UID 1 BODY[] {23}", + "from: a@b.com", // 15 bytes + "", // 2 + "test", // 6 + ")", + getNextTag(true) + " OK SUCCESS" + }); + mFolder.fetch(new Message[] { message }, fp, null); + assertEquals("a@b.com", message.getHeader("from")[0]); + } + + public void testFetchBody() throws MessagingException { + final MockTransport mock = openAndInjectMockTransport(); + setupOpenFolder(mock); + mFolder.open(OpenMode.READ_WRITE, null); + final Message message = mFolder.createMessage("1"); + + final FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY); + mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[\\]\\)", + new String[] { + "* 9 FETCH (UID 1 BODY[] {23}", + "from: a@b.com", // 15 bytes + "", // 2 + "test", // 6 + ")", + getNextTag(true) + " OK SUCCESS" + }); + mFolder.fetch(new Message[] { message }, fp, null); + assertEquals("a@b.com", message.getHeader("from")[0]); + } + + public void testFetchAttachment() throws Exception { + MockTransport mock = openAndInjectMockTransport(); + setupOpenFolder(mock); + mFolder.open(OpenMode.READ_WRITE, null); + final Message message = mFolder.createMessage("1"); + + final FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.STRUCTURE); + mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODYSTRUCTURE\\)", + new String[] { + "* 9 FETCH (UID 1 BODYSTRUCTURE ((\"TEXT\" \"PLAIN\" (\"CHARSET\" \"ISO-8859-1\")" + + " CID NIL \"7BIT\" 18 3 NIL NIL NIL)" + + "(\"IMAGE\" \"PNG\"" + + " (\"NAME\" \"device.png\") NIL NIL \"BASE64\" 117840 NIL (\"ATTACHMENT\"" + + "(\"FILENAME\" \"device.png\")) NIL)" + + "\"MIXED\"))", + getNextTag(true) + " OK SUCCESS" + }); + mFolder.fetch(new Message[] { message }, fp, null); + + // Check mime structure, and get the second part. + Body body = message.getBody(); + assertTrue(body instanceof MimeMultipart); + MimeMultipart mimeMultipart = (MimeMultipart) body; + assertEquals(2, mimeMultipart.getCount()); + + Part part1 = mimeMultipart.getBodyPart(1); + assertTrue(part1 instanceof MimeBodyPart); + MimeBodyPart mimePart1 = (MimeBodyPart) part1; + + // Fetch the second part + fp.clear(); + fp.add(mimePart1); + mock.expect(getNextTag(false) + " UID FETCH 1 \\(UID BODY.PEEK\\[2\\]\\)", + new String[] { + "* 9 FETCH (UID 1 BODY[2] {4}", + "YWJj)", // abc in base64 + getNextTag(true) + " OK SUCCESS" + }); + mFolder.fetch(new Message[] { message }, fp, null); + + assertEquals("abc", + Utility.fromUtf8(IOUtils.toByteArray(mimePart1.getBody().getInputStream()))); + } + /** * Test for proper operations on servers that return "NIL" for empty message bodies. */ diff --git a/tests/src/com/android/email/mail/transport/MockTransport.java b/tests/src/com/android/email/mail/transport/MockTransport.java index f59330ba4..a54424b2c 100644 --- a/tests/src/com/android/email/mail/transport/MockTransport.java +++ b/tests/src/com/android/email/mail/transport/MockTransport.java @@ -39,12 +39,12 @@ public class MockTransport implements Transport { // All flags defining debug or development code settings must be FALSE // when code is checked in or released. private static boolean DEBUG_LOG_STREAMS = true; - + private static String LOG_TAG = "MockTransport"; private boolean mSslAllowed = false; private boolean mTlsAllowed = false; - + private boolean mOpen; private boolean mInputOpen; private int mConnectionSecurity; @@ -58,23 +58,23 @@ public class MockTransport implements Transport { public static final int ACTION_INJECT_TEXT = 0; public static final int ACTION_SERVER_CLOSE = 1; public static final int ACTION_CLIENT_CLOSE = 2; - + int mAction; String mPattern; String[] mResponses; - + Transaction(String pattern, String[] responses) { mAction = ACTION_INJECT_TEXT; mPattern = pattern; mResponses = responses; } - + Transaction(int otherType) { mAction = otherType; mPattern = null; mResponses = null; } - + @Override public String toString() { switch (mAction) { @@ -89,7 +89,7 @@ public class MockTransport implements Transport { } } } - + private ArrayList mPairs = new ArrayList(); /** @@ -108,7 +108,7 @@ public class MockTransport implements Transport { public void expect(String pattern, String response) { expect(pattern, (response == null) ? null : new String[] { response }); } - + /** * Give the mock a pattern to wait for and a multi-line response to send back. * @param pattern Java RegEx to wait for @@ -127,7 +127,7 @@ public class MockTransport implements Transport { expect("^" + Pattern.quote(literal) + "$", responses); } - /** + /** * Tell the Mock Transport that we expect it to be closed. This will preserve * the remaining entries in the expect() stream and allow us to "ride over" the close (which * would normally reset everything). @@ -135,7 +135,7 @@ public class MockTransport implements Transport { public void expectClose() { mPairs.add(new Transaction(Transaction.ACTION_CLIENT_CLOSE)); } - + private void sendResponse(String[] responses) { for (String s : responses) { mQueuedInput.add(s); @@ -145,7 +145,7 @@ public class MockTransport implements Transport { public boolean canTrySslSecurity() { return (mConnectionSecurity == CONNECTION_SECURITY_SSL); } - + public boolean canTryTlsSecurity() { return (mConnectionSecurity == Transport.CONNECTION_SECURITY_TLS); } @@ -155,7 +155,7 @@ public class MockTransport implements Transport { } /** - * This simulates a condition where the server has closed its side, causing + * This simulates a condition where the server has closed its side, causing * reads to fail. */ public void closeInputStream() { @@ -199,7 +199,7 @@ public class MockTransport implements Transport { /** * This normally serves as a pseudo-clone, for use by Imap. For the purposes of unit testing, - * until we need something more complex, we'll just return the actual MockTransport. Then we + * until we need something more complex, we'll just return the actual MockTransport. Then we * don't have to worry about dealing with test metadata like the expects list or socket state. */ public Transport newInstanceWithConfiguration() { @@ -239,9 +239,9 @@ public class MockTransport implements Transport { * from the mQueuedInput list, but if the list is empty, we also peek the expect list. This * supports banners, multi-line responses, and any other cases where we respond without * a specific expect pattern. - * + * * If no response text is available, we assert (failing our test) as an underflow. - * + * * Logs the read text if DEBUG_LOG_STREAMS is true. */ public String readLine() throws IOException { @@ -251,7 +251,7 @@ public class MockTransport implements Transport { } // if there's nothing to read, see if we can find a null-pattern response if (0 == mQueuedInput.size()) { - Transaction pair = mPairs.get(0); + Transaction pair = mPairs.size() > 0 ? mPairs.get(0) : null; if (pair != null && pair.mPattern == null) { mPairs.remove(0); sendResponse(pair.mResponses); @@ -278,7 +278,7 @@ public class MockTransport implements Transport { public void setSoTimeout(int timeoutMilliseconds) /* throws SocketException */ { } - + public void setUri(URI uri, int defaultPort) { SmtpSenderUnitTests.assertTrue("Don't call setUri on a mock transport", false); } @@ -289,7 +289,7 @@ public class MockTransport implements Transport { * If the string was expected, we push the corresponding responses into the mQueuedInput * list, for subsequent calls to readLine(). If the string does not match, we assert * the mismatch. If no string was expected, we assert it as an overflow. - * + * * Logs the written text if DEBUG_LOG_STREAMS is true. */ public void writeLine(String s, String sensitiveReplacement) /* throws IOException */ { @@ -307,7 +307,7 @@ public class MockTransport implements Transport { sendResponse(pair.mResponses); } } - + /** * This is an InputStream that satisfies the needs of getInputStream() */ @@ -315,7 +315,7 @@ public class MockTransport implements Transport { byte[] mNextLine = null; int mNextIndex = 0; - + /** * Reads from the same input buffer as readLine() */ @@ -324,11 +324,11 @@ public class MockTransport implements Transport { if (!mInputOpen) { throw new IOException(); } - + if (mNextLine != null && mNextIndex < mNextLine.length) { return mNextLine[mNextIndex++]; } - + // previous line was exhausted so try to get another one String next = readLine(); if (next == null) { @@ -340,12 +340,12 @@ public class MockTransport implements Transport { if (mNextLine != null && mNextIndex < mNextLine.length) { return mNextLine[mNextIndex++]; } - - // no joy - throw an exception - throw new IOException(); + + // no joy - throw an exception + throw new IOException(); } } - + /** * This is an OutputStream that satisfies the needs of getOutputStream() */