• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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