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