summaryrefslogtreecommitdiffstats
path: root/provider_src/com/android/email/mail/store/imap/ImapResponseParser.java
blob: 8dd1cf610032436a23d6a069c125513576295b6c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.email.mail.store.imap;

import android.text.TextUtils;

import com.android.email.DebugUtils;
import com.android.email.FixedLengthInputStream;
import com.android.email.PeekableInputStream;
import com.android.email.mail.transport.DiscourseLogger;
import com.android.emailcommon.Logging;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.utility.LoggingInputStream;
import com.android.mail.utils.LogUtils;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;

/**
 * IMAP response parser.
 */
public class ImapResponseParser {
    private static final boolean DEBUG_LOG_RAW_STREAM = false; // DO NOT RELEASE AS 'TRUE'

    /**
     * Literal larger than this will be stored in temp file.
     */
    public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024;

    /** Input stream */
    private final PeekableInputStream mIn;

    /**
     * To log network activities when the parser crashes.
     *
     * <p>We log all bytes received from the server, except for the part sent as literals.
     */
    private final DiscourseLogger mDiscourseLogger;

    private final int mLiteralKeepInMemoryThreshold;

    /** StringBuilder used by readUntil() */
    private final StringBuilder mBufferReadUntil = new StringBuilder();

    /** StringBuilder used by parseBareString() */
    private final StringBuilder mParseBareString = new StringBuilder();

    /**
     * We store all {@link ImapResponse} in it.  {@link #destroyResponses()} must be called from
     * time to time to destroy them and clear it.
     */
    private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>();

    /**
     * Exception thrown when we receive BYE.  It derives from IOException, so it'll be treated
     * in the same way EOF does.
     */
    public static class ByeException extends IOException {
        public static final String MESSAGE = "Received BYE";
        public ByeException() {
            super(MESSAGE);
        }
    }

    /**
     * Public constructor for normal use.
     */
    public ImapResponseParser(InputStream in, DiscourseLogger discourseLogger) {
        this(in, discourseLogger, LITERAL_KEEP_IN_MEMORY_THRESHOLD);
    }

    /**
     * Constructor for testing to override the literal size threshold.
     */
    /* package for test */ ImapResponseParser(InputStream in, DiscourseLogger discourseLogger,
            int literalKeepInMemoryThreshold) {
        if (DEBUG_LOG_RAW_STREAM && DebugUtils.DEBUG) {
            in = new LoggingInputStream(in);
        }
        mIn = new PeekableInputStream(in);
        mDiscourseLogger = discourseLogger;
        mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold;
    }

    private static IOException newEOSException() {
        final String message = "End of stream reached";
        if (DebugUtils.DEBUG) {
            LogUtils.d(Logging.LOG_TAG, message);
        }
        return new IOException(message);
    }

    /**
     * Peek next one byte.
     *
     * Throws IOException() if reaches EOF.  As long as logical response lines end with \r\n,
     * we shouldn't see EOF during parsing.
     */
    private int peek() throws IOException {
        final int next = mIn.peek();
        if (next == -1) {
            throw newEOSException();
        }
        return next;
    }

    /**
     * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}.
     *
     * Throws IOException() if reaches EOF.  As long as logical response lines end with \r\n,
     * we shouldn't see EOF during parsing.
     */
    private int readByte() throws IOException {
        int next = mIn.read();
        if (next == -1) {
            throw newEOSException();
        }
        mDiscourseLogger.addReceivedByte(next);
        return next;
    }

    /**
     * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it.
     *
     * @see #readResponse()
     */
    public void destroyResponses() {
        for (ImapResponse r : mResponsesToDestroy) {
            r.destroy();
        }
        mResponsesToDestroy.clear();
    }

