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