Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit de401db9 authored by cketti's avatar cketti
Browse files

Add support for folder names with brackets encoded as 'astring' in LIST response

Fixes #786
parent d686ae86
Loading
Loading
Loading
Loading
+9 −9
Original line number Diff line number Diff line
@@ -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;
            }
        }
+27 −16
Original line number Diff line number Diff line
@@ -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();
    }
}
+86 −72
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ class ImapResponseParser {
    private ImapResponse response;
    private Exception exception;


    public ImapResponseParser(PeekableInputStream in) {
        this.inputStream = in;
    }
@@ -40,17 +41,13 @@ class ImapResponseParser {
     */
    public ImapResponse readResponse(ImapResponseCallback callback) throws IOException {
        try {
            int ch = inputStream.peek();
            if (ch == '*') {
                parseUntaggedResponse();
                response = new ImapResponse(callback, false, null);
                readTokens(response);
            } else if (ch == '+') {
                response = new ImapResponse(callback, parseCommandContinuationRequest(), null);
                parseResponseText(response);
            int peek = inputStream.peek();
            if (peek == '+') {
                readContinuationRequest(callback);
            } else if (peek == '*') {
                readUntaggedResponse(callback);
            } else {
                response = new ImapResponse(callback, false, parseTaggedResponse());
                readTokens(response);
                readTaggedResponse(callback);
            }

            if (exception != null) {
@@ -64,6 +61,29 @@ class ImapResponseParser {
        }
    }

    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 {

@@ -148,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) {
@@ -187,12 +209,11 @@ class ImapResponseParser {

        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.
@@ -200,6 +221,17 @@ 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 (inputStream.peek() == ' ') {
            expect(' ');
@@ -229,9 +261,9 @@ class ImapResponseParser {
            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 ")";
@@ -254,66 +286,53 @@ 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);

        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 ImapList parseSequence(ImapList parent) throws IOException {
        expect('[');

        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);
            }
        }
@@ -321,27 +340,22 @@ class ImapResponseParser {
        return list;
    }

    private String parseAtom() throws IOException {
    private String parseBareString(boolean allowBrackets) throws IOException {
        StringBuilder sb = new StringBuilder();

        int ch;
        while (true) {
            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();
@@ -441,6 +455,13 @@ class ImapResponseParser {
        throw new IOException("readStringUntil(): end of stream reached");
    }

    private String readStringUntilEndOfLine() throws IOException {
        String rest = readStringUntil('\r');
        expect('\n');

        return rest;
    }

    private void expect(char expected) throws IOException {
        int readByte = inputStream.read();
        if (readByte != expected) {
@@ -457,18 +478,11 @@ class ImapResponseParser {
                symbol.equalsIgnoreCase("BYE");
    }

    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);
    }
}
+48 −1
Original line number Diff line number Diff line
@@ -338,6 +338,53 @@ public class ImapResponseParserTest {
        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 byteArrayInputStream = new ByteArrayInputStream(response.getBytes());
        PeekableInputStream peekableInputStream = new PeekableInputStream(byteArrayInputStream);
@@ -345,7 +392,7 @@ public class ImapResponseParserTest {
    }

    private ImapResponse createResponse(Object... tokens) {
        ImapResponse response = new ImapResponse(null, false, null);
        ImapResponse response = ImapResponse.newUntaggedResponse(null);
        response.addAll(Arrays.asList(tokens));
        return response;
    }