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