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