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