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