    /**
     * Reads the next response available on the stream and returns an
     * {@link ImapResponse} object that represents it.
     *
     * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse}
     * is stored in the internal storage.  When the {@link ImapResponse} is no longer used
     * {@link #destroyResponses} should be called to destroy all the responses in the array.
     *
     * @return the parsed {@link ImapResponse} object.
     * @exception ByeException when detects BYE.
     */
    public ImapResponse readResponse() throws IOException, MessagingException {
        ImapResponse response = null;
        try {
            response = parseResponse();
            if (DebugUtils.DEBUG) {
                LogUtils.d(Logging.LOG_TAG, "<<< " + response.toString());
            }

        } catch (RuntimeException e) {
            // Parser crash -- log network activities.
            onParseError(e);
            throw e;
        } catch (IOException e) {
            // Network error, or received an unexpected char.
            onParseError(e);
            throw e;
        }

        // Handle this outside of try-catch.  We don't have to dump protocol log when getting BYE.
        if (response.is(0, ImapConstants.BYE)) {
            LogUtils.w(Logging.LOG_TAG, ByeException.MESSAGE);
            response.destroy();
            throw new ByeException();
        }
        mResponsesToDestroy.add(response);
        return response;
    }

    private void onParseError(Exception e) {
        // Read a few more bytes, so that the log will contain some more context, even if the parser
        // crashes in the middle of a response.
        // This also makes sure the byte in question will be logged, no matter where it crashes.
        // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception
        // before actually reading it.
        // However, we don't want to read too much, because then it may get into an email message.
        try {
            for (int i = 0; i < 4; i++) {
                int b = readByte();
                if (b == -1 || b == '\n') {
                    break;
                }
            }
        } catch (IOException ignore) {
        }
        LogUtils.w(Logging.LOG_TAG, "Exception detected: " + e.getMessage());
        mDiscourseLogger.logLastDiscourse();
    }

