ImapResponseParser.java revision 32311cce0153fbb2708d871626a0797cc93b7e4e
1/*
2 * Copyright (C) 2010 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.imap;
18
19import com.android.email.Email;
20import com.android.email.FixedLengthInputStream;
21import com.android.email.PeekableInputStream;
22import com.android.email.mail.transport.DiscourseLogger;
23import com.android.email.mail.transport.LoggingInputStream;
24import com.android.emailcommon.mail.MessagingException;
25
26import android.text.TextUtils;
27import android.util.Log;
28
29import java.io.IOException;
30import java.io.InputStream;
31import java.util.ArrayList;
32
33/**
34 * IMAP response parser.
35 */
36public class ImapResponseParser {
37    private static final boolean DEBUG_LOG_RAW_STREAM = false; // DO NOT RELEASE AS 'TRUE'
38
39    /**
40     * Literal larger than this will be stored in temp file.
41     */
42    private static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 16 * 1024 * 1024;
43
44    /** Input stream */
45    private final PeekableInputStream mIn;
46
47    /**
48     * To log network activities when the parser crashes.
49     *
50     * <p>We log all bytes received from the server, except for the part sent as literals.
51     */
52    private final DiscourseLogger mDiscourseLogger;
53
54    private final int mLiteralKeepInMemoryThreshold;
55
56    /** StringBuilder used by readUntil() */
57    private final StringBuilder mBufferReadUntil = new StringBuilder();
58
59    /** StringBuilder used by parseBareString() */
60    private final StringBuilder mParseBareString = new StringBuilder();
61
62    /**
63     * We store all {@link ImapResponse} in it.  {@link #destroyResponses()} must be called from
64     * time to time to destroy them and clear it.
65     */
66    private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();
67
68    /**
69     * Exception thrown when we receive BYE.  It derives from IOException, so it'll be treated
70     * in the same way EOF does.
71     */
72    public static class ByeException extends IOException {
73        public static final String MESSAGE = "Received BYE";
74        public ByeException() {
75            super(MESSAGE);
76        }
77    }
78
79    /**
80     * Public constructor for normal use.
81     */
82    public ImapResponseParser(InputStream in, DiscourseLogger discourseLogger) {
83        this(in, discourseLogger, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
84    }
85
86    /**
87     * Constructor for testing to override the literal size threshold.
88     */
89    /* package for test */ ImapResponseParser(InputStream in, DiscourseLogger discourseLogger,
90            int literalKeepInMemoryThreshold) {
91        if (DEBUG_LOG_RAW_STREAM && Email.DEBUG) {
92            in = new LoggingInputStream(in);
93        }
94        mIn = new PeekableInputStream(in);
95        mDiscourseLogger = discourseLogger;
96        mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
97    }
98
99    private static IOException newEOSException() {
100        final String message = "End of stream reached";
101        if (Email.DEBUG) {
102            Log.d(Email.LOG_TAG, message);
103        }
104        return new IOException(message);
105    }
106
107    /**
108     * Peek next one byte.
109     *
110     * Throws IOException() if reaches EOF.  As long as logical response lines end with \r\n,
111     * we shouldn't see EOF during parsing.
112     */
113    private int peek() throws IOException {
114        final int next = mIn.peek();
115        if (next == -1) {
116            throw newEOSException();
117        }
118        return next;
119    }
120
121    /**
122     * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
123     *
124     * Throws IOException() if reaches EOF.  As long as logical response lines end with \r\n,
125     * we shouldn't see EOF during parsing.
126     */
127    private int readByte() throws IOException {
128        int next = mIn.read();
129        if (next == -1) {
130            throw newEOSException();
131        }
132        mDiscourseLogger.addReceivedByte(next);
133        return next;
134    }
135
136    /**
137     * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
138     *
139     * @see #readResponse()
140     */
141    public void destroyResponses() {
142        for (ImapResponse r : mResponsesToDestroy) {
143            r.destroy();
144        }
145        mResponsesToDestroy.clear();
146    }
147
148    /**
149     * Reads the next response available on the stream and returns an
150     * {@link ImapResponse} object that represents it.
151     *
152     * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse}
153     * is stored in the internal storage.  When the {@link ImapResponse} is no longer used
154     * {@link #destroyResponses} should be called to destroy all the responses in the array.
155     *
156     * @return the parsed {@link ImapResponse} object.
157     * @exception ByeException when detects BYE.
158     */
159    public ImapResponse readResponse() throws IOException, MessagingException {
160        ImapResponse response = null;
161        try {
162            response = parseResponse();
163            if (Email.DEBUG) {
164                Log.d(Email.LOG_TAG, "<<< " + response.toString());
165            }
166
167        } catch (RuntimeException e) {
168            // Parser crash -- log network activities.
169            onParseError(e);
170            throw e;
171        } catch (IOException e) {
172            // Network error, or received an unexpected char.
173            onParseError(e);
174            throw e;
175        }
176
177        // Handle this outside of try-catch.  We don't have to dump protocol log when getting BYE.
178        if (response.is(0, ImapConstants.BYE)) {
179            Log.w(Email.LOG_TAG, ByeException.MESSAGE);
180            response.destroy();
181            throw new ByeException();
182        }
183        mResponsesToDestroy.add(response);
184        return response;
185    }
186
187    private void onParseError(Exception e) {
188        // Read a few more bytes, so that the log will contain some more context, even if the parser
189        // crashes in the middle of a response.
190        // This also makes sure the byte in question will be logged, no matter where it crashes.
191        // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
192        // before actually reading it.
193        // However, we don't want to read too much, because then it may get into an email message.
194        try {
195            for (int i = 0; i < 4; i++) {
196                int b = readByte();
197                if (b == -1 || b == '\n') {
198                    break;
199                }
200            }
201        } catch (IOException ignore) {
202        }
203        Log.w(Email.LOG_TAG, "Exception detected: " + e.getMessage());
204        mDiscourseLogger.logLastDiscourse();
205    }
206
207    /**
208     * Read next byte from stream and throw it away.  If the byte is different from {@code expected}
209     * throw {@link MessagingException}.
210     */
211    /* package for test */ void expect(char expected) throws IOException {
212        final int next = readByte();
213        if (expected != next) {
214            throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)",
215                    (int) expected, expected, next, (char) next));
216        }
217    }
218
219    /**
220     * Read bytes until we find {@code end}, and return all as string.
221     * The {@code end} will be read (rather than peeked) and won't be included in the result.
222     */
223    /* package for test */ String readUntil(char end) throws IOException {
224        mBufferReadUntil.setLength(0);
225        for (;;) {
226            final int ch = readByte();
227            if (ch != end) {
228                mBufferReadUntil.append((char) ch);
229            } else {
230                return mBufferReadUntil.toString();
231            }
232        }
233    }
234
235    /**
236     * Read all bytes until \r\n.
237     */
238    /* package */ String readUntilEol() throws IOException {
239        String ret = readUntil('\r');
240        expect('\n'); // TODO Should this really be error?
241        return ret;
242    }
243
244    /**
245     * Parse and return the response line.
246     */
247    private ImapResponse parseResponse() throws IOException, MessagingException {
248        // We need to destroy the response if we get an exception.
249        // So, we first store the response that's being built in responseToDestroy, until it's
250        // completely built, at which point we copy it into responseToReturn and null out
251        // responseToDestroyt.
252        // If responseToDestroy is not null in finally, we destroy it because that means
253        // we got an exception somewhere.
254        ImapResponse responseToDestroy = null;
255        final ImapResponse responseToReturn;
256
257        try {
258            final int ch = peek();
259            if (ch == '+') { // Continuation request
260                readByte(); // skip +
261                expect(' ');
262                responseToDestroy = new ImapResponse(null, true);
263
264                // If it's continuation request, we don't really care what's in it.
265                responseToDestroy.add(new ImapSimpleString(readUntilEol()));
266
267                // Response has successfully been built.  Let's return it.
268                responseToReturn = responseToDestroy;
269                responseToDestroy = null;
270            } else {
271                // Status response or response data
272                final String tag;
273                if (ch == '*') {
274                    tag = null;
275                    readByte(); // skip *
276                    expect(' ');
277                } else {
278                    tag = readUntil(' ');
279                }
280                responseToDestroy = new ImapResponse(tag, false);
281
282                final ImapString firstString = parseBareString();
283                responseToDestroy.add(firstString);
284
285                // parseBareString won't eat a space after the string, so we need to skip it,
286                // if exists.
287                // If the next char is not ' ', it should be EOL.
288                if (peek() == ' ') {
289                    readByte(); // skip ' '
290
291                    if (responseToDestroy.isStatusResponse()) { // It's a status response
292
293                        // Is there a response code?
294                        final int next = peek();
295                        if (next == '[') {
296                            responseToDestroy.add(parseList('[', ']'));
297                            if (peek() == ' ') { // Skip following space
298                                readByte();
299                            }
300                        }
301
302                        String rest = readUntilEol();
303                        if (!TextUtils.isEmpty(rest)) {
304                            // The rest is free-form text.
305                            responseToDestroy.add(new ImapSimpleString(rest));
306                        }
307                    } else { // It's a response data.
308                        parseElements(responseToDestroy, '\0');
309                    }
310                } else {
311                    expect('\r');
312                    expect('\n');
313                }
314
315                // Response has successfully been built.  Let's return it.
316                responseToReturn = responseToDestroy;
317                responseToDestroy = null;
318            }
319        } finally {
320            if (responseToDestroy != null) {
321                // We get an exception.
322                responseToDestroy.destroy();
323            }
324        }
325
326        return responseToReturn;
327    }
328
329    private ImapElement parseElement() throws IOException, MessagingException {
330        final int next = peek();
331        switch (next) {
332            case '(':
333                return parseList('(', ')');
334            case '[':
335                return parseList('[', ']');
336            case '"':
337                readByte(); // Skip "
338                return new ImapSimpleString(readUntil('"'));
339            case '{':
340                return parseLiteral();
341            case '\r':  // CR
342                readByte(); // Consume \r
343                expect('\n'); // Should be followed by LF.
344                return null;
345            case '\n': // LF // There shouldn't be a bare LF, but just in case.
346                readByte(); // Consume \n
347                return null;
348            default:
349                return parseBareString();
350        }
351    }
352
353    /**
354     * Parses an atom.
355     *
356     * Special case: If an atom contains '[', everything until the next ']' will be considered
357     * a part of the atom.
358     * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
359     *
360     * If the value is "NIL", returns an empty string.
361     */
362    private ImapString parseBareString() throws IOException, MessagingException {
363        mParseBareString.setLength(0);
364        for (;;) {
365            final int ch = peek();
366
367            // TODO Can we clean this up?  (This condition is from the old parser.)
368            if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
369                    // ']' is not part of atom (it's in resp-specials)
370                    ch == ']' ||
371                    // docs claim that flags are \ atom but atom isn't supposed to
372                    // contain
373                    // * and some flags contain *
374                    // ch == '%' || ch == '*' ||
375                    ch == '%' ||
376                    // TODO probably should not allow \ and should recognize
377                    // it as a flag instead
378                    // ch == '"' || ch == '\' ||
379                    ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) {
380                if (mParseBareString.length() == 0) {
381                    throw new MessagingException("Expected string, none found.");
382                }
383                String s = mParseBareString.toString();
384
385                // NIL will be always converted into the empty string.
386                if (ImapConstants.NIL.equalsIgnoreCase(s)) {
387                    return ImapString.EMPTY;
388                }
389                return new ImapSimpleString(s);
390            } else if (ch == '[') {
391                // Eat all until next ']'
392                mParseBareString.append((char) readByte());
393                mParseBareString.append(readUntil(']'));
394                mParseBareString.append(']'); // readUntil won't include the end char.
395            } else {
396                mParseBareString.append((char) readByte());
397            }
398        }
399    }
400
401    private void parseElements(ImapList list, char end)
402            throws IOException, MessagingException {
403        for (;;) {
404            for (;;) {
405                final int next = peek();
406                if (next == end) {
407                    return;
408                }
409                if (next != ' ') {
410                    break;
411                }
412                // Skip space
413                readByte();
414            }
415            final ImapElement el = parseElement();
416            if (el == null) { // EOL
417                return;
418            }
419            list.add(el);
420        }
421    }
422
423    private ImapList parseList(char opening, char closing)
424            throws IOException, MessagingException {
425        expect(opening);
426        final ImapList list = new ImapList();
427        parseElements(list, closing);
428        expect(closing);
429        return list;
430    }
431
432    private ImapString parseLiteral() throws IOException, MessagingException {
433        expect('{');
434        final int size;
435        try {
436            size = Integer.parseInt(readUntil('}'));
437        } catch (NumberFormatException nfe) {
438            throw new MessagingException("Invalid length in literal");
439        }
440        expect('\r');
441        expect('\n');
442        FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
443        if (size > mLiteralKeepInMemoryThreshold) {
444            return new ImapTempFileLiteral(in);
445        } else {
446            return new ImapMemoryLiteral(in);
447        }
448    }
449}
450