Merge pull request #825 from k9mail/GH-786_fix_parsing_for_folders_with_brackets

Fix parsing IMAP LIST responses containing folder names with brackets
This commit is contained in:
cketti 2015-10-11 11:47:30 +02:00
commit e6c52d3580
4 changed files with 553 additions and 196 deletions

View file

@ -44,7 +44,7 @@ public class ImapList extends ArrayList<Object> {
return getDate(getString(index));
}
public Date getKeyedDate(Object key) throws MessagingException {
public Date getKeyedDate(String key) throws MessagingException {
return getDate(getKeyedString(key));
}
@ -60,7 +60,7 @@ public class ImapList extends ArrayList<Object> {
}
public Object getKeyedValue(Object key) {
public Object getKeyedValue(String key) {
for (int i = 0, count = size() - 1; i < count; i++) {
if (ImapResponseParser.equalsIgnoreCase(get(i), key)) {
return get(i + 1);
@ -69,34 +69,34 @@ public class ImapList extends ArrayList<Object> {
return null;
}
public ImapList getKeyedList(Object key) {
public ImapList getKeyedList(String key) {
return (ImapList)getKeyedValue(key);
}
public String getKeyedString(Object key) {
public String getKeyedString(String key) {
return (String)getKeyedValue(key);
}
public int getKeyedNumber(Object key) {
public int getKeyedNumber(String key) {
return Integer.parseInt(getKeyedString(key));
}
public boolean containsKey(Object key) {
public boolean containsKey(String key) {
if (key == null) {
return false;
}
for (int i = 0, count = size() - 1; i < count; i++) {
if (ImapResponseParser.equalsIgnoreCase(key, get(i))) {
if (ImapResponseParser.equalsIgnoreCase(get(i), key)) {
return true;
}
}
return false;
}
public int getKeyIndex(Object key) {
public int getKeyIndex(String key) {
for (int i = 0, count = size() - 1; i < count; i++) {
if (ImapResponseParser.equalsIgnoreCase(key, get(i))) {
if (ImapResponseParser.equalsIgnoreCase(get(i), key)) {
return i;
}
}

View file

@ -8,40 +8,51 @@ package com.fsck.k9.mail.store.imap;
* object will contain all of the available tokens at the time the response is received.
* </p>
*/
public class ImapResponse extends ImapList {
class ImapResponse extends ImapList {
private static final long serialVersionUID = 6886458551615975669L;
private ImapResponseCallback mCallback;
private final boolean mCommandContinuationRequested;
private final String mTag;
private ImapResponseCallback callback;
private final boolean commandContinuationRequested;
private final String tag;
public ImapResponse(ImapResponseCallback callback,
boolean mCommandContinuationRequested, String mTag) {
this.mCallback = callback;
this.mCommandContinuationRequested = mCommandContinuationRequested;
this.mTag = mTag;
private ImapResponse(ImapResponseCallback callback, boolean commandContinuationRequested, String tag) {
this.callback = callback;
this.commandContinuationRequested = commandContinuationRequested;
this.tag = tag;
}
public static ImapResponse newContinuationRequest(ImapResponseCallback callback) {
return new ImapResponse(callback, true, null);
}
public static ImapResponse newUntaggedResponse(ImapResponseCallback callback) {
return new ImapResponse(callback, false, null);
}
public static ImapResponse newTaggedResponse(ImapResponseCallback callback, String tag) {
return new ImapResponse(callback, false, tag);
}
public boolean isContinuationRequested() {
return mCommandContinuationRequested;
return commandContinuationRequested;
}
public String getTag() {
return mTag;
return tag;
}
public ImapResponseCallback getCallback() {
return mCallback;
return callback;
}
public void setCallback(ImapResponseCallback mCallback) {
this.mCallback = mCallback;
public void setCallback(ImapResponseCallback callback) {
this.callback = callback;
}
public String getAlertText() {
if (size() > 1 && ImapResponseParser.equalsIgnoreCase("[ALERT]", get(1))) {
if (size() > 1 && ImapResponseParser.equalsIgnoreCase(get(1), "[ALERT]")) {
StringBuilder sb = new StringBuilder();
for (int i = 2, count = size(); i < count; i++) {
sb.append(get(i).toString());
@ -55,6 +66,6 @@ public class ImapResponse extends ImapList {
@Override
public String toString() {
return "#" + (mCommandContinuationRequested ? "+" : mTag) + "# " + super.toString();
return "#" + (commandContinuationRequested ? "+" : tag) + "# " + super.toString();
}
}

View file

@ -1,12 +1,6 @@
package com.fsck.k9.mail.store.imap;
import android.text.TextUtils;
import android.util.Log;
import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.filter.PeekableInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
@ -15,17 +9,27 @@ import java.util.List;
import java.util.Locale;
import java.util.Set;
import android.text.TextUtils;
import android.util.Log;
import com.fsck.k9.mail.K9MailLib;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.filter.PeekableInputStream;
import static com.fsck.k9.mail.K9MailLib.DEBUG_PROTOCOL_IMAP;
import static com.fsck.k9.mail.K9MailLib.LOG_TAG;
import static com.fsck.k9.mail.store.imap.ImapCommands.CAPABILITY_CAPABILITY;
class ImapResponseParser {
private PeekableInputStream mIn;
private ImapResponse mResponse;
private Exception mException;
private PeekableInputStream inputStream;
private ImapResponse response;
private Exception exception;
public ImapResponseParser(PeekableInputStream in) {
this.mIn = in;
this.inputStream = in;
}
public ImapResponse readResponse() throws IOException {
@ -33,87 +37,107 @@ class ImapResponseParser {
}
/**
* Reads the next response available on the stream and returns an
* ImapResponse object that represents it.
* Reads the next response available on the stream and returns an {@code ImapResponse} object that represents it.
*/
public ImapResponse readResponse(ImapResponseCallback callback) throws IOException {
try {
int ch = mIn.peek();
if (ch == '*') {
parseUntaggedResponse();
mResponse = new ImapResponse(callback, false, null);
readTokens(mResponse);
} else if (ch == '+') {
mResponse = new ImapResponse(callback, parseCommandContinuationRequest(), null);
parseResponseText(mResponse);
int peek = inputStream.peek();
if (peek == '+') {
readContinuationRequest(callback);
} else if (peek == '*') {
readUntaggedResponse(callback);
} else {
mResponse = new ImapResponse(callback, false, parseTaggedResponse());
readTokens(mResponse);
readTaggedResponse(callback);
}
if (mException != null) {
throw new RuntimeException("readResponse(): Exception in callback method", mException);
if (exception != null) {
throw new RuntimeException("readResponse(): Exception in callback method", exception);
}
return mResponse;
return response;
} finally {
mResponse = null;
mException = null;
response = null;
exception = null;
}
}
protected List<ImapResponse> readStatusResponse(String tag,
String commandToLog,
String logId,
UntaggedHandler untaggedHandler)
throws IOException, MessagingException {
private void readContinuationRequest(ImapResponseCallback callback) throws IOException {
parseCommandContinuationRequest();
response = ImapResponse.newContinuationRequest(callback);
skipIfSpace();
String rest = readStringUntilEndOfLine();
response.add(rest);
}
private void readUntaggedResponse(ImapResponseCallback callback) throws IOException {
parseUntaggedResponse();
response = ImapResponse.newUntaggedResponse(callback);
readTokens(response);
}
private void readTaggedResponse(ImapResponseCallback callback) throws IOException {
String tag = parseTaggedResponse();
response = ImapResponse.newTaggedResponse(callback, tag);
readTokens(response);
}
List<ImapResponse> readStatusResponse(String tag, String commandToLog, String logId,
UntaggedHandler untaggedHandler) throws IOException, MessagingException {
List<ImapResponse> responses = new ArrayList<ImapResponse>();
ImapResponse response;
do {
response = readResponse();
if (K9MailLib.isDebug() && DEBUG_PROTOCOL_IMAP) {
Log.v(LOG_TAG, logId + "<<<" + response);
}
if (response.getTag() != null && !response.getTag().equalsIgnoreCase(tag)) {
Log.w(LOG_TAG, "After sending tag " + tag + ", got tag response from previous command " + response + " for " + logId);
Log.w(LOG_TAG, "After sending tag " + tag + ", got tag response from previous command " + response +
" for " + logId);
Iterator<ImapResponse> iter = responses.iterator();
Iterator<ImapResponse> responseIterator = responses.iterator();
while (iter.hasNext()) {
ImapResponse delResponse = iter.next();
if (delResponse.getTag() != null
|| delResponse.size() < 2
|| (!equalsIgnoreCase(delResponse.get(1), "EXISTS") &&
!equalsIgnoreCase(delResponse.get(1), "EXPUNGE"))) {
iter.remove();
while (responseIterator.hasNext()) {
ImapResponse delResponse = responseIterator.next();
if (delResponse.getTag() != null || delResponse.size() < 2 || (
!equalsIgnoreCase(delResponse.get(1), "EXISTS") &&
!equalsIgnoreCase(delResponse.get(1), "EXPUNGE"))) {
responseIterator.remove();
}
}
response = null;
continue;
}
if (untaggedHandler != null) {
untaggedHandler.handleAsyncUntaggedResponse(response);
}
responses.add(response);
responses.add(response);
} while (response == null || response.getTag() == null);
if (response.size() < 1 || !equalsIgnoreCase(response.get(0), "OK")) {
throw new ImapException("Command: " + commandToLog + "; response: " + response.toString(), response.getAlertText());
throw new ImapException("Command: " + commandToLog + "; response: " + response.toString(),
response.getAlertText());
}
return responses;
}
protected static Set<String> parseCapabilities(List<ImapResponse> responses) {
static Set<String> parseCapabilities(List<ImapResponse> responses) {
HashSet<String> capabilities = new HashSet<String>();
for (ImapResponse response : responses) {
ImapList list = null;
if (!response.isEmpty() && equalsIgnoreCase(response.get(0), "OK")) {
for (Object thisPart : response) {
if (thisPart instanceof ImapList) {
ImapList thisList = (ImapList)thisPart;
ImapList thisList = (ImapList) thisPart;
if (equalsIgnoreCase(thisList.get(0), CAPABILITY_CAPABILITY)) {
list = thisList;
break;
@ -124,11 +148,11 @@ class ImapResponseParser {
list = response;
}
if (list != null && list.size() > 1 &&
equalsIgnoreCase(list.get(0), CAPABILITY_CAPABILITY)) {
for (Object capability : list.subList(1, list.size())) {
if (capability instanceof String) {
capabilities.add(((String)capability).toUpperCase(Locale.US));
if (list != null && list.size() > 1 && equalsIgnoreCase(list.get(0), CAPABILITY_CAPABILITY)) {
for (Object listItem : list.subList(1, list.size())) {
if (listItem instanceof String) {
String capability = (String) listItem;
capabilities.add(capability.toUpperCase(Locale.US));
}
}
}
@ -144,6 +168,8 @@ class ImapResponseParser {
if (isStatusResponse(firstToken)) {
parseResponseText(response);
} else if (equalsIgnoreCase(firstToken, "LIST")) {
parseListResponse(response);
} else {
Object token;
while ((token = readToken(response)) != null) {
@ -156,7 +182,6 @@ class ImapResponseParser {
/**
* Parse {@code resp-text} tokens
*
* <p>
* Responses "OK", "PREAUTH", "BYE", "NO", "BAD", and continuation request responses can
* contain {@code resp-text} tokens. We parse the {@code resp-text-code} part as tokens and
@ -175,21 +200,20 @@ class ImapResponseParser {
* The {@link ImapResponse} instance that holds the parsed tokens of the response.
*
* @throws IOException
* If there's a network error.
* If there's a network error.
*
* @see #isStatusResponse(String)
*/
private void parseResponseText(ImapResponse parent) throws IOException {
skipIfSpace();
int next = mIn.peek();
int next = inputStream.peek();
if (next == '[') {
parseSequence(parent);
parseList(parent, '[', ']');
skipIfSpace();
}
String rest = readStringUntil('\r');
expect('\n');
String rest = readStringUntilEndOfLine();
if (!TextUtils.isEmpty(rest)) {
// The rest is free-form text.
@ -197,8 +221,19 @@ class ImapResponseParser {
}
}
private void parseListResponse(ImapResponse response) throws IOException {
expect(' ');
parseList(response, '(', ')');
expect(' ');
String delimiter = parseQuoted();
response.add(delimiter);
expect(' ');
String name = parseString();
response.add(name);
}
private void skipIfSpace() throws IOException {
if (mIn.peek() == ' ') {
if (inputStream.peek() == ' ') {
expect(' ');
}
}
@ -210,7 +245,7 @@ class ImapResponseParser {
* elements including List.
*
* @return The next token in the response or null if there are no more
* tokens.
* tokens.
*/
private Object readToken(ImapResponse response) throws IOException {
while (true) {
@ -223,11 +258,12 @@ class ImapResponseParser {
private Object parseToken(ImapList parent) throws IOException {
while (true) {
int ch = mIn.peek();
int ch = inputStream.peek();
if (ch == '(') {
return parseList(parent);
return parseList(parent, '(', ')');
} else if (ch == '[') {
return parseSequence(parent);
return parseList(parent, '[', ']');
} else if (ch == ')') {
expect(')');
return ")";
@ -250,98 +286,88 @@ class ImapResponseParser {
} else if (ch == '\t') {
expect('\t');
} else {
return parseAtom();
return parseBareString(true);
}
}
}
private String parseString() throws IOException {
int ch = inputStream.peek();
if (ch == '"') {
return parseQuoted();
} else if (ch == '{') {
return (String) parseLiteral();
} else {
return parseBareString(false);
}
}
private boolean parseCommandContinuationRequest() throws IOException {
expect('+');
return true;
}
// * OK [UIDNEXT 175] Predicted next UID
private void parseUntaggedResponse() throws IOException {
expect('*');
expect(' ');
}
// 3 OK [READ-WRITE] Select completed.
private String parseTaggedResponse() throws IOException {
return readStringUntil(' ');
}
private ImapList parseList(ImapList parent) throws IOException {
expect('(');
private ImapList parseList(ImapList parent, char start, char end) throws IOException {
expect(start);
ImapList list = new ImapList();
parent.add(list);
String endString = String.valueOf(end);
Object token;
while (true) {
token = parseToken(list);
if (token == null) {
return null;
} else if (token.equals(")")) {
} else if (token.equals(endString)) {
break;
} else if (token instanceof ImapList) {
// Do nothing
} else {
} else if (!(token instanceof ImapList)) {
list.add(token);
}
}
return list;
}
private ImapList parseSequence(ImapList parent) throws IOException {
expect('[');
ImapList list = new ImapList();
parent.add(list);
Object token;
while (true) {
token = parseToken(list);
if (token == null) {
return null;
} else if (token.equals("]")) {
break;
} else if (token instanceof ImapList) {
// Do nothing
} else {
list.add(token);
}
}
return list;
}
private String parseAtom() throws IOException {
private String parseBareString(boolean allowBrackets) throws IOException {
StringBuilder sb = new StringBuilder();
int ch;
while (true) {
ch = mIn.peek();
ch = inputStream.peek();
if (ch == -1) {
throw new IOException("parseAtom(): end of stream reached");
} else if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
ch == '[' || ch == ']' ||
// docs claim that flags are \ atom but atom isn't supposed to
// contain
// * and some flags contain *
// ch == '%' || ch == '*' ||
// ch == '%' ||
// TODO probably should not allow \ and should recognize
// it as a flag instead
// ch == '"' || ch == '\' ||
ch == '"' || (ch >= 0x00 && ch <= 0x1f) || ch == 0x7f) {
throw new IOException("parseBareString(): end of stream reached");
}
if (ch == '(' || ch == ')' || (allowBrackets && (ch == '[' || ch == ']')) ||
ch == '{' || ch == ' ' || ch == '"' ||
(ch >= 0x00 && ch <= 0x1f) || ch == 0x7f) {
if (sb.length() == 0) {
throw new IOException(String.format("parseAtom(): (%04x %c)", ch, ch));
throw new IOException(String.format("parseBareString(): (%04x %c)", ch, ch));
}
return sb.toString();
} else {
sb.append((char)mIn.read());
sb.append((char) inputStream.read());
}
}
}
/**
* A "{" has been read. Read the rest of the size string, the space and then
* notify the callback with an InputStream.
* A "{" has been read. Read the rest of the size string, the space and then notify the callback with an
* {@code InputStream}.
*/
private Object parseLiteral() throws IOException {
expect('{');
@ -353,24 +379,23 @@ class ImapResponseParser {
return "";
}
if (mResponse.getCallback() != null) {
FixedLengthInputStream fixed = new FixedLengthInputStream(mIn, size);
if (response.getCallback() != null) {
FixedLengthInputStream fixed = new FixedLengthInputStream(inputStream, size);
Object result = null;
try {
result = mResponse.getCallback().foundLiteral(mResponse, fixed);
result = response.getCallback().foundLiteral(response, fixed);
} catch (IOException e) {
// Pass IOExceptions through
throw e;
} catch (Exception e) {
// Catch everything else and save it for later.
mException = e;
//Log.e(LOG_TAG, "parseLiteral(): Exception in callback method", e);
exception = e;
}
// Check if only some of the literal data was read
int available = fixed.available();
if ((available > 0) && (available != size)) {
if (available > 0 && available != size) {
// If so, skip the rest
while (fixed.available() > 0) {
fixed.skip(fixed.available());
@ -385,7 +410,7 @@ class ImapResponseParser {
byte[] data = new byte[size];
int read = 0;
while (read != size) {
int count = mIn.read(data, read, size - read);
int count = inputStream.read(data, read, size - read);
if (count == -1) {
throw new IOException("parseLiteral(): end of stream reached");
}
@ -401,14 +426,14 @@ class ImapResponseParser {
StringBuilder sb = new StringBuilder();
int ch;
boolean escape = false;
while ((ch = mIn.read()) != -1) {
if (!escape && (ch == '\\')) {
while ((ch = inputStream.read()) != -1) {
if (!escape && ch == '\\') {
// Found the escape character
escape = true;
} else if (!escape && (ch == '"')) {
} else if (!escape && ch == '"') {
return sb.toString();
} else {
sb.append((char)ch);
sb.append((char) ch);
escape = false;
}
}
@ -417,47 +442,47 @@ class ImapResponseParser {
private String readStringUntil(char end) throws IOException {
StringBuilder sb = new StringBuilder();
int ch;
while ((ch = mIn.read()) != -1) {
while ((ch = inputStream.read()) != -1) {
if (ch == end) {
return sb.toString();
} else {
sb.append((char)ch);
sb.append((char) ch);
}
}
throw new IOException("readStringUntil(): end of stream reached");
}
private int expect(char ch) throws IOException {
int d;
if ((d = mIn.read()) != ch) {
throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)", (int)ch,
ch, d, (char)d));
}
return d;
private String readStringUntilEndOfLine() throws IOException {
String rest = readStringUntil('\r');
expect('\n');
return rest;
}
public boolean isStatusResponse(String symbol) {
private void expect(char expected) throws IOException {
int readByte = inputStream.read();
if (readByte != expected) {
throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)",
(int) expected, expected, readByte, (char) readByte));
}
}
private boolean isStatusResponse(String symbol) {
return symbol.equalsIgnoreCase("OK") ||
symbol.equalsIgnoreCase("NO") ||
symbol.equalsIgnoreCase("BAD") ||
symbol.equalsIgnoreCase("PREAUTH") ||
symbol.equalsIgnoreCase("BYE");
symbol.equalsIgnoreCase("NO") ||
symbol.equalsIgnoreCase("BAD") ||
symbol.equalsIgnoreCase("PREAUTH") ||
symbol.equalsIgnoreCase("BYE");
}
public static boolean equalsIgnoreCase(Object o1, Object o2) {
if (o1 != null && o2 != null && o1 instanceof String && o2 instanceof String) {
String s1 = (String)o1;
String s2 = (String)o2;
return s1.equalsIgnoreCase(s2);
} else if (o1 != null) {
return o1.equals(o2);
} else if (o2 != null) {
return o2.equals(o1);
} else {
// Both o1 and o2 are null
return true;
static boolean equalsIgnoreCase(Object token, String symbol) {
if (token == null || !(token instanceof String)) {
return false;
}
}
return symbol.equalsIgnoreCase((String) token);
}
}

View file

@ -1,30 +1,36 @@
package com.fsck.k9.mail.store.imap;
import com.fsck.k9.mail.filter.PeekableInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.filter.PeekableInputStream;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import static com.fsck.k9.mail.store.imap.ImapResponseParser.parseCapabilities;
import static java.util.Arrays.asList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class ImapResponseParserTest {
public class ImapResponseParserTest {
@Test public void testSimpleOkResponse() throws IOException {
@Test
public void testSimpleOkResponse() throws IOException {
ImapResponseParser parser = createParser("* OK\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
@ -32,8 +38,10 @@ public class ImapResponseParserTest {
assertEquals("OK", response.get(0));
}
@Test public void testOkResponseWithText() throws IOException {
@Test
public void testOkResponseWithText() throws IOException {
ImapResponseParser parser = createParser("* OK Some text here\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
@ -42,8 +50,10 @@ public class ImapResponseParserTest {
assertEquals("Some text here", response.get(1));
}
@Test public void testOkResponseWithRespTextCode() throws IOException {
@Test
public void testOkResponseWithRespTextCode() throws IOException {
ImapResponseParser parser = createParser("* OK [UIDVALIDITY 3857529045]\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
@ -57,8 +67,10 @@ public class ImapResponseParserTest {
assertEquals("3857529045", respTextCode.get(1));
}
@Test public void testOkResponseWithRespTextCodeAndText() throws IOException {
@Test
public void testOkResponseWithRespTextCodeAndText() throws IOException {
ImapResponseParser parser = createParser("* OK [token1 token2] {x} test [...]\r\n");
ImapResponse response = parser.readResponse();
assertNotNull(response);
@ -73,9 +85,10 @@ public class ImapResponseParserTest {
assertEquals("token2", respTextCode.get(1));
}
@Test
public void testReadStatusResponseWithOKResponse() throws Exception {
ImapResponseParser parser = createParser("* COMMAND BAR\tBAZ\r\nTAG OK COMMAND completed\r\n");
@Test public void testReadStatusResponseWithOKResponse() throws Exception {
ImapResponseParser parser = createParser("* COMMAND BAR BAZ\r\nTAG OK COMMAND completed\r\n");
List<ImapResponse> responses = parser.readStatusResponse("TAG", null, null, null);
assertEquals(2, responses.size());
@ -83,31 +96,339 @@ public class ImapResponseParserTest {
assertEquals(asList("OK", "COMMAND completed"), responses.get(1));
}
@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(expected = ImapException.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 testParseCapabilities() throws Exception {
ImapResponse capabilityResponse = new ImapResponse(null, false, null);
capabilityResponse.addAll(Arrays.asList("CAPABILITY", "FOO", "BAR"));
Set<String> capabilities = parseCapabilities(Arrays.asList(capabilityResponse));
@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 = new TestImapResponseCallback(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 = new TestImapResponseCallback(0, null);
ImapResponse response = parser.readResponse(callback);
assertEquals(1, response.size());
assertEquals("test", response.getString(0));
assertTrue(callback.foundLiteralCalled);
}
@Test
public void testParseLiteralWithIncompleteConsumingCallbackReturningString() throws Exception {
ImapResponseParser parser = createParser("* {4}\r\ntest\r\n");
TestImapResponseCallback callback = new TestImapResponseCallback(2, "ninja");
ImapResponse response = parser.readResponse(callback);
assertEquals(1, response.size());
assertEquals("ninja", response.getString(0));
}
@Test(expected = RuntimeException.class)
public void testParseLiteralWithThrowingCallback() 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 RuntimeException();
}
};
parser.readResponse(callback);
}
@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
public void testParseCapabilities() throws Exception {
ImapResponse capabilityResponse = createResponse("CAPABILITY", "FOO", "BAR");
List<ImapResponse> responses = Collections.singletonList(capabilityResponse);
Set<String> capabilities = parseCapabilities(responses);
assertEquals(2, capabilities.size());
assertTrue(capabilities.contains("FOO"));
assertTrue(capabilities.contains("BAR"));
}
@Test public void testParseCapabilitiesWithInvalidResponse() throws Exception {
ImapResponse capabilityResponse = new ImapResponse(null, false, null);
capabilityResponse.addAll(Arrays.asList("FOO", "BAZ"));
assertTrue(parseCapabilities(Arrays.asList(capabilityResponse)).isEmpty());
@Test
public void testParseCapabilitiesWithInvalidResponse() throws Exception {
ImapResponse capabilityResponse = createResponse("FOO", "BAZ");
List<ImapResponse> responses = Collections.singletonList(capabilityResponse);
Set<String> capabilities = parseCapabilities(responses);
assertTrue(capabilities.isEmpty());
}
@Test
public void testParseCapabilitiesWithMultipleResponses() throws Exception {
ImapResponse responseOne = createResponse("CAPABILITY", "foo");
ImapResponse responseTwo = createResponse("capability", "bar");
List<ImapResponse> responses = Arrays.asList(responseOne, responseTwo);
Set<String> capabilities = parseCapabilities(responses);
assertEquals(2, capabilities.size());
assertTrue(capabilities.contains("FOO"));
assertTrue(capabilities.contains("BAR"));
}
@Test
public void testOkResponseWithCapabilities() throws Exception {
ImapResponseParser parser = createParser("* OK [CAPABILITY foo bar]\r\n");
List<ImapResponse> responses = Collections.singletonList(parser.readResponse());
Set<String> capabilities = parseCapabilities(responses);
assertEquals(2, capabilities.size());
assertTrue(capabilities.contains("FOO"));
assertTrue(capabilities.contains("BAR"));
}
@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
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));
}
private ImapResponseParser createParser(String response) {
ByteArrayInputStream in = new ByteArrayInputStream(response.getBytes());
PeekableInputStream pin = new PeekableInputStream(in);
return new ImapResponseParser(pin);
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(response.getBytes());
PeekableInputStream peekableInputStream = new PeekableInputStream(byteArrayInputStream);
return new ImapResponseParser(peekableInputStream);
}
private ImapResponse createResponse(Object... tokens) {
ImapResponse response = ImapResponse.newUntaggedResponse(null);
response.addAll(Arrays.asList(tokens));
return response;
}
private class TestImapResponseCallback implements ImapResponseCallback {
private final int readNumberOfBytes;
private final Object returnValue;
public boolean foundLiteralCalled = false;
TestImapResponseCallback(int readNumberOfBytes, Object returnValue) {
this.readNumberOfBytes = readNumberOfBytes;
this.returnValue = returnValue;
}
@Override
public Object foundLiteral(ImapResponse response, FixedLengthInputStream literal) throws Exception {
foundLiteralCalled = true;
int skipBytes = readNumberOfBytes;
long skippedBytes;
do {
skippedBytes = literal.skip(skipBytes);
skipBytes -= skippedBytes;
} while (skippedBytes > 0);
return returnValue;
}
}
private class TestUntaggedHandler implements UntaggedHandler {
public final List<ImapResponse> responses = new ArrayList<ImapResponse>();
@Override
public void handleAsyncUntaggedResponse(ImapResponse response) {
responses.add(response);
}
}
}