    /**
     * Read next byte from stream and throw it away.  If the byte is different from {@code expected}
     * throw {@link MessagingException}.
     */
    /* package for test */ void expect(char expected) throws IOException {
        final int next = readByte();
        if (expected != next) {
            throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)",
                    (int) expected, expected, next, (char) next));
        }
    }

    /**
     * Read bytes until we find {@code end}, and return all as string.
     * The {@code end} will be read (rather than peeked) and won't be included in the result.
     */
    /* package for test */ String readUntil(char end) throws IOException {
        mBufferReadUntil.setLength(0);
        for (;;) {
            final int ch = readByte();
            if (ch != end) {
                mBufferReadUntil.append((char) ch);
            } else {
                return mBufferReadUntil.toString();
            }
        }
    }

    /**
     * Read all bytes until \r\n.
     */
    /* package */ String readUntilEol() throws IOException {
        String ret = readUntil('\r');
        expect('\n'); // TODO Should this really be error?
        return ret;
    }

    /**
     * Parse and return the response line.
     */
    private ImapResponse parseResponse() throws IOException, MessagingException {
        // We need to destroy the response if we get an exception.
        // So, we first store the response that's being built in responseToDestroy, until it's
        // completely built, at which point we copy it into responseToReturn and null out
        // responseToDestroyt.
        // If responseToDestroy is not null in finally, we destroy it because that means
        // we got an exception somewhere.
        ImapResponse responseToDestroy = null;
        final ImapResponse responseToReturn;

        try {
            final int ch = peek();
            if (ch == '+') { // Continuation request
                readByte(); // skip +
                expect(' ');
                responseToDestroy = new ImapResponse(null, true);

                // If it's continuation request, we don't really care what's in it.
                responseToDestroy.add(new ImapSimpleString(readUntilEol()));

                // Response has successfully been built.  Let's return it.
                responseToReturn = responseToDestroy;
                responseToDestroy = null;
            } else {
                // Status response or response data
                final String tag;
                if (ch == '*') {
                    tag = null;
                    readByte(); // skip *
                    expect(' ');
                } else {
                    tag = readUntil(' ');
                }
                responseToDestroy = new ImapResponse(tag, false);

                final ImapString firstString = parseBareString();
                responseToDestroy.add(firstString);

                // parseBareString won't eat a space after the string, so we need to skip it,
                // if exists.
                // If the next char is not ' ', it should be EOL.
                if (peek() == ' ') {
                    readByte(); // skip ' '

                    if (responseToDestroy.isStatusResponse()) { // It's a status response

                        // Is there a response code?
                        final int next = peek();
                        if (next == '[') {
                            responseToDestroy.add(parseList('[', ']'));
                            if (peek() == ' ') { // Skip following space
                                readByte();
                            }
                        }

                        String rest = readUntilEol();
                        if (!TextUtils.isEmpty(rest)) {
                            // The rest is free-form text.
                            responseToDestroy.add(new ImapSimpleString(rest));
                        }
                    } else { // It's a response data.
                        parseElements(responseToDestroy, '\0');
                    }
                } else {
                    expect('\r');
                    expect('\n');
                }

                // Response has successfully been built.  Let's return it.
                responseToReturn = responseToDestroy;
                responseToDestroy = null;
            }
        } finally {
            if (responseToDestroy != null) {
                // We get an exception.
                responseToDestroy.destroy();
            }
        }

        return responseToReturn;
    }

    private ImapElement parseElement() throws IOException, MessagingException {
        final int next = peek();
        switch (next) {
            case '(':
                return parseList('(', ')');
            case '[':
                return parseList('[', ']');
            case '"':
                readByte(); // Skip "
                return new ImapSimpleString(readUntil('"'));
            case '{':
                return parseLiteral();
            case '\r':  // CR
                readByte(); // Consume \r
                expect('\n'); // Should be followed by LF.
                return null;
            case '\n': // LF // There shouldn't be a bare LF, but just in case.
                readByte(); // Consume \n
                return null;
            default:
                return parseBareString();
        }
    }

    /**
     * Parses an atom.
     *
     * Special case: If an atom contains '[', everything until the next ']' will be considered
     * a part of the atom.
     * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString)
     *
     * If the value is "NIL", returns an empty string.
     */
    private ImapString parseBareString() throws IOException, MessagingException {
        mParseBareString.setLength(0);
        for (;;) {
            final int ch = peek();

            // TODO Can we clean this up?  (This condition is from the old parser.)
            if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
                    // ']' is not part of atom (it's in resp-specials)
                    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 == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) {
                if (mParseBareString.length() == 0) {
                    throw new MessagingException("Expected string, none found.");
                }
                String s = mParseBareString.toString();

                // NIL will be always converted into the empty string.
                if (ImapConstants.NIL.equalsIgnoreCase(s)) {
                    return ImapString.EMPTY;
                }
                return new ImapSimpleString(s);
            } else if (ch == '[') {
                // Eat all until next ']'
                mParseBareString.append((char) readByte());
                mParseBareString.append(readUntil(']'));
                mParseBareString.append(']'); // readUntil won't include the end char.
            } else {
                mParseBareString.append((char) readByte());
            }
        }
    }

    private void parseElements(ImapList list, char end)
            throws IOException, MessagingException {
        for (;;) {
            for (;;) {
                final int next = peek();
                if (next == end) {
                    return;
                }
                if (next != ' ') {
                    break;
                }
                // Skip space
                readByte();
            }
            final ImapElement el = parseElement();
            if (el == null) { // EOL
                return;
            }
            list.add(el);
        }
    }

    private ImapList parseList(char opening, char closing)
            throws IOException, MessagingException {
        expect(opening);
        final ImapList list = new ImapList();
        parseElements(list, closing);
        expect(closing);
        return list;
    }

    private ImapString parseLiteral() throws IOException, MessagingException {
        expect('{');
        final int size;
        try {
            size = Integer.parseInt(readUntil('}'));
        } catch (NumberFormatException nfe) {
            throw new MessagingException("Invalid length in literal");
        }
        if (size < 0) {
            throw new MessagingException("Invalid negative length in literal");
        }
        expect('\r');
        expect('\n');
        FixedLengthInputStream in = new FixedLengthInputStream(mIn, size);
        if (size > mLiteralKeepInMemoryThreshold) {
            return new ImapTempFileLiteral(in);
        } else {
            return new ImapMemoryLiteral(in);
        }
    }
}