001    /**
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    package org.apache.geronimo.javamail.store.imap.connection;
019    
020    import java.io.ByteArrayOutputStream;
021    import java.io.UnsupportedEncodingException;
022    import java.util.ArrayList; 
023    import java.util.Date; 
024    import java.util.List; 
025    
026    import javax.mail.Flags;
027    import javax.mail.MessagingException;
028    import javax.mail.internet.InternetAddress;
029    import javax.mail.internet.MailDateFormat;
030    import javax.mail.internet.ParameterList;
031    
032    import org.apache.geronimo.javamail.util.ResponseFormatException; 
033    
034    /**
035     * @version $Rev: 670052 $ $Date: 2008-06-20 16:15:52 -0400 (Fri, 20 Jun 2008) $
036     */
037    public class IMAPResponseTokenizer {
038        /*
039         * set up the decoding table.
040         */
041        protected static final byte[] decodingTable = new byte[256];
042    
043        protected static void initializeDecodingTable()
044        {
045            for (int i = 0; i < IMAPCommand.encodingTable.length; i++)
046            {
047                decodingTable[IMAPCommand.encodingTable[i]] = (byte)i;
048            }
049        }
050    
051    
052        static {
053            initializeDecodingTable();
054        }
055        
056        // a singleton formatter for header dates.
057        protected static MailDateFormat dateParser = new MailDateFormat();
058        
059        
060        public static class Token {
061            // Constant values from J2SE 1.4 API Docs (Constant values)
062            public static final int ATOM = -1;
063            public static final int QUOTEDSTRING = -2;
064            public static final int LITERAL = -3;
065            public static final int NUMERIC = -4;
066            public static final int EOF = -5;
067            public static final int NIL = -6;
068            // special single character markers     
069            public static final int CONTINUATION = '-';
070            public static final int UNTAGGED = '*';
071                
072            /**
073             * The type indicator.  This will be either a specific type, represented by 
074             * a negative number, or the actual character value. 
075             */
076            private int type;
077            /**
078             * The String value associated with this token.  All tokens have a String value, 
079             * except for the EOF and NIL tokens. 
080             */
081            private String value;
082    
083            public Token(int type, String value) {
084                this.type = type;
085                this.value = value;
086            }
087    
088            public int getType() {
089                return type;
090            }
091    
092            public String getValue() {
093                return value;
094            }
095    
096            public boolean isType(int type) {
097                return this.type == type;
098            }
099    
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