1 /* 2 * Copyright (C) 2008 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; 18 19 import com.android.email.Email; 20 import com.android.email.FixedLengthInputStream; 21 import com.android.email.PeekableInputStream; 22 import com.android.email.mail.MessagingException; 23 import com.android.email.mail.transport.LoggingInputStream; 24 25 import android.util.Config; 26 import android.util.Log; 27 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.text.ParseException; 31 import java.text.SimpleDateFormat; 32 import java.util.ArrayList; 33 import java.util.Date; 34 import java.util.Locale; 35 36 public class ImapResponseParser { 37 // DEBUG ONLY - Always check in as "false" 38 private static boolean DEBUG_LOG_RAW_STREAM = false; 39 40 // mDateTimeFormat is used only for parsing IMAP's FETCH ENVELOPE command, in which 41 // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be 42 // handled by Locale.US 43 SimpleDateFormat mDateTimeFormat = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US); 44 PeekableInputStream mIn; 45 InputStream mActiveLiteral; 46 ImapResponseParser(InputStream in)47 public ImapResponseParser(InputStream in) { 48 if (DEBUG_LOG_RAW_STREAM && Config.LOGD && Email.DEBUG) { 49 in = new LoggingInputStream(in); 50 } 51 this.mIn = new PeekableInputStream(in); 52 } 53 54 /** 55 * Reads the next response available on the stream and returns an 56 * ImapResponse object that represents it. 57 * 58 * @return 59 * @throws IOException 60 */ readResponse()61 public ImapResponse readResponse() throws IOException { 62 ImapResponse response = new ImapResponse(); 63 if (mActiveLiteral != null) { 64 while (mActiveLiteral.read() != -1) 65 ; 66 mActiveLiteral = null; 67 } 68 int ch = mIn.peek(); 69 if (ch == '*') { 70 parseUntaggedResponse(); 71 readTokens(response); 72 } else if (ch == '+') { 73 response.mCommandContinuationRequested = 74 parseCommandContinuationRequest(); 75 readTokens(response); 76 } else { 77 response.mTag = parseTaggedResponse(); 78 readTokens(response); 79 } 80 if (Config.LOGD) { 81 if (Email.DEBUG) { 82 Log.d(Email.LOG_TAG, "<<< " + response.toString()); 83 } 84 } 85 return response; 86 } 87 readTokens(ImapResponse response)88 private void readTokens(ImapResponse response) throws IOException { 89 response.clear(); 90 Object token; 91 while ((token = readToken()) != null) { 92 if (response != null) { 93 response.add(token); 94 } 95 if (mActiveLiteral != null) { 96 break; 97 } 98 } 99 response.mCompleted = token == null; 100 } 101 102 /** 103 * Reads the next token of the response. The token can be one of: String - 104 * for NIL, QUOTED, NUMBER, ATOM. InputStream - for LITERAL. 105 * InputStream.available() returns the total length of the stream. 106 * ImapResponseList - for PARENTHESIZED LIST. Can contain any of the above 107 * elements including List. 108 * 109 * @return The next token in the response or null if there are no more 110 * tokens. 111 * @throws IOException 112 */ readToken()113 public Object readToken() throws IOException { 114 while (true) { 115 Object token = parseToken(); 116 if (token == null || !token.equals(")")) { 117 return token; 118 } 119 } 120 } 121 parseToken()122 private Object parseToken() throws IOException { 123 if (mActiveLiteral != null) { 124 while (mActiveLiteral.read() != -1) 125 ; 126 mActiveLiteral = null; 127 } 128 while (true) { 129 int ch = mIn.peek(); 130 if (ch == '(') { 131 return parseList(); 132 } else if (ch == ')') { 133 expect(')'); 134 return ")"; 135 } else if (ch == '"') { 136 return parseQuoted(); 137 } else if (ch == '{') { 138 mActiveLiteral = parseLiteral(); 139 return mActiveLiteral; 140 } else if (ch == ' ') { 141 expect(' '); 142 } else if (ch == '\r') { 143 expect('\r'); 144 expect('\n'); 145 return null; 146 } else if (ch == '\n') { 147 expect('\n'); 148 return null; 149 } else { 150 return parseAtom(); 151 } 152 } 153 } 154 parseCommandContinuationRequest()155 private boolean parseCommandContinuationRequest() throws IOException { 156 expect('+'); 157 expect(' '); 158 return true; 159 } 160 161 // * OK [UIDNEXT 175] Predicted next UID parseUntaggedResponse()162 private void parseUntaggedResponse() throws IOException { 163 expect('*'); 164 expect(' '); 165 } 166 167 // 3 OK [READ-WRITE] Select completed. parseTaggedResponse()168 private String parseTaggedResponse() throws IOException { 169 String tag = readStringUntil(' '); 170 return tag; 171 } 172 parseList()173 private ImapList parseList() throws IOException { 174 expect('('); 175 ImapList list = new ImapList(); 176 Object token; 177 while (true) { 178 token = parseToken(); 179 if (token == null) { 180 break; 181 } else if (token instanceof InputStream) { 182 list.add(token); 183 break; 184 } else if (token.equals(")")) { 185 break; 186 } else { 187 list.add(token); 188 } 189 } 190 return list; 191 } 192 parseAtom()193 private String parseAtom() throws IOException { 194 StringBuffer sb = new StringBuffer(); 195 int ch; 196 while (true) { 197 ch = mIn.peek(); 198 if (ch == -1) { 199 if (Config.LOGD && Email.DEBUG) { 200 Log.d(Email.LOG_TAG, "parseAtom(): end of stream reached"); 201 } 202 throw new IOException("parseAtom(): end of stream reached"); 203 } else if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' || 204 // docs claim that flags are \ atom but atom isn't supposed to 205 // contain 206 // * and some falgs contain * 207 // ch == '%' || ch == '*' || 208 ch == '%' || 209 // TODO probably should not allow \ and should recognize 210 // it as a flag instead 211 // ch == '"' || ch == '\' || 212 ch == '"' || (ch >= 0x00 && ch <= 0x1f) || ch == 0x7f) { 213 if (sb.length() == 0) { 214 throw new IOException(String.format("parseAtom(): (%04x %c)", (int)ch, ch)); 215 } 216 return sb.toString(); 217 } else { 218 sb.append((char)mIn.read()); 219 } 220 } 221 } 222 223 /** 224 * A { has been read, read the rest of the size string, the space and then 225 * notify the listener with an InputStream. 226 * 227 * @param mListener 228 * @throws IOException 229 */ parseLiteral()230 private InputStream parseLiteral() throws IOException { 231 expect('{'); 232 int size = Integer.parseInt(readStringUntil('}')); 233 expect('\r'); 234 expect('\n'); 235 FixedLengthInputStream fixed = new FixedLengthInputStream(mIn, size); 236 return fixed; 237 } 238 239 /** 240 * A " has been read, read to the end of the quoted string and notify the 241 * listener. 242 * 243 * @param mListener 244 * @throws IOException 245 */ parseQuoted()246 private String parseQuoted() throws IOException { 247 expect('"'); 248 return readStringUntil('"'); 249 } 250 readStringUntil(char end)251 private String readStringUntil(char end) throws IOException { 252 StringBuffer sb = new StringBuffer(); 253 int ch; 254 while ((ch = mIn.read()) != -1) { 255 if (ch == end) { 256 return sb.toString(); 257 } else { 258 sb.append((char)ch); 259 } 260 } 261 if (Config.LOGD && Email.DEBUG) { 262 Log.d(Email.LOG_TAG, "readQuotedString(): end of stream reached"); 263 } 264 throw new IOException("readQuotedString(): end of stream reached"); 265 } 266 expect(char ch)267 private int expect(char ch) throws IOException { 268 int d; 269 if ((d = mIn.read()) != ch) { 270 if (d == -1 && Config.LOGD && Email.DEBUG) { 271 Log.d(Email.LOG_TAG, "expect(): end of stream reached"); 272 } 273 throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)", (int)ch, 274 ch, d, (char)d)); 275 } 276 return d; 277 } 278 279 /** 280 * Represents an IMAP LIST response and is also the base class for the 281 * ImapResponse. 282 */ 283 public class ImapList extends ArrayList<Object> { getList(int index)284 public ImapList getList(int index) { 285 return (ImapList)get(index); 286 } 287 getString(int index)288 public String getString(int index) { 289 return (String)get(index); 290 } 291 getLiteral(int index)292 public InputStream getLiteral(int index) { 293 return (InputStream)get(index); 294 } 295 getNumber(int index)296 public int getNumber(int index) { 297 return Integer.parseInt(getString(index)); 298 } 299 getDate(int index)300 public Date getDate(int index) throws MessagingException { 301 try { 302 return mDateTimeFormat.parse(getString(index)); 303 } catch (ParseException pe) { 304 throw new MessagingException("Unable to parse IMAP datetime", pe); 305 } 306 } 307 getKeyedValue(Object key)308 public Object getKeyedValue(Object key) { 309 for (int i = 0, count = size(); i < count; i++) { 310 if (get(i).equals(key)) { 311 return get(i + 1); 312 } 313 } 314 return null; 315 } 316 getKeyedList(Object key)317 public ImapList getKeyedList(Object key) { 318 return (ImapList)getKeyedValue(key); 319 } 320 getKeyedString(Object key)321 public String getKeyedString(Object key) { 322 return (String)getKeyedValue(key); 323 } 324 getKeyedLiteral(Object key)325 public InputStream getKeyedLiteral(Object key) { 326 return (InputStream)getKeyedValue(key); 327 } 328 getKeyedNumber(Object key)329 public int getKeyedNumber(Object key) { 330 return Integer.parseInt(getKeyedString(key)); 331 } 332 getKeyedDate(Object key)333 public Date getKeyedDate(Object key) throws MessagingException { 334 try { 335 String value = getKeyedString(key); 336 if (value == null) { 337 return null; 338 } 339 return mDateTimeFormat.parse(value); 340 } catch (ParseException pe) { 341 throw new MessagingException("Unable to parse IMAP datetime", pe); 342 } 343 } 344 } 345 346 /** 347 * Represents a single response from the IMAP server. Tagged responses will 348 * have a non-null tag. Untagged responses will have a null tag. The object 349 * will contain all of the available tokens at the time the response is 350 * received. In general, it will either contain all of the tokens of the 351 * response or all of the tokens up until the first LITERAL. If the object 352 * does not contain the entire response the caller must call more() to 353 * continue reading the response until more returns false. 354 */ 355 public class ImapResponse extends ImapList { 356 private boolean mCompleted; 357 358 boolean mCommandContinuationRequested; 359 String mTag; 360 361 /* 362 * Return true if this response is completely read and parsed. 363 */ completed()364 public boolean completed() { 365 return mCompleted; 366 } 367 368 /* 369 * Nail down the last element that possibly is FixedLengthInputStream literal. 370 */ nailDown()371 public void nailDown() throws IOException { 372 int last = size() - 1; 373 if (last >= 0) { 374 Object o = get(last); 375 if (o instanceof FixedLengthInputStream) { 376 FixedLengthInputStream is = (FixedLengthInputStream) o; 377 byte[] buffer = new byte[is.available()]; 378 is.read(buffer); 379 set(last, (Object) new String(buffer)); 380 } 381 } 382 } 383 384 /* 385 * Append all response elements to this and copy completed flag. 386 */ appendAll(ImapResponse other)387 public void appendAll(ImapResponse other) { 388 addAll(other); 389 mCompleted = other.mCompleted; 390 } 391 more()392 public boolean more() throws IOException { 393 if (mCompleted) { 394 return false; 395 } 396 readTokens(this); 397 return true; 398 } 399 getAlertText()400 public String getAlertText() { 401 if (size() > 1 && "[ALERT]".equals(getString(1))) { 402 StringBuffer sb = new StringBuffer(); 403 for (int i = 2, count = size(); i < count; i++) { 404 sb.append(get(i).toString()); 405 sb.append(' '); 406 } 407 return sb.toString(); 408 } else { 409 return null; 410 } 411 } 412 toString()413 public String toString() { 414 return "#" + mTag + "# " + super.toString(); 415 } 416 } 417 418 } 419