View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.geronimo.javamail.store.imap.connection;
19  
20  import java.io.ByteArrayOutputStream;
21  import java.io.UnsupportedEncodingException;
22  import java.util.ArrayList; 
23  import java.util.Date; 
24  import java.util.List; 
25  
26  import javax.mail.Flags;
27  import javax.mail.MessagingException;
28  import javax.mail.internet.InternetAddress;
29  import javax.mail.internet.MailDateFormat;
30  import javax.mail.internet.ParameterList;
31  
32  import org.apache.geronimo.javamail.util.ResponseFormatException; 
33  
34  /**
35   * @version $Rev: 670052 $ $Date: 2008-06-20 16:15:52 -0400 (Fri, 20 Jun 2008) $
36   */
37  public class IMAPResponseTokenizer {
38      /*
39       * set up the decoding table.
40       */
41      protected static final byte[] decodingTable = new byte[256];
42  
43      protected static void initializeDecodingTable()
44      {
45          for (int i = 0; i < IMAPCommand.encodingTable.length; i++)
46          {
47              decodingTable[IMAPCommand.encodingTable[i]] = (byte)i;
48          }
49      }
50  
51  
52      static {
53          initializeDecodingTable();
54      }
55      
56      // a singleton formatter for header dates.
57      protected static MailDateFormat dateParser = new MailDateFormat();
58      
59      
60      public static class Token {
61          // Constant values from J2SE 1.4 API Docs (Constant values)
62          public static final int ATOM = -1;
63          public static final int QUOTEDSTRING = -2;
64          public static final int LITERAL = -3;
65          public static final int NUMERIC = -4;
66          public static final int EOF = -5;
67          public static final int NIL = -6;
68          // special single character markers     
69          public static final int CONTINUATION = '-';
70          public static final int UNTAGGED = '*';
71              
72          /**
73           * The type indicator.  This will be either a specific type, represented by 
74           * a negative number, or the actual character value. 
75           */
76          private int type;
77          /**
78           * The String value associated with this token.  All tokens have a String value, 
79           * except for the EOF and NIL tokens. 
80           */
81          private String value;
82  
83          public Token(int type, String value) {
84              this.type = type;
85              this.value = value;
86          }
87  
88          public int getType() {
89              return type;
90          }
91  
92          public String getValue() {
93              return value;
94          }
95  
96          public boolean isType(int type) {
97              return this.type == type;
98          }
99  
100         /**
101          * Return the token as an integer value.  If this can't convert, an exception is 
102          * thrown. 
103          * 
104          * @return The integer value of the token. 
105          * @exception ResponseFormatException
106          */
107         public int getInteger() throws MessagingException {
108             if (value != null) {
109                 try {
110                     return Integer.parseInt(value);
111                 } catch (NumberFormatException e) {
112                 }
113             }
114 
115             throw new ResponseFormatException("Number value expected in response; fount: " + value);
116         }
117 
118         /**
119          * Return the token as a long value.  If it can't convert, an exception is 
120          * thrown. 
121          * 
122          * @return The token as a long value. 
123          * @exception ResponseFormatException
124          */
125         public long getLong() throws MessagingException {
126             if (value != null) {
127                 try {
128                     return Long.parseLong(value);
129                 } catch (NumberFormatException e) {
130                 }
131             }
132             throw new ResponseFormatException("Number value expected in response; fount: " + value);
133         }
134         
135         /**
136          * Handy debugging toString() method for token. 
137          * 
138          * @return The string value of the token. 
139          */
140         public String toString() {
141             if (type == NIL) {
142                 return "NIL"; 
143             }
144             else if (type == EOF) {
145                 return "EOF";
146             }
147             
148             if (value == null) {
149                 return ""; 
150             }
151             return value; 
152         }
153     }
154 
155     public static final Token EOF = new Token(Token.EOF, null);
156     public static final Token NIL = new Token(Token.NIL, null);
157 
158     private static final String WHITE = " \t\n\r";
159     // The list of delimiter characters we process when    
160     // handling parsing of ATOMs.  
161     private static final String atomDelimiters = "(){}%*\"\\" + WHITE;
162     // this set of tokens is a slighly expanded set used for 
163     // specific response parsing.  When dealing with Body 
164     // section names, there are sub pieces to the name delimited 
165     // by "[", "]", ".", "<", ">" and SPACE, so reading these using 
166     // a superset of the ATOM processing makes for easier parsing. 
167     private static final String tokenDelimiters = "<>[].(){}%*\"\\" + WHITE;
168 
169     // the response data read from the connection
170     private byte[] response;
171     // current parsing position
172     private int pos;
173 
174     public IMAPResponseTokenizer(byte [] response) {
175         this.response = response;
176     }
177 
178     /**
179      * Get the remainder of the response as a string.
180      *
181      * @return A string representing the remainder of the response.
182      */
183     public String getRemainder() {
184         // make sure we're still in range
185         if (pos >= response.length) {
186             return "";
187         }
188 
189         return new String(response, pos, response.length - pos);
190     }
191     
192     
193     public Token next() throws MessagingException {
194         return next(false);
195     }
196 
197     public Token next(boolean nilAllowed) throws MessagingException {
198         return readToken(nilAllowed, false);
199     }
200 
201     public Token next(boolean nilAllowed, boolean expandedDelimiters) throws MessagingException {
202         return readToken(nilAllowed, expandedDelimiters);
203     }
204 
205     public Token peek() throws MessagingException {
206         return peek(false, false);
207     }
208 
209     public Token peek(boolean nilAllowed) throws MessagingException {
210         return peek(nilAllowed, false);
211     }
212 
213     public Token peek(boolean nilAllowed, boolean expandedDelimiters) throws MessagingException {
214         int start = pos;
215         try {
216             return readToken(nilAllowed, expandedDelimiters);
217         } finally {
218             pos = start;
219         }
220     }
221 
222     /**
223      * Read an ATOM token from the parsed response.
224      *
225      * @return A token containing the value of the atom token.
226      */
227     private Token readAtomicToken(String delimiters) {
228         // skip to next delimiter
229         int start = pos;
230         while (++pos < response.length) {
231             // break on the first non-atom character.
232             byte ch = response[pos];
233             if (delimiters.indexOf(response[pos]) != -1 || ch < 32 || ch >= 127) {
234                 break;
235             }
236         }
237         
238         // Numeric tokens we store as a different type.  
239         String value = new String(response, start, pos - start); 
240         try {
241             int intValue = Integer.parseInt(value); 
242             return new Token(Token.NUMERIC, value);
243         } catch (NumberFormatException e) {
244         }
245         return new Token(Token.ATOM, value);
246     }
247 
248     /**
249      * Read the next token from the response.
250      *
251      * @return The next token from the response.  White space is skipped, and comment
252      *         tokens are also skipped if indicated.
253      * @exception ResponseFormatException
254      */
255     private Token readToken(boolean nilAllowed, boolean expandedDelimiters) throws MessagingException {
256         String delimiters = expandedDelimiters ? tokenDelimiters : atomDelimiters; 
257         
258         if (pos >= response.length) {
259             return EOF;
260         } else {
261             byte ch = response[pos];
262             if (ch == '\"') {
263                 return readQuotedString();
264             // beginning of a length-specified literal?
265             } else if (ch == '{') {
266                 return readLiteral();
267             // white space, eat this and find a real token.
268             } else if (WHITE.indexOf(ch) != -1) {
269                 eatWhiteSpace();
270                 return readToken(nilAllowed, expandedDelimiters);
271             // either a CTL or special.  These characters have a self-defining token type.
272             } else if (ch < 32 || ch >= 127 || delimiters.indexOf(ch) != -1) {
273                 pos++;
274                 return new Token((int)ch, String.valueOf((char)ch));
275             } else {
276                 // start of an atom, parse it off.
277                 Token token = readAtomicToken(delimiters);
278                 // now, if we've been asked to look at NIL tokens, check to see if it is one,
279                 // and return that instead of the ATOM.
280                 if (nilAllowed) {
281                     if (token.getValue().equalsIgnoreCase("NIL")) {
282                         return NIL;
283                     }
284                 }
285                 return token;
286             }
287         }
288     }
289 
290     /**
291      * Read the next token from the response, returning it as a byte array value.
292      *
293      * @return The next token from the response.  White space is skipped, and comment
294      *         tokens are also skipped if indicated.
295      * @exception ResponseFormatException
296      */
297     private byte[] readData(boolean nilAllowed) throws MessagingException {
298         if (pos >= response.length) {
299             return null;
300         } else {
301             byte ch = response[pos];
302             if (ch == '\"') {
303                 return readQuotedStringData();
304             // beginning of a length-specified literal?
305             } else if (ch == '{') {
306                 return readLiteralData();
307             // white space, eat this and find a real token.
308             } else if (WHITE.indexOf(ch) != -1) {
309                 eatWhiteSpace();
310                 return readData(nilAllowed);
311             // either a CTL or special.  These characters have a self-defining token type.
312             } else if (ch < 32 || ch >= 127 || atomDelimiters.indexOf(ch) != -1) {
313                 throw new ResponseFormatException("Invalid string value: " + ch);
314             } else {
315                 // only process this if we're allowing NIL as an option.
316                 if (nilAllowed) {
317                     // start of an atom, parse it off.
318                     Token token = next(true);
319                     if (token.isType(Token.NIL)) {
320                         return null;
321                     }
322                     // invalid token type.
323                     throw new ResponseFormatException("Invalid string value: " + token.getValue());
324                 }
325                 // invalid token type.
326                 throw new ResponseFormatException("Invalid string value: " + ch);
327             }
328         }
329     }
330 
331     /**
332      * Extract a substring from the response string and apply any
333      * escaping/folding rules to the string.
334      *
335      * @param start  The starting offset in the response.
336      * @param end    The response end offset + 1.
337      *
338      * @return The processed string value.
339      * @exception ResponseFormatException
340      */
341     private byte[] getEscapedValue(int start, int end) throws MessagingException {
342         ByteArrayOutputStream value = new ByteArrayOutputStream();
343 
344         for (int i = start; i < end; i++) {
345             byte ch = response[i];
346             // is this an escape character?
347             if (ch == '\\') {
348                 i++;
349                 if (i == end) {
350                     throw new ResponseFormatException("Invalid escape character");
351                 }
352                 value.write(response[i]);
353             }
354             // line breaks are ignored, except for naked '\n' characters, which are consider
355             // parts of linear whitespace.
356             else if (ch == '\r') {
357                 // see if this is a CRLF sequence, and skip the second if it is.
358                 if (i < end - 1 && response[i + 1] == '\n') {
359                     i++;
360                 }
361             }
362             else {
363                 // just append the ch value.
364                 value.write(ch);
365             }
366         }
367         return value.toByteArray();
368     }
369 
370     /**
371      * Parse out a quoted string from the response, applying escaping
372      * rules to the value.
373      *
374      * @return The QUOTEDSTRING token with the value.
375      * @exception ResponseFormatException
376      */
377     private Token readQuotedString() throws MessagingException {
378 
379         String value = new String(readQuotedStringData());
380         return new Token(Token.QUOTEDSTRING, value);
381     }
382 
383     /**
384      * Parse out a quoted string from the response, applying escaping
385      * rules to the value.
386      *
387      * @return The byte array with the resulting string bytes.
388      * @exception ResponseFormatException
389      */
390     private byte[] readQuotedStringData() throws MessagingException {
391         int start = pos + 1;
392         boolean requiresEscaping = false;
393 
394         // skip to end of comment/string
395         while (++pos < response.length) {
396             byte ch = response[pos];
397             if (ch == '"') {
398                 byte[] value;
399                 if (requiresEscaping) {
400                     value = getEscapedValue(start, pos);
401                 }
402                 else {
403                     value = subarray(start, pos);
404                 }
405                 // step over the delimiter for all cases.
406                 pos++;
407                 return value;
408             }
409             else if (ch == '\\') {
410                 pos++;
411                 requiresEscaping = true;
412             }
413             // we need to process line breaks also
414             else if (ch == '\r') {
415                 requiresEscaping = true;
416             }
417         }
418 
419         throw new ResponseFormatException("Missing '\"'");
420     }
421 
422 
423     /**
424      * Parse out a literal string from the response, using the length
425      * encoded before the listeral.
426      *
427      * @return The LITERAL token with the value.
428      * @exception ResponseFormatException
429      */
430     protected Token readLiteral() throws MessagingException {
431         String value = new String(readLiteralData());
432         return new Token(Token.LITERAL, value);
433     }
434 
435 
436     /**
437      * Parse out a literal string from the response, using the length
438      * encoded before the listeral.
439      *
440      * @return The byte[] array with the value.
441      * @exception ResponseFormatException
442      */
443     protected byte[] readLiteralData() throws MessagingException {
444         int lengthStart = pos + 1;
445 
446         // see if we have a close marker.
447         int lengthEnd = indexOf("}\r\n", lengthStart);
448         if (lengthEnd == -1) {
449             throw new ResponseFormatException("Missing terminator on literal length");
450         }
451 
452         int count = 0;
453         try {
454             count = Integer.parseInt(substring(lengthStart, lengthEnd));
455         } catch (NumberFormatException e) {
456             throw new ResponseFormatException("Invalid literal length " + substring(lengthStart, lengthEnd));
457         }
458         
459         // step over the length
460         pos = lengthEnd + 3;
461 
462         // too long?
463         if (pos + count > response.length) {
464             throw new ResponseFormatException("Invalid literal length: " + count);
465         }
466 
467         byte[] value = subarray(pos, pos + count);
468         pos += count;
469         
470         return value;
471     }
472 
473 
474     /**
475      * Extract a substring from the response buffer.
476      *
477      * @param start  The starting offset.
478      * @param end    The end offset (+ 1).
479      *
480      * @return A String extracted from the buffer.
481      */
482     protected String substring(int start, int end ) {
483         return new String(response, start, end - start);
484     }
485 
486 
487     /**
488      * Extract a subarray from the response buffer.
489      *
490      * @param start  The starting offset.
491      * @param end    The end offset (+ 1).
492      *
493      * @return A byte array string extracted rom the buffer.
494      */
495     protected byte[] subarray(int start, int end ) {
496         byte[] result = new byte[end - start];
497         System.arraycopy(response, start, result, 0, end - start);
498         return result;
499     }
500 
501 
502     /**
503      * Test if the bytes in the response buffer match a given
504      * string value.
505      *
506      * @param position The compare position.
507      * @param needle   The needle string we're testing for.
508      *
509      * @return True if the bytes match the needle value, false for any
510      *         mismatch.
511      */
512     public boolean match(int position, String needle) {
513         int length = needle.length();
514 
515         if (response.length - position < length) {
516             return false;
517         }
518 
519         for (int i = 0; i < length; i++) {
520             if (response[position + i ] != needle.charAt(i)) {
521                 return false;
522             }
523         }
524         return true;
525     }
526 
527 
528     /**
529      * Search for a given string starting from the current position
530      * cursor.
531      *
532      * @param needle The search string.
533      *
534      * @return The index of a match (in absolute byte position in the
535      *         response buffer).
536      */
537     public int indexOf(String needle) {
538         return indexOf(needle, pos);
539     }
540 
541     /**
542      * Search for a string in the response buffer starting from the
543      * indicated position.
544      *
545      * @param needle   The search string.
546      * @param position The starting buffer position.
547      *
548      * @return The index of the match position.  Returns -1 for no match.
549      */
550     public int indexOf(String needle, int position) {
551         // get the last possible match position
552         int last = response.length - needle.length();
553         // no match possible
554         if (last < position) {
555             return -1;
556         }
557 
558         for (int i = position; i <= last; i++) {
559             if (match(i, needle)) {
560                 return i;
561             }
562         }
563         return -1;
564     }
565 
566 
567 
568     /**
569      * Skip white space in the token string.
570      */
571     private void eatWhiteSpace() {
572         // skip to end of whitespace
573         while (++pos < response.length
574                 && WHITE.indexOf(response[pos]) != -1)
575             ;
576     }
577     
578     
579     /**
580      * Ensure that the next token in the parsed response is a
581      * '(' character.
582      *
583      * @exception ResponseFormatException
584      */
585     public void checkLeftParen() throws MessagingException {
586         Token token = next();
587         if (token.getType() != '(') {
588             throw new ResponseFormatException("Missing '(' in response");
589         }
590     }
591 
592 
593     /**
594      * Ensure that the next token in the parsed response is a
595      * ')' character.
596      *
597      * @exception ResponseFormatException
598      */
599     public void checkRightParen() throws MessagingException {
600         Token token = next();
601         if (token.getType() != ')') {
602             throw new ResponseFormatException("Missing ')' in response");
603         }
604     }
605 
606 
607     /**
608      * Read a string-valued token from the response.  A string
609      * valued token can be either a quoted string, a literal value,
610      * or an atom.  Any other token type is an error.
611      *
612      * @return The string value of the source token.
613      * @exception ResponseFormatException
614      */
615     public String readString() throws MessagingException {
616         Token token = next();
617         int type = token.getType();
618 
619         if (type != Token.ATOM && type != Token.QUOTEDSTRING && type != Token.LITERAL && type != Token.NUMERIC) {
620             throw new ResponseFormatException("String token expected in response: " + token.getValue());
621         }
622         return token.getValue();
623     }
624     
625 
626     /**
627      * Read an encoded string-valued token from the response.  A string
628      * valued token can be either a quoted string, a literal value,
629      * or an atom.  Any other token type is an error.
630      *
631      * @return The string value of the source token.
632      * @exception ResponseFormatException
633      */
634     public String readEncodedString() throws MessagingException {
635         String value = readString(); 
636         return decode(value); 
637     }
638 
639 
640     /**
641      * Decode a Base 64 encoded string value.
642      * 
643      * @param original The original encoded string.
644      * 
645      * @return The decoded string. 
646      * @exception MessagingException
647      */
648     public String decode(String original) throws MessagingException {
649         StringBuffer result = new StringBuffer();
650 
651         for (int i = 0; i < original.length(); i++) {
652             char ch = original.charAt(i);
653 
654             if (ch == '&') {
655                 i = decode(original, i, result);
656             }
657             else {
658                 result.append(ch);
659             }
660         }
661 
662         return result.toString();
663     }
664 
665 
666     /**
667      * Decode a section of an encoded string value. 
668      * 
669      * @param original The original source string.
670      * @param index    The current working index.
671      * @param result   The StringBuffer used for the decoded result.
672      * 
673      * @return The new index for the decoding operation. 
674      * @exception MessagingException
675      */
676     public static int decode(String original, int index, StringBuffer result) throws MessagingException {
677         // look for the section terminator
678         int terminator = original.indexOf('-', index);
679 
680         // unmatched?
681         if (terminator == -1) {
682             throw new MessagingException("Invalid UTF-7 encoded string");
683         }
684 
685         // is this just an escaped "&"?
686         if (terminator == index + 1) {
687             // append and skip over this.
688             result.append('&');
689             return index + 2;
690         }
691 
692         // step over the starting char
693         index++;
694 
695         int chars = terminator - index;
696         int quads = chars / 4;
697         int residual = chars % 4;
698 
699         // buffer for decoded characters
700         byte[] buffer = new byte[4];
701         int bufferCount = 0;
702 
703         // process each of the full triplet pairs
704         for (int i = 0; i < quads; i++) {
705             byte b1 = decodingTable[original.charAt(index++) & 0xff];
706             byte b2 = decodingTable[original.charAt(index++) & 0xff];
707             byte b3 = decodingTable[original.charAt(index++) & 0xff];
708             byte b4 = decodingTable[original.charAt(index++) & 0xff];
709 
710             buffer[bufferCount++] = (byte)((b1 << 2) | (b2 >> 4));
711             buffer[bufferCount++] = (byte)((b2 << 4) | (b3 >> 2));
712             buffer[bufferCount++] = (byte)((b3 << 6) | b4);
713 
714             // we've written 3 bytes to the buffer, but we might have a residual from a previous
715             // iteration to deal with.
716             if (bufferCount == 4) {
717                 // two complete chars here
718                 b1 = buffer[0];
719                 b2 = buffer[1];
720                 result.append((char)((b1 << 8) + (b2 & 0xff)));
721                 b1 = buffer[2];
722                 b2 = buffer[3];
723                 result.append((char)((b1 << 8) + (b2 & 0xff)));
724                 bufferCount = 0;
725             }
726             else {
727                 // we need to save the 3rd byte for the next go around
728                 b1 = buffer[0];
729                 b2 = buffer[1];
730                 result.append((char)((b1 << 8) + (b2 & 0xff)));
731                 buffer[0] = buffer[2];
732                 bufferCount = 1;
733             }
734         }
735 
736         // properly encoded, we should have an even number of bytes left.
737 
738         switch (residual) {
739             // no residual...so we better not have an extra in the buffer
740             case 0:
741                 // this is invalid...we have an odd number of bytes so far,
742                 if (bufferCount == 1) {
743                     throw new MessagingException("Invalid UTF-7 encoded string");
744                 }
745             // one byte left.  This shouldn't be valid.  We need at least 2 bytes to
746             // encode one unprintable char.
747             case 1:
748                 throw new MessagingException("Invalid UTF-7 encoded string");
749 
750             // ok, we have two bytes left, which can only encode a single byte.  We must have
751             // a dangling unhandled char.
752             case 2:
753             {
754                 if (bufferCount != 1) {
755                     throw new MessagingException("Invalid UTF-7 encoded string");
756                 }
757                 byte b1 = decodingTable[original.charAt(index++) & 0xff];
758                 byte b2 = decodingTable[original.charAt(index++) & 0xff];
759                 buffer[bufferCount++] = (byte)((b1 << 2) | (b2 >> 4));
760 
761                 b1 = buffer[0];
762                 b2 = buffer[1];
763                 result.append((char)((b1 << 8) + (b2 & 0xff)));
764                 break;
765             }
766 
767             // we have 2 encoded chars.  In this situation, we can't have a leftover.
768             case 3:
769             {
770                 // this is invalid...we have an odd number of bytes so far,
771                 if (bufferCount == 1) {
772                     throw new MessagingException("Invalid UTF-7 encoded string");
773                 }
774                 byte b1 = decodingTable[original.charAt(index++) & 0xff];
775                 byte b2 = decodingTable[original.charAt(index++) & 0xff];
776                 byte b3 = decodingTable[original.charAt(index++) & 0xff];
777 
778                 buffer[bufferCount++] = (byte)((b1 << 2) | (b2 >> 4));
779                 buffer[bufferCount++] = (byte)((b2 << 4) | (b3 >> 2));
780 
781                 b1 = buffer[0];
782                 b2 = buffer[1];
783                 result.append((char)((b1 << 8) + (b2 & 0xff)));
784                 break;
785             }
786         }
787 
788         // return the new scan location
789         return terminator + 1;
790     }
791 
792     /**
793      * Read a string-valued token from the response, verifying this is an ATOM token.
794      *
795      * @return The string value of the source token.
796      * @exception ResponseFormatException
797      */
798     public String readAtom() throws MessagingException {
799         return readAtom(false); 
800     }
801     
802 
803     /**
804      * Read a string-valued token from the response, verifying this is an ATOM token.
805      *
806      * @return The string value of the source token.
807      * @exception ResponseFormatException
808      */
809     public String readAtom(boolean expandedDelimiters) throws MessagingException {
810         Token token = next(false, expandedDelimiters);
811         int type = token.getType();
812 
813         if (type != Token.ATOM) {
814             throw new ResponseFormatException("ATOM token expected in response: " + token.getValue());
815         }
816         return token.getValue();
817     }
818 
819 
820     /**
821      * Read a number-valued token from the response.  This must be an ATOM
822      * token.
823      *
824      * @return The integer value of the source token.
825      * @exception ResponseFormatException
826      */
827     public int readInteger() throws MessagingException {
828         Token token = next();
829         return token.getInteger(); 
830     }
831 
832 
833     /**
834      * Read a number-valued token from the response.  This must be an ATOM
835      * token.
836      *
837      * @return The long value of the source token.
838      * @exception ResponseFormatException
839      */
840     public int readLong() throws MessagingException {
841         Token token = next();
842         return token.getInteger(); 
843     }
844 
845 
846     /**
847      * Read a string-valued token from the response.  A string
848      * valued token can be either a quoted string, a literal value,
849      * or an atom.  Any other token type is an error.
850      *
851      * @return The string value of the source token.
852      * @exception ResponseFormatException
853      */
854     public String readStringOrNil() throws MessagingException {
855         // we need to recognize the NIL token.
856         Token token = next(true);
857         int type = token.getType();
858 
859         if (type != Token.ATOM && type != Token.QUOTEDSTRING && type != Token.LITERAL && type != Token.NIL) {
860             throw new ResponseFormatException("String token or NIL expected in response: " + token.getValue());
861         }
862         // this returns null if the token is the NIL token.
863         return token.getValue();
864     }
865 
866 
867     /**
868      * Read a quoted string-valued token from the response.
869      * Any other token type other than NIL is an error.
870      *
871      * @return The string value of the source token.
872      * @exception ResponseFormatException
873      */
874     protected String readQuotedStringOrNil() throws MessagingException {
875         // we need to recognize the NIL token.
876         Token token = next(true);
877         int type = token.getType();
878 
879         if (type != Token.QUOTEDSTRING && type != Token.NIL) {
880             throw new ResponseFormatException("String token or NIL expected in response");
881         }
882         // this returns null if the token is the NIL token.
883         return token.getValue();
884     }
885 
886 
887     /**
888      * Read a date from a response string.  This is expected to be in
889      * Internet Date format, but there's a lot of variation implemented
890      * out there.  If we're unable to format this correctly, we'll
891      * just return null.
892      *
893      * @return A Date object created from the source date.
894      */
895     public Date readDate() throws MessagingException {
896         String value = readString();
897 
898         try {
899             return dateParser.parse(value);
900         } catch (Exception e) {
901             // we're just skipping over this, so return null
902             return null;
903         }
904     }
905 
906 
907     /**
908      * Read a date from a response string.  This is expected to be in
909      * Internet Date format, but there's a lot of variation implemented
910      * out there.  If we're unable to format this correctly, we'll
911      * just return null.
912      *
913      * @return A Date object created from the source date.
914      */
915     public Date readDateOrNil() throws MessagingException {
916         String value = readStringOrNil();
917         // this might be optional
918         if (value == null) {
919             return null; 
920         }
921 
922         try {
923             return dateParser.parse(value);
924         } catch (Exception e) {
925             // we're just skipping over this, so return null
926             return null;
927         }
928     }
929 
930     /**
931      * Read an internet address from a Fetch response.  The
932      * addresses are returned as a set of string tokens in the
933      * order "personal list mailbox host".  Any of these tokens
934      * can be NIL.
935      *
936      * The address may also be the start of a group list, which
937      * is indicated by the host being NIL.  If we have found the
938      * start of a group, then we need to parse multiple elements
939      * until we find the group end marker (indicated by both the
940      * mailbox and the host being NIL), and create a group
941      * InternetAddress instance from this.
942      *
943      * @return An InternetAddress instance parsed from the
944      *         element.
945      * @exception ResponseFormatException
946      */
947     public InternetAddress readAddress() throws MessagingException {
948         // we recurse, expecting a null response back for sublists.  
949         if (peek().getType() != '(') {
950             return null; 
951         }
952         
953         // must start with a paren
954         checkLeftParen(); 
955 
956         // personal information
957         String personal = readStringOrNil();
958         // the domain routine information.
959         String routing = readStringOrNil();
960         // the target mailbox
961         String mailbox = readStringOrNil();
962         // and finally the host
963         String host = readStringOrNil();
964         // and validate the closing paren
965         checkRightParen();
966 
967         // if this is a real address, we need to compose
968         if (host != null) {
969             StringBuffer address = new StringBuffer();
970             if (routing != null) {
971                 address.append(routing);
972                 address.append(':');
973             }
974             address.append(mailbox);
975             address.append('@');
976             address.append(host);
977 
978             try {
979                 return new InternetAddress(address.toString(), personal);
980             } catch (UnsupportedEncodingException e) {
981                 throw new ResponseFormatException("Invalid Internet address format");
982             }
983         }
984         else {
985             // we're going to recurse on this.  If the mailbox is null (the group name), this is the group item
986             // terminator.
987             if (mailbox == null) {
988                 return null;
989             }
990 
991             StringBuffer groupAddress = new StringBuffer();
992 
993             groupAddress.append(mailbox);
994             groupAddress.append(':');
995             int count = 0;
996 
997             while (true) {
998                 // now recurse until we hit the end of the list
999                 InternetAddress member = readAddress();
1000                 if (member == null) {
1001                     groupAddress.append(';');
1002 
1003                     try {
1004                         return new InternetAddress(groupAddress.toString(), personal);
1005                     } catch (UnsupportedEncodingException e) {
1006                         throw new ResponseFormatException("Invalid Internet address format");
1007                     }
1008                 }
1009                 else {
1010                     if (count != 0) {
1011                         groupAddress.append(',');
1012                     }
1013                     groupAddress.append(member.toString());
1014                     count++;
1015                 }
1016             }
1017         }
1018     }
1019 
1020 
1021     /**
1022      * Parse out a list of addresses.  This list of addresses is
1023      * surrounded by parentheses, and each address is also
1024      * parenthized (SP?).
1025      *
1026      * @return An array of the parsed addresses.
1027      * @exception ResponseFormatException
1028      */
1029     public InternetAddress[] readAddressList() throws MessagingException {
1030         // must start with a paren, but can be NIL also.
1031         Token token = next(true);
1032         int type = token.getType();
1033 
1034         // either of these results in a null address.  The caller determines based on
1035         // context whether this was optional or not.
1036         if (type == Token.NIL) {
1037             return null;
1038         }
1039         // non-nil address and no paren.  This is a syntax error.
1040         else if (type != '(') {
1041             throw new ResponseFormatException("Missing '(' in response");
1042         }
1043 
1044         List addresses = new ArrayList();
1045 
1046         // we have a list, now parse it.
1047         while (notListEnd()) {
1048             // go read the next address.  If we had an address, add to the list.
1049             // an address ITEM cannot be NIL inside the parens. 
1050             InternetAddress address = readAddress();
1051             addresses.add(address);
1052         }
1053         // we need to skip over the peeked token.
1054         checkRightParen(); 
1055         return (InternetAddress[])addresses.toArray(new InternetAddress[addresses.size()]);
1056     }
1057 
1058 
1059     /**
1060      * Check to see if we're at the end of a parenthized list
1061      * without advancing the parsing pointer.  If we are at the
1062      * end, then this will step over the closing paren.
1063      *
1064      * @return True if the next token is a closing list paren, false otherwise.
1065      * @exception ResponseFormatException
1066      */
1067     public boolean checkListEnd() throws MessagingException {
1068         Token token = peek(true);
1069         if (token.getType() == ')') {
1070             // step over this token.
1071             next();
1072             return true;
1073         }
1074         return false;
1075     }
1076 
1077 
1078     /**
1079      * Reads a string item which can be encoded either as a single
1080      * string-valued token or a parenthized list of string tokens.
1081      *
1082      * @return A List containing all of the strings.
1083      * @exception ResponseFormatException
1084      */
1085     public List readStringList() throws MessagingException {
1086         Token token = peek();
1087 
1088         List list = new ArrayList();
1089 
1090         if (token.getType() == '(') {
1091             next();
1092 
1093             while (notListEnd()) {
1094                 String value = readString();
1095                 // this can be NIL, technically
1096                 if (value != null) {
1097                     list.add(value);
1098                 }
1099             }
1100             // step over the closing paren 
1101             next(); 
1102         }
1103         else {
1104             // just a single string value.
1105             String value = readString();
1106             // this can be NIL, technically
1107             if (value != null) {
1108                 list.add(value);
1109             }
1110         }
1111         return list;
1112     }
1113 
1114 
1115     /**
1116      * Reads all remaining tokens and returns them as a list of strings. 
1117      * NIL values are not supported. 
1118      *
1119      * @return A List containing all of the strings.
1120      * @exception ResponseFormatException
1121      */
1122     public List readStrings() throws MessagingException {
1123         List list = new ArrayList();
1124         
1125         while (hasMore()) {
1126             String value = readString();
1127             list.add(value);
1128         }
1129         return list; 
1130     }
1131 
1132 
1133     /**
1134      * Skip over an extension item.  This may be either a string
1135      * token or a parenthized item (with potential nesting).
1136      *
1137      * At the point where this is called, we're looking for a closing
1138      * ')', but we know it is not that.  An EOF is an error, however,
1139      */
1140     public void skipExtensionItem() throws MessagingException {
1141         Token token = next();
1142         int type = token.getType();
1143 
1144         // list form?  Scan to find the correct list closure.
1145         if (type == '(') {
1146             skipNestedValue();
1147         }
1148         // found an EOF?  Big problem
1149         else if (type == Token.EOF) {
1150             throw new ResponseFormatException("Missing ')'");
1151         }
1152     }
1153 
1154     /**
1155      * Skip over a parenthized value that we're not interested in.
1156      * These lists may contain nested sublists, so we need to
1157      * handle the nesting properly.
1158      */
1159     public void skipNestedValue() throws MessagingException {
1160         Token token = next();
1161 
1162         while (true) {
1163             int type = token.getType();
1164             // list terminator?
1165             if (type == ')') {
1166                 return;
1167             }
1168             // unexpected end of the tokens.
1169             else if (type == Token.EOF) {
1170                 throw new ResponseFormatException("Missing ')'");
1171             }
1172             // encountered a nested list?
1173             else if (type == '(') {
1174                 // recurse and finish this list.
1175                 skipNestedValue();
1176             }
1177             // we're just skipping the token.
1178             token = next();
1179         }
1180     }
1181 
1182     /**
1183      * Get the next token and verify that it's of the expected type
1184      * for the context.
1185      *
1186      * @param type   The type of token we're expecting.
1187      */
1188     public void checkToken(int type) throws MessagingException {
1189         Token token = next();
1190         if (token.getType() != type) {
1191             throw new ResponseFormatException("Unexpected token: " + token.getValue());
1192         }
1193     }
1194 
1195 
1196     /**
1197      * Read the next token as binary data.  The next token can be a literal, a quoted string, or
1198      * the token NIL (which returns a null result).  Any other token throws a ResponseFormatException.
1199      *
1200      * @return A byte array representing the rest of the response data.
1201      */
1202     public byte[] readByteArray() throws MessagingException {
1203         return readData(true);
1204     }
1205     
1206     
1207     /**
1208      * Determine what type of token encoding needs to be 
1209      * used for a string value.
1210      * 
1211      * @param value  The string to test.
1212      * 
1213      * @return Either Token.ATOM, Token.QUOTEDSTRING, or 
1214      *         Token.LITERAL, depending on the characters contained
1215      *         in the value.
1216      */
1217     static public int getEncoding(byte[] value) {
1218         
1219         // a null string always needs to be represented as a quoted literal. 
1220         if (value.length == 0) {
1221             return Token.QUOTEDSTRING; 
1222         }
1223         
1224         for (int i = 0; i < value.length; i++) {
1225             int ch = value[i]; 
1226             // make sure the sign extension is eliminated 
1227             ch = ch & 0xff;
1228             // check first for any characters that would 
1229             // disqualify a quoted string 
1230             // NULL
1231             if (ch == 0x00) {
1232                 return Token.LITERAL; 
1233             }
1234             // non-7bit ASCII
1235             if (ch > 0x7F) {
1236                 return Token.LITERAL; 
1237             }
1238             // carriage return
1239             if (ch == '\r') {
1240                 return Token.LITERAL; 
1241             }
1242             // linefeed 
1243             if (ch == '\n') {
1244                 return Token.LITERAL; 
1245             }
1246             // now check for ATOM disqualifiers 
1247             if (atomDelimiters.indexOf(ch) != -1) {
1248                 return Token.QUOTEDSTRING; 
1249             }
1250             // CTL character.  We've already eliminated the high characters 
1251             if (ch < 0x20) {
1252                 return Token.QUOTEDSTRING; 
1253             }
1254         }
1255         // this can be an ATOM token 
1256         return Token.ATOM;
1257     }
1258     
1259     
1260     /**
1261      * Read a ContentType or ContentDisposition parameter 
1262      * list from an IMAP command response.
1263      * 
1264      * @return A ParameterList instance containing the parameters. 
1265      * @exception MessagingException
1266      */
1267     public ParameterList readParameterList() throws MessagingException {
1268         ParameterList params = new ParameterList(); 
1269         
1270         // read the tokens, taking NIL into account. 
1271         Token token = next(true, false); 
1272         
1273         // just return an empty list if this is NIL 
1274         if (token.isType(token.NIL)) {
1275             return params; 
1276         }
1277         
1278         // these are pairs of strings for each parameter value 
1279         while (notListEnd()) {
1280             String name = readString(); 
1281             String value = readString(); 
1282             params.set(name, value); 
1283         }
1284         // we need to consume the list terminator 
1285         checkRightParen(); 
1286         return params; 
1287     }
1288     
1289     
1290     /**
1291      * Test if we have more data in the response buffer.
1292      * 
1293      * @return true if there are more tokens to process.  false if 
1294      *         we've reached the end of the stream.
1295      */
1296     public boolean hasMore() throws MessagingException {
1297         // we need to eat any white space that might be in the stream.  
1298         eatWhiteSpace();
1299         return pos < response.length; 
1300     }
1301     
1302     
1303     /**
1304      * Tests if we've reached the end of a parenthetical
1305      * list in our parsing stream.
1306      * 
1307      * @return true if the next token will be a ')'.  false if the 
1308      *         next token is anything else.
1309      * @exception MessagingException
1310      */
1311     public boolean notListEnd() throws MessagingException {
1312         return peek().getType() != ')';
1313     }
1314     
1315     /**
1316      * Read a list of Flag values from an IMAP response, 
1317      * returning a Flags instance containing the appropriate 
1318      * pieces. 
1319      * 
1320      * @return A Flags instance with the flag values. 
1321      * @exception MessagingException
1322      */
1323     public Flags readFlagList() throws MessagingException {
1324         Flags flags = new Flags();
1325         
1326         // this should be a list here 
1327         checkLeftParen(); 
1328         
1329         // run through the flag list 
1330         while (notListEnd()) {
1331             // the flags are a bit of a pain.  The flag names include "\" in the name, which 
1332             // is not a character allowed in an atom.  This requires a bit of customized parsing 
1333             // to handle this. 
1334             Token token = next(); 
1335             // flags can be specified as just atom tokens, so allow this as a user flag. 
1336             if (token.isType(token.ATOM)) {
1337                 // append the atom as a raw name 
1338                 flags.add(token.getValue()); 
1339             }
1340             // all of the system flags start with a '\' followed by 
1341             // an atom.  They also can be extension flags.  IMAP has a special 
1342             // case of "\*" that we need to check for. 
1343             else if (token.isType('\\')) {
1344                 token = next(); 
1345                 // the next token is the real bit we need to process. 
1346                 if (token.isType('*')) {
1347                     // this indicates USER flags are allowed. 
1348                     flags.add(Flags.Flag.USER); 
1349                 }
1350                 // if this is an atom name, handle as a system flag 
1351                 else if (token.isType(Token.ATOM)) {
1352                     String name = token.getValue(); 
1353                     if (name.equalsIgnoreCase("Seen")) {
1354                         flags.add(Flags.Flag.SEEN);
1355                     }
1356                     else if (name.equalsIgnoreCase("RECENT")) {
1357                         flags.add(Flags.Flag.RECENT);
1358                     }
1359                     else if (name.equalsIgnoreCase("DELETED")) {
1360                         flags.add(Flags.Flag.DELETED);
1361                     }
1362                     else if (name.equalsIgnoreCase("ANSWERED")) {
1363                         flags.add(Flags.Flag.ANSWERED);
1364                     }
1365                     else if (name.equalsIgnoreCase("DRAFT")) {
1366                         flags.add(Flags.Flag.DRAFT);
1367                     }
1368                     else if (name.equalsIgnoreCase("FLAGGED")) {
1369                         flags.add(Flags.Flag.FLAGGED);
1370                     }
1371                     else {
1372                         // this is a server defined flag....just add the name with the 
1373                         // flag thingy prepended. 
1374                         flags.add("\\" + name); 
1375                     }
1376                 }
1377                 else {
1378                     throw new MessagingException("Invalid Flag: " + token.getValue()); 
1379                 }
1380             }
1381             else {
1382                 throw new MessagingException("Invalid Flag: " + token.getValue()); 
1383             }
1384         }
1385         
1386         // step over this for good practice. 
1387         checkRightParen(); 
1388         
1389         return flags; 
1390     }
1391     
1392     
1393     /**
1394      * Read a list of Flag values from an IMAP response, 
1395      * returning a Flags instance containing the appropriate 
1396      * pieces. 
1397      * 
1398      * @return A Flags instance with the flag values. 
1399      * @exception MessagingException
1400      */
1401     public List readSystemNameList() throws MessagingException {
1402         List flags = new ArrayList(); 
1403         
1404         // this should be a list here 
1405         checkLeftParen(); 
1406         
1407         // run through the flag list 
1408         while (notListEnd()) {
1409             // the flags are a bit of a pain.  The flag names include "\" in the name, which 
1410             // is not a character allowed in an atom.  This requires a bit of customized parsing 
1411             // to handle this. 
1412             Token token = next(); 
1413             // all of the system flags start with a '\' followed by 
1414             // an atom.  They also can be extension flags.  IMAP has a special 
1415             // case of "\*" that we need to check for. 
1416             if (token.isType('\\')) {
1417                 token = next(); 
1418                 // if this is an atom name, handle as a system flag 
1419                 if (token.isType(Token.ATOM)) {
1420                     // add the token value to the list WITH the 
1421                     // flag indicator included.  The attributes method returns 
1422                     // these flag indicators, so we need to include it. 
1423                     flags.add("\\" + token.getValue()); 
1424                 }
1425                 else {
1426                     throw new MessagingException("Invalid Flag: " + token.getValue()); 
1427                 }
1428             }
1429             else {
1430                 throw new MessagingException("Invalid Flag: " + token.getValue()); 
1431             }
1432         }
1433         
1434         // step over this for good practice. 
1435         checkRightParen(); 
1436         
1437         return flags; 
1438     }
1439 }
1440