package com.fsck.k9.mail.store.imap;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.fsck.k9.mail.K9LibRobolectricTestRunner;
import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.filter.PeekableInputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(K9LibRobolectricTestRunner.class)
public class ImapResponseParserTest {
private PeekableInputStream peekableInputStream;
@Test
public void testSimpleOkResponse() throws IOException {
ImapResponseParser parser = createParser("* OK\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
assertEquals(1, response.size());
assertEquals("OK", response.get(0));
}
@Test
public void testOkResponseWithText() throws IOException {
ImapResponseParser parser = createParser("* OK Some text here\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
assertEquals(2, response.size());
assertEquals("OK", response.get(0));
assertEquals("Some text here", response.get(1));
}
@Test
public void testOkResponseWithRespTextCode() throws IOException {
ImapResponseParser parser = createParser("* OK [UIDVALIDITY 3857529045]\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
assertEquals(2, response.size());
assertEquals("OK", response.get(0));
assertTrue(response.get(1) instanceof ImapList);
ImapList respTextCode = (ImapList) response.get(1);
assertEquals(2, respTextCode.size());
assertEquals("UIDVALIDITY", respTextCode.get(0));
assertEquals("3857529045", respTextCode.get(1));
}
@Test
public void testOkResponseWithRespTextCodeAndText() throws IOException {
ImapResponseParser parser = createParser("* OK [token1 token2] {x} test [...]\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
assertEquals(3, response.size());
assertEquals("OK", response.get(0));
assertTrue(response.get(1) instanceof ImapList);
assertEquals("{x} test [...]", response.get(2));
ImapList respTextCode = (ImapList) response.get(1);
assertEquals(2, respTextCode.size());
assertEquals("token1", respTextCode.get(0));
assertEquals("token2", respTextCode.get(1));
}
@Test
public void testReadStatusResponseWithOKResponse() throws Exception {
ImapResponseParser parser = createParser("* COMMAND BAR\tBAZ\r\n" +
"TAG OK COMMAND completed\r\n");
List<ImapResponse> responses = parser.readStatusResponse("TAG", null, null, null);
assertEquals(2, responses.size());
assertEquals(asList("COMMAND", "BAR", "BAZ"), responses.get(0));
assertEquals(asList("OK", "COMMAND completed"), responses.get(1));
}
@Test
public void testReadStatusResponseUntaggedHandlerGetsUntaggedOnly() throws Exception {
ImapResponseParser parser = createParser(
"* UNTAGGED\r\n" +
"A2 OK COMMAND completed\r\n");
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
parser.readStatusResponse("A2", null, null, untaggedHandler);
assertEquals(1, untaggedHandler.responses.size());
assertEquals(asList("UNTAGGED"), untaggedHandler.responses.get(0));
}
@Test
public void testReadStatusResponseSkippingWrongTag() throws Exception {
ImapResponseParser parser = createParser("* UNTAGGED\r\n" +
"* 0 EXPUNGE\r\n" +
"* 42 EXISTS\r\n" +
"A1 COMMAND BAR BAZ\r\n" +
"A2 OK COMMAND completed\r\n");
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
List<ImapResponse> responses = parser.readStatusResponse("A2", null, null, untaggedHandler);
assertEquals(3, responses.size());
assertEquals(asList("0", "EXPUNGE"), responses.get(0));
assertEquals(asList("42", "EXISTS"), responses.get(1));
assertEquals(asList("OK", "COMMAND completed"), responses.get(2));
assertEquals(asList("UNTAGGED"), untaggedHandler.responses.get(0));
assertEquals(responses.get(0), untaggedHandler.responses.get(1));
assertEquals(responses.get(1), untaggedHandler.responses.get(2));
}
@Test
public void testReadStatusResponseUntaggedHandlerStillCalledOnNegativeReply() throws Exception {
ImapResponseParser parser = createParser(
"+ text\r\n" +
"A2 NO Bad response\r\n");
TestUntaggedHandler untaggedHandler = new TestUntaggedHandler();
try {
List<ImapResponse> responses = parser.readStatusResponse("A2", null, null, untaggedHandler);
} catch (NegativeImapResponseException e) {
}
assertEquals(1, untaggedHandler.responses.size());
assertEquals(asList("text"), untaggedHandler.responses.get(0));
}
@Test(expected = NegativeImapResponseException.class)
public void testReadStatusResponseWithErrorResponse() throws Exception {
ImapResponseParser parser = createParser("* COMMAND BAR BAZ\r\nTAG ERROR COMMAND errored\r\n");
parser.readStatusResponse("TAG", null, null, null);
}
@Test
public void testRespTextCodeWithList() throws Exception {
ImapResponseParser parser = createParser("* OK [PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen " +
"\\Draft NonJunk $MDNSent \\*)] Flags permitted.\r\n");
ImapResponse response = parser.readResponse();
assertEquals(3, response.size());
assertTrue(response.get(1) instanceof ImapList);
assertEquals(2, response.getList(1).size());
assertEquals("PERMANENTFLAGS", response.getList(1).getString(0));
assertTrue(response.getList(1).get(1) instanceof ImapList);
assertEquals("\\Answered", response.getList(1).getList(1).getString(0));
assertEquals("\\Flagged", response.getList(1).getList(1).getString(1));
assertEquals("\\Deleted", response.getList(1).getList(1).getString(2));
assertEquals("\\Seen", response.getList(1).getList(1).getString(3));
assertEquals("\\Draft", response.getList(1).getList(1).getString(4));
assertEquals("NonJunk", response.getList(1).getList(1).getString(5));
assertEquals("$MDNSent", response.getList(1).getList(1).getString(6));
assertEquals("\\*", response.getList(1).getList(1).getString(7));
}
@Test
public void testExistsResponse() throws Exception {
ImapResponseParser parser = createParser("* 23 EXISTS\r\n");
ImapResponse response = parser.readResponse();
assertEquals(2, response.size());
assertEquals(23, response.getNumber(0));
assertEquals("EXISTS", response.getString(1));
}
@Test(expected = IOException.class)
public void testReadStringUntilEndOfStream() throws IOException {
ImapResponseParser parser = createParser("* OK Some text ");
parser.readResponse();
}
@Test
public void testCommandContinuation() throws Exception {
ImapResponseParser parser = createParser("+ Ready for additional command text\r\n");
ImapResponse response = parser.readResponse();
assertEquals(1, response.size());
assertEquals("Ready for additional command text", response.getString(0));
}
@Test
public void testParseLiteral() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
ImapResponse response = parser.readResponse();
assertEquals(1, response.size());
assertEquals("test", response.getString(0));
}
@Test
public void testParseLiteralWithEmptyString() throws Exception {
ImapResponseParser parser = createParser("* {0}\r\n\r\n");
ImapResponse response = parser.readResponse();
assertEquals(1, response.size());
assertEquals("", response.getString(0));
}
@Test(expected = IOException.class)
public void testParseLiteralToEndOfStream() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\nabc");
parser.readResponse();
}
@Test
public void testParseLiteralWithConsumingCallbackReturningNull() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(4, "cheeseburger");
ImapResponse response = parser.readResponse(callback);
assertEquals(1, response.size());
assertEquals("cheeseburger", response.getString(0));
}
@Test
public void testParseLiteralWithNonConsumingCallbackReturningNull() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(0, null);
ImapResponse response = parser.readResponse(callback);
assertEquals(1, response.size());
assertEquals("test", response.getString(0));
assertTrue(callback.foundLiteralCalled);
assertAllInputConsumed();
}
@Test
public void readResponse_withPartlyConsumingCallbackReturningNull_shouldThrow() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(2, null);
try {
parser.readResponse(callback);
fail();
} catch (AssertionError e) {
assertEquals("Callback consumed some data but returned no result", e.getMessage());
}
}
@Test
public void readResponse_withPartlyConsumingCallbackThatThrows_shouldReadAllDataAndThrow() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(2);
try {
parser.readResponse(callback);
fail();
} catch (ImapResponseParserException e) {
assertEquals("readResponse(): Exception in callback method", e.getMessage());
assertEquals(ImapResponseParserTestException.class, e.getCause().getClass());
}
assertAllInputConsumed();
}
@Test
public void readResponse_withCallbackThatThrowsRepeatedly_shouldConsumeAllInputAndThrowFirstException()
throws Exception {
ImapResponseParser parser = createParser("* {3}\r\none {3}\r\ntwo\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(3);
try {
parser.readResponse(callback);
fail();
} catch (ImapResponseParserException e) {
assertEquals("readResponse(): Exception in callback method", e.getMessage());
assertEquals(ImapResponseParserTestException.class, e.getCause().getClass());
assertEquals(0, ((ImapResponseParserTestException) e.getCause()).instanceNumber);
}
assertAllInputConsumed();
}
@Test
public void testParseLiteralWithIncompleteConsumingCallbackReturningString() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = TestImapResponseCallback.readBytesAndReturn(2, "ninja");
ImapResponse response = parser.readResponse(callback);
assertEquals(1, response.size());
assertEquals("ninja", response.getString(0));
assertAllInputConsumed();
}
@Test
public void testParseLiteralWithThrowingCallback() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
ImapResponseCallback callback = TestImapResponseCallback.readBytesAndThrow(0);
try {
parser.readResponse(callback);
fail();
} catch (ImapResponseParserException e) {
assertEquals("readResponse(): Exception in callback method", e.getMessage());
}
assertAllInputConsumed();
}
@Test(expected = IOException.class)
public void testParseLiteralWithCallbackThrowingIOException() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
ImapResponseCallback callback = new ImapResponseCallback() {
@Override
public Object foundLiteral(ImapResponse response, FixedLengthInputStream literal) throws Exception {
throw new IOException();
}
};
parser.readResponse(callback);
}
@Test
public void testParseQuoted() throws Exception {
ImapResponseParser parser = createParser("* \"qu\\\"oted\"\r\n");
ImapResponse response = parser.readResponse();
assertEquals(1, response.size());
assertEquals("qu\"oted", response.getString(0));
}
@Test(expected = IOException.class)
public void testParseQuotedToEndOfStream() throws Exception {
ImapResponseParser parser = createParser("* \"abc");
parser.readResponse();
}
@Test(expected = IOException.class)
public void testParseAtomToEndOfStream() throws Exception {
ImapResponseParser parser = createParser("* abc");
parser.readResponse();
}
@Test(expected = IOException.class)
public void testParseUntaggedResponseWithoutSpace() throws Exception {
ImapResponseParser parser = createParser("*\r\n");
parser.readResponse();
}
@Test
public void testListResponseContainingFolderNameWithBrackets() throws Exception {
ImapResponseParser parser = createParser("* LIST (\\HasNoChildren) \".\" [FolderName]\r\n");
ImapResponse response = parser.readResponse();
assertEquals(4, response.size());
assertEquals("LIST", response.get(0));
assertEquals(1, response.getList(1).size());
assertEquals("\\HasNoChildren", response.getList(1).getString(0));
assertEquals(".", response.get(2));
assertEquals("[FolderName]", response.get(3));
}
@Test(expected = IOException.class)
public void testListResponseContainingFolderNameContainingBracketsThrowsException() throws Exception {
ImapResponseParser parser = createParser(
"* LIST (\\NoInferiors) \"/\" Root/Folder/Subfolder()\r\n");
parser.readResponse();
}
@Test
public void readResponseShouldReadWholeListResponseLine() throws Exception {
ImapResponseParser parser = createParser("* LIST (\\HasNoChildren) \".\" [FolderName]\r\n" +
"TAG OK [List complete]\r\n");
parser.readResponse();
ImapResponse responseTwo = parser.readResponse();
assertEquals("TAG", responseTwo.getTag());
}
@Test
public void readResponse_withListResponseContainingNil() throws Exception {
ImapResponseParser parser = createParser("* LIST (\\NoInferiors) NIL INBOX\r\n");
ImapResponse response = parser.readResponse();
assertEquals(4, response.size());
assertEquals("LIST", response.get(0));
assertEquals(1, response.getList(1).size());
assertEquals("\\NoInferiors", response.getList(1).getString(0));
assertEquals(null, response.get(2));
assertEquals("INBOX", response.get(3));
}
@Test
public void readResponse_withListAsFirstToken_shouldThrow() throws Exception {
ImapResponseParser parser = createParser("* [1 2] 3\r\n");
try {
parser.readResponse();
fail("Expected exception");
} catch (IOException e) {
assertEquals("Unexpected non-string token: ImapList - [1, 2]", e.getMessage());
}
}
@Test
public void testFetchResponse() throws Exception {
ImapResponseParser parser = createParser("* 1 FETCH (" +
"UID 23 " +
"INTERNALDATE \"01-Jul-2015 12:34:56 +0200\" " +
"RFC822.SIZE 3456 " +
"BODY[HEADER.FIELDS (date subject from)] \"<headers>\" " +
"FLAGS (\\Seen))\r\n");
ImapResponse response = parser.readResponse();
assertEquals(3, response.size());
assertEquals("1", response.getString(0));
assertEquals("FETCH", response.getString(1));
assertEquals("UID", response.getList(2).getString(0));
assertEquals(23, response.getList(2).getNumber(1));
assertEquals("INTERNALDATE", response.getList(2).getString(2));
assertEquals("01-Jul-2015 12:34:56 +0200", response.getList(2).getString(3));
assertEquals("RFC822.SIZE", response.getList(2).getString(4));
assertEquals(3456, response.getList(2).getNumber(5));
assertEquals("BODY", response.getList(2).getString(6));
assertEquals(2, response.getList(2).getList(7).size());
assertEquals("HEADER.FIELDS", response.getList(2).getList(7).getString(0));
assertEquals(3, response.getList(2).getList(7).getList(1).size());
assertEquals("date", response.getList(2).getList(7).getList(1).getString(0));
assertEquals("subject", response.getList(2).getList(7).getList(1).getString(1));
assertEquals("from", response.getList(2).getList(7).getList(1).getString(2));
assertEquals("<headers>", response.getList(2).getString(8));
assertEquals("FLAGS", response.getList(2).getString(9));
assertEquals(1, response.getList(2).getList(10).size());
assertEquals("\\Seen", response.getList(2).getList(10).getString(0));
}
@Test
public void readStatusResponse_withNoResponse_shouldThrow() throws Exception {
ImapResponseParser parser = createParser("1 NO\r\n");
try {
parser.readStatusResponse("1", "COMMAND", "[logId]", null);
fail("Expected exception");
} catch (NegativeImapResponseException e) {
assertEquals("Command: COMMAND; response: #1# [NO]", e.getMessage());
}
}
@Test
public void readStatusResponse_withNoResponseAndAlertText_shouldThrowWithAlertText() throws Exception {
ImapResponseParser parser = createParser("1 NO [ALERT] Access denied\r\n");
try {
parser.readStatusResponse("1", "COMMAND", "[logId]", null);
fail("Expected exception");
} catch (NegativeImapResponseException e) {
assertEquals("Access denied", e.getAlertText());
}
}
private ImapResponseParser createParser(String response) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(response.getBytes());
peekableInputStream = new PeekableInputStream(byteArrayInputStream);
return new ImapResponseParser(peekableInputStream);
}
private void assertAllInputConsumed() throws IOException {
assertEquals(0, peekableInputStream.available());
}
static class TestImapResponseCallback implements ImapResponseCallback {
private final int readNumberOfBytes;
private final Object returnValue;
private final boolean throwException;
private int exceptionCount = 0;
public boolean foundLiteralCalled = false;
public static TestImapResponseCallback readBytesAndReturn(int readNumberOfBytes, Object returnValue) {
return new TestImapResponseCallback(readNumberOfBytes, returnValue, false);
}
public static TestImapResponseCallback readBytesAndThrow(int readNumberOfBytes) {
return new TestImapResponseCallback(readNumberOfBytes, null, true);
}
private TestImapResponseCallback(int readNumberOfBytes, Object returnValue, boolean throwException) {
this.readNumberOfBytes = readNumberOfBytes;
this.returnValue = returnValue;
this.throwException = throwException;
}
@Override
public Object foundLiteral(ImapResponse response, FixedLengthInputStream literal) throws Exception {
foundLiteralCalled = true;
int skipBytes = readNumberOfBytes;
while (skipBytes > 0) {
long skippedBytes = literal.skip(skipBytes);
skipBytes -= skippedBytes;
}
if (throwException) {
throw new ImapResponseParserTestException(exceptionCount++);
}
return returnValue;
}
}
static class ImapResponseParserTestException extends RuntimeException {
public final int instanceNumber;
public ImapResponseParserTestException(int instanceNumber) {
this.instanceNumber = instanceNumber;
}
}
static class TestUntaggedHandler implements UntaggedHandler {
public final List<ImapResponse> responses = new ArrayList<ImapResponse>();
@Override
public void handleAsyncUntaggedResponse(ImapResponse response) {
responses.add(response);
}
}
}