geronimo-javamail_1.5/geronimo-javamail_1.5_provider/src/main/java/org/apache/geronimo/javamail/store/imap/connection/IMAPResponseTokenizer.java [37:1466]:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public class IMAPResponseTokenizer {
    /*
     * set up the decoding table.
     */
    protected static final byte[] decodingTable = new byte[256];

    protected static void initializeDecodingTable()
    {
        for (int i = 0; i < IMAPCommand.encodingTable.length; i++)
        {
            decodingTable[IMAPCommand.encodingTable[i]] = (byte)i;
        }
    }


    static {
        initializeDecodingTable();
    }

    // a singleton formatter for header dates.
    protected static MailDateFormat dateParser = new MailDateFormat();


    public static class Token {
        // Constant values from J2SE 1.4 API Docs (Constant values)
        public static final int ATOM = -1;
        public static final int QUOTEDSTRING = -2;
        public static final int LITERAL = -3;
        public static final int NUMERIC = -4;
        public static final int EOF = -5;
        public static final int NIL = -6;
        // special single character markers
        public static final int CONTINUATION = '+';
        public static final int UNTAGGED = '*';

        /**
         * The type indicator.  This will be either a specific type, represented by
         * a negative number, or the actual character value.
         */
        private int type;
        /**
         * The String value associated with this token.  All tokens have a String value,
         * except for the EOF and NIL tokens.
         */
        private String value;

        public Token(int type, String value) {
            this.type = type;
            this.value = value;
        }

        public int getType() {
            return type;
        }

        public String getValue() {
            return value;
        }

        public boolean isType(int type) {
            return this.type == type;
        }

        /**
         * Return the token as an integer value.  If this can't convert, an exception is
         * thrown.
         *
         * @return The integer value of the token.
         * @exception ResponseFormatException
         */
        public int getInteger() throws MessagingException {
            if (value != null) {
                try {
                    return Integer.parseInt(value);
                } catch (NumberFormatException e) {
                }
            }

            throw new ResponseFormatException("Number value expected in response; fount: " + value);
        }

        /**
         * Return the token as a long value.  If it can't convert, an exception is
         * thrown.
         *
         * @return The token as a long value.
         * @exception ResponseFormatException
         */
        public long getLong() throws MessagingException {
            if (value != null) {
                try {
                    return Long.parseLong(value);
                } catch (NumberFormatException e) {
                }
            }
            throw new ResponseFormatException("Number value expected in response; fount: " + value);
        }

        /**
         * Handy debugging toString() method for token.
         *
         * @return The string value of the token.
         */
        public String toString() {
            if (type == NIL) {
                return "NIL";
            }
            else if (type == EOF) {
                return "EOF";
            }

            if (value == null) {
                return "";
            }
            return value;
        }
    }

    public static final Token EOF = new Token(Token.EOF, null);
    public static final Token NIL = new Token(Token.NIL, null);

    private static final String WHITE = " \t\n\r";
    // The list of delimiter characters we process when
    // handling parsing of ATOMs.
    private static final String atomDelimiters = "(){}%*\"\\" + WHITE;
    // this set of tokens is a slighly expanded set used for
    // specific response parsing.  When dealing with Body
    // section names, there are sub pieces to the name delimited
    // by "[", "]", ".", "<", ">" and SPACE, so reading these using
    // a superset of the ATOM processing makes for easier parsing.
    private static final String tokenDelimiters = "<>[].(){}%*\"\\" + WHITE;

    // the response data read from the connection
    private byte[] response;
    // current parsing position
    private int pos;

    public IMAPResponseTokenizer(byte [] response) {
        this.response = response;
    }

    /**
     * Get the remainder of the response as a string.
     *
     * @return A string representing the remainder of the response.
     */
    public String getRemainder() {
        // make sure we're still in range
        if (pos >= response.length) {
            return "";
        }

        try {
            return new String(response, pos, response.length - pos, "ISO8859-1");
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }


    public Token next() throws MessagingException {
        return next(false);
    }

    public Token next(boolean nilAllowed) throws MessagingException {
        return readToken(nilAllowed, false);
    }

    public Token next(boolean nilAllowed, boolean expandedDelimiters) throws MessagingException {
        return readToken(nilAllowed, expandedDelimiters);
    }

    public Token peek() throws MessagingException {
        return peek(false, false);
    }

    public Token peek(boolean nilAllowed) throws MessagingException {
        return peek(nilAllowed, false);
    }

    public Token peek(boolean nilAllowed, boolean expandedDelimiters) throws MessagingException {
        int start = pos;
        try {
            return readToken(nilAllowed, expandedDelimiters);
        } finally {
            pos = start;
        }
    }

    /**
     * Read an ATOM token from the parsed response.
     *
     * @return A token containing the value of the atom token.
     */
    private Token readAtomicToken(String delimiters) {
        // skip to next delimiter
        int start = pos;
        while (++pos < response.length) {
            // break on the first non-atom character.
            byte ch = response[pos];
            if (delimiters.indexOf(response[pos]) != -1 || ch < 32 || ch >= 127) {
                break;
            }
        }

        try {
            // Numeric tokens we store as a different type.
            String value = new String(response, start, pos - start, "ISO8859-1");
            try {
                int intValue = Integer.parseInt(value);
                return new Token(Token.NUMERIC, value);
            } catch (NumberFormatException e) {
            }
            return new Token(Token.ATOM, value);
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }

    /**
     * Read the next token from the response.
     *
     * @return The next token from the response.  White space is skipped, and comment
     *         tokens are also skipped if indicated.
     * @exception ResponseFormatException
     */
    private Token readToken(boolean nilAllowed, boolean expandedDelimiters) throws MessagingException {
        String delimiters = expandedDelimiters ? tokenDelimiters : atomDelimiters;

        if (pos >= response.length) {
            return EOF;
        } else {
            byte ch = response[pos];
            if (ch == '\"') {
                return readQuotedString();
            // beginning of a length-specified literal?
            } else if (ch == '{') {
                return readLiteral();
            // white space, eat this and find a real token.
            } else if (WHITE.indexOf(ch) != -1) {
                eatWhiteSpace();
                return readToken(nilAllowed, expandedDelimiters);
            // either a CTL or special.  These characters have a self-defining token type.
            } else if (ch < 32 || ch >= 127 || delimiters.indexOf(ch) != -1) {
                pos++;
                return new Token((int)ch, String.valueOf((char)ch));
            } else {
                // start of an atom, parse it off.
                Token token = readAtomicToken(delimiters);
                // now, if we've been asked to look at NIL tokens, check to see if it is one,
                // and return that instead of the ATOM.
                if (nilAllowed) {
                    if (token.getValue().equalsIgnoreCase("NIL")) {
                        return NIL;
                    }
                }
                return token;
            }
        }
    }

    /**
     * Read the next token from the response, returning it as a byte array value.
     *
     * @return The next token from the response.  White space is skipped, and comment
     *         tokens are also skipped if indicated.
     * @exception ResponseFormatException
     */
    private byte[] readData(boolean nilAllowed) throws MessagingException {
        if (pos >= response.length) {
            return null;
        } else {
            byte ch = response[pos];
            if (ch == '\"') {
                return readQuotedStringData();
            // beginning of a length-specified literal?
            } else if (ch == '{') {
                return readLiteralData();
            // white space, eat this and find a real token.
            } else if (WHITE.indexOf(ch) != -1) {
                eatWhiteSpace();
                return readData(nilAllowed);
            // either a CTL or special.  These characters have a self-defining token type.
            } else if (ch < 32 || ch >= 127 || atomDelimiters.indexOf(ch) != -1) {
                throw new ResponseFormatException("Invalid string value: " + ch);
            } else {
                // only process this if we're allowing NIL as an option.
                if (nilAllowed) {
                    // start of an atom, parse it off.
                    Token token = next(true);
                    if (token.isType(Token.NIL)) {
                        return null;
                    }
                    // invalid token type.
                    throw new ResponseFormatException("Invalid string value: " + token.getValue());
                }
                // invalid token type.
                throw new ResponseFormatException("Invalid string value: " + ch);
            }
        }
    }

    /**
     * Extract a substring from the response string and apply any
     * escaping/folding rules to the string.
     *
     * @param start  The starting offset in the response.
     * @param end    The response end offset + 1.
     *
     * @return The processed string value.
     * @exception ResponseFormatException
     */
    private byte[] getEscapedValue(int start, int end) throws MessagingException {
        ByteArrayOutputStream value = new ByteArrayOutputStream();

        for (int i = start; i < end; i++) {
            byte ch = response[i];
            // is this an escape character?
            if (ch == '\\') {
                i++;
                if (i == end) {
                    throw new ResponseFormatException("Invalid escape character");
                }
                value.write(response[i]);
            }
            // line breaks are ignored, except for naked '\n' characters, which are consider
            // parts of linear whitespace.
            else if (ch == '\r') {
                // see if this is a CRLF sequence, and skip the second if it is.
                if (i < end - 1 && response[i + 1] == '\n') {
                    i++;
                }
            }
            else {
                // just append the ch value.
                value.write(ch);
            }
        }
        return value.toByteArray();
    }

    /**
     * Parse out a quoted string from the response, applying escaping
     * rules to the value.
     *
     * @return The QUOTEDSTRING token with the value.
     * @exception ResponseFormatException
     */
    private Token readQuotedString() throws MessagingException {
        try {
            String value = new String(readQuotedStringData(), "ISO8859-1");
            return new Token(Token.QUOTEDSTRING, value);
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }

    /**
     * Parse out a quoted string from the response, applying escaping
     * rules to the value.
     *
     * @return The byte array with the resulting string bytes.
     * @exception ResponseFormatException
     */
    private byte[] readQuotedStringData() throws MessagingException {
        int start = pos + 1;
        boolean requiresEscaping = false;

        // skip to end of comment/string
        while (++pos < response.length) {
            byte ch = response[pos];
            if (ch == '"') {
                byte[] value;
                if (requiresEscaping) {
                    value = getEscapedValue(start, pos);
                }
                else {
                    value = subarray(start, pos);
                }
                // step over the delimiter for all cases.
                pos++;
                return value;
            }
            else if (ch == '\\') {
                pos++;
                requiresEscaping = true;
            }
            // we need to process line breaks also
            else if (ch == '\r') {
                requiresEscaping = true;
            }
        }

        throw new ResponseFormatException("Missing '\"'");
    }


    /**
     * Parse out a literal string from the response, using the length
     * encoded before the listeral.
     *
     * @return The LITERAL token with the value.
     * @exception ResponseFormatException
     */
    protected Token readLiteral() throws MessagingException {
        try {
            String value = new String(readLiteralData(), "ISO8859-1");
            return new Token(Token.LITERAL, value);
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }


    /**
     * Parse out a literal string from the response, using the length
     * encoded before the listeral.
     *
     * @return The byte[] array with the value.
     * @exception ResponseFormatException
     */
    protected byte[] readLiteralData() throws MessagingException {
        int lengthStart = pos + 1;

        // see if we have a close marker.
        int lengthEnd = indexOf("}\r\n", lengthStart);
        if (lengthEnd == -1) {
            throw new ResponseFormatException("Missing terminator on literal length");
        }

        int count = 0;
        try {
            count = Integer.parseInt(substring(lengthStart, lengthEnd));
        } catch (NumberFormatException e) {
            throw new ResponseFormatException("Invalid literal length " + substring(lengthStart, lengthEnd));
        }

        // step over the length
        pos = lengthEnd + 3;

        // too long?
        if (pos + count > response.length) {
            throw new ResponseFormatException("Invalid literal length: " + count);
        }

        byte[] value = subarray(pos, pos + count);
        pos += count;

        return value;
    }


    /**
     * Extract a substring from the response buffer.
     *
     * @param start  The starting offset.
     * @param end    The end offset (+ 1).
     *
     * @return A String extracted from the buffer.
     */
    protected String substring(int start, int end ) {
        try {
            return new String(response, start, end - start, "ISO8859-1");
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }


    /**
     * Extract a subarray from the response buffer.
     *
     * @param start  The starting offset.
     * @param end    The end offset (+ 1).
     *
     * @return A byte array string extracted rom the buffer.
     */
    protected byte[] subarray(int start, int end ) {
        byte[] result = new byte[end - start];
        System.arraycopy(response, start, result, 0, end - start);
        return result;
    }


    /**
     * Test if the bytes in the response buffer match a given
     * string value.
     *
     * @param position The compare position.
     * @param needle   The needle string we're testing for.
     *
     * @return True if the bytes match the needle value, false for any
     *         mismatch.
     */
    public boolean match(int position, String needle) {
        int length = needle.length();

        if (response.length - position < length) {
            return false;
        }

        for (int i = 0; i < length; i++) {
            if (response[position + i ] != needle.charAt(i)) {
                return false;
            }
        }
        return true;
    }


    /**
     * Search for a given string starting from the current position
     * cursor.
     *
     * @param needle The search string.
     *
     * @return The index of a match (in absolute byte position in the
     *         response buffer).
     */
    public int indexOf(String needle) {
        return indexOf(needle, pos);
    }

    /**
     * Search for a string in the response buffer starting from the
     * indicated position.
     *
     * @param needle   The search string.
     * @param position The starting buffer position.
     *
     * @return The index of the match position.  Returns -1 for no match.
     */
    public int indexOf(String needle, int position) {
        // get the last possible match position
        int last = response.length - needle.length();
        // no match possible
        if (last < position) {
            return -1;
        }

        for (int i = position; i <= last; i++) {
            if (match(i, needle)) {
                return i;
            }
        }
        return -1;
    }



    /**
     * Skip white space in the token string.
     */
    private void eatWhiteSpace() {
        // skip to end of whitespace
        while (++pos < response.length
                && WHITE.indexOf(response[pos]) != -1)
            ;
    }


    /**
     * Ensure that the next token in the parsed response is a
     * '(' character.
     *
     * @exception ResponseFormatException
     */
    public void checkLeftParen() throws MessagingException {
        Token token = next();
        if (token.getType() != '(') {
            throw new ResponseFormatException("Missing '(' in response");
        }
    }


    /**
     * Ensure that the next token in the parsed response is a
     * ')' character.
     *
     * @exception ResponseFormatException
     */
    public void checkRightParen() throws MessagingException {
        Token token = next();
        if (token.getType() != ')') {
            throw new ResponseFormatException("Missing ')' in response");
        }
    }


    /**
     * Read a string-valued token from the response.  A string
     * valued token can be either a quoted string, a literal value,
     * or an atom.  Any other token type is an error.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readString() throws MessagingException {
        Token token = next(true);
        int type = token.getType();
        if (type == Token.NIL) {
            return null;
        }
        if (type != Token.ATOM && type != Token.QUOTEDSTRING && type != Token.LITERAL && type != Token.NUMERIC) {
            throw new ResponseFormatException("String token expected in response: " + token.getValue());
        }
        return token.getValue();
    }


    /**
     * Read an encoded string-valued token from the response.  A string
     * valued token can be either a quoted string, a literal value,
     * or an atom.  Any other token type is an error.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readEncodedString() throws MessagingException {
        String value = readString();
        return decode(value);
    }


    /**
     * Decode a Base 64 encoded string value.
     *
     * @param original The original encoded string.
     *
     * @return The decoded string.
     * @exception MessagingException
     */
    public String decode(String original) throws MessagingException {
        StringBuffer result = new StringBuffer();

        for (int i = 0; i < original.length(); i++) {
            char ch = original.charAt(i);

            if (ch == '&') {
                i = decode(original, i, result);
            }
            else {
                result.append(ch);
            }
        }

        return result.toString();
    }


    /**
     * Decode a section of an encoded string value.
     *
     * @param original The original source string.
     * @param index    The current working index.
     * @param result   The StringBuffer used for the decoded result.
     *
     * @return The new index for the decoding operation.
     * @exception MessagingException
     */
    public static int decode(String original, int index, StringBuffer result) throws MessagingException {
        // look for the section terminator
        int terminator = original.indexOf('-', index);

        // unmatched?
        if (terminator == -1) {
            throw new MessagingException("Invalid UTF-7 encoded string");
        }

        // is this just an escaped "&"?
        if (terminator == index + 1) {
            // append and skip over this.
            result.append('&');
            return index + 2;
        }

        // step over the starting char
        index++;

        int chars = terminator - index;
        int quads = chars / 4;
        int residual = chars % 4;

        // buffer for decoded characters
        byte[] buffer = new byte[4];
        int bufferCount = 0;

        // process each of the full triplet pairs
        for (int i = 0; i < quads; i++) {
            byte b1 = decodingTable[original.charAt(index++) & 0xff];
            byte b2 = decodingTable[original.charAt(index++) & 0xff];
            byte b3 = decodingTable[original.charAt(index++) & 0xff];
            byte b4 = decodingTable[original.charAt(index++) & 0xff];

            buffer[bufferCount++] = (byte)((b1 << 2) | (b2 >> 4));
            buffer[bufferCount++] = (byte)((b2 << 4) | (b3 >> 2));
            buffer[bufferCount++] = (byte)((b3 << 6) | b4);

            // we've written 3 bytes to the buffer, but we might have a residual from a previous
            // iteration to deal with.
            if (bufferCount == 4) {
                // two complete chars here
                b1 = buffer[0];
                b2 = buffer[1];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                b1 = buffer[2];
                b2 = buffer[3];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                bufferCount = 0;
            }
            else {
                // we need to save the 3rd byte for the next go around
                b1 = buffer[0];
                b2 = buffer[1];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                buffer[0] = buffer[2];
                bufferCount = 1;
            }
        }

        // properly encoded, we should have an even number of bytes left.

        switch (residual) {
            // no residual...so we better not have an extra in the buffer
            case 0:
                // this is invalid...we have an odd number of bytes so far,
                if (bufferCount == 1) {
                    throw new MessagingException("Invalid UTF-7 encoded string");
                }
            // one byte left.  This shouldn't be valid.  We need at least 2 bytes to
            // encode one unprintable char.
            case 1:
                throw new MessagingException("Invalid UTF-7 encoded string");

            // ok, we have two bytes left, which can only encode a single byte.  We must have
            // a dangling unhandled char.
            case 2:
            {
                if (bufferCount != 1) {
                    throw new MessagingException("Invalid UTF-7 encoded string");
                }
                byte b1 = decodingTable[original.charAt(index++) & 0xff];
                byte b2 = decodingTable[original.charAt(index++) & 0xff];
                buffer[bufferCount++] = (byte)((b1 << 2) | (b2 >> 4));

                b1 = buffer[0];
                b2 = buffer[1];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                break;
            }

            // we have 2 encoded chars.  In this situation, we can't have a leftover.
            case 3:
            {
                // this is invalid...we have an odd number of bytes so far,
                if (bufferCount == 1) {
                    throw new MessagingException("Invalid UTF-7 encoded string");
                }
                byte b1 = decodingTable[original.charAt(index++) & 0xff];
                byte b2 = decodingTable[original.charAt(index++) & 0xff];
                byte b3 = decodingTable[original.charAt(index++) & 0xff];

                buffer[bufferCount++] = (byte)((b1 << 2) | (b2 >> 4));
                buffer[bufferCount++] = (byte)((b2 << 4) | (b3 >> 2));

                b1 = buffer[0];
                b2 = buffer[1];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                break;
            }
        }

        // return the new scan location
        return terminator + 1;
    }

    /**
     * Read a string-valued token from the response, verifying this is an ATOM token.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readAtom() throws MessagingException {
        return readAtom(false);
    }


    /**
     * Read a string-valued token from the response, verifying this is an ATOM token.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readAtom(boolean expandedDelimiters) throws MessagingException {
        Token token = next(false, expandedDelimiters);
        int type = token.getType();

        if (type != Token.ATOM) {
            throw new ResponseFormatException("ATOM token expected in response: " + token.getValue());
        }
        return token.getValue();
    }


    /**
     * Read a number-valued token from the response.  This must be an ATOM
     * token.
     *
     * @return The integer value of the source token.
     * @exception ResponseFormatException
     */
    public int readInteger() throws MessagingException {
        Token token = next();
        return token.getInteger();
    }


    /**
     * Read a number-valued token from the response.  This must be an ATOM
     * token.
     *
     * @return The long value of the source token.
     * @exception ResponseFormatException
     */
    public int readLong() throws MessagingException {
        Token token = next();
        return token.getInteger();
    }


    /**
     * Read a string-valued token from the response.  A string
     * valued token can be either a quoted string, a literal value,
     * or an atom.  Any other token type is an error.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readStringOrNil() throws MessagingException {
        // we need to recognize the NIL token.
        Token token = next(true);
        int type = token.getType();

        if (type != Token.ATOM && type != Token.QUOTEDSTRING && type != Token.LITERAL && type != Token.NIL) {
            throw new ResponseFormatException("String token or NIL expected in response: " + token.getValue());
        }
        // this returns null if the token is the NIL token.
        return token.getValue();
    }


    /**
     * Read a quoted string-valued token from the response.
     * Any other token type other than NIL is an error.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    protected String readQuotedStringOrNil() throws MessagingException {
        // we need to recognize the NIL token.
        Token token = next(true);
        int type = token.getType();

        if (type != Token.QUOTEDSTRING && type != Token.NIL) {
            throw new ResponseFormatException("String token or NIL expected in response");
        }
        // this returns null if the token is the NIL token.
        return token.getValue();
    }


    /**
     * Read a date from a response string.  This is expected to be in
     * Internet Date format, but there's a lot of variation implemented
     * out there.  If we're unable to format this correctly, we'll
     * just return null.
     *
     * @return A Date object created from the source date.
     */
    public Date readDate() throws MessagingException {
        String value = readString();

        try {
            return dateParser.parse(value);
        } catch (Exception e) {
            // we're just skipping over this, so return null
            return null;
        }
    }


    /**
     * Read a date from a response string.  This is expected to be in
     * Internet Date format, but there's a lot of variation implemented
     * out there.  If we're unable to format this correctly, we'll
     * just return null.
     *
     * @return A Date object created from the source date.
     */
    public Date readDateOrNil() throws MessagingException {
        String value = readStringOrNil();
        // this might be optional
        if (value == null) {
            return null;
        }

        try {
            return dateParser.parse(value);
        } catch (Exception e) {
            // we're just skipping over this, so return null
            return null;
        }
    }

    /**
     * Read an internet address from a Fetch response.  The
     * addresses are returned as a set of string tokens in the
     * order "personal list mailbox host".  Any of these tokens
     * can be NIL.
     *
     * The address may also be the start of a group list, which
     * is indicated by the host being NIL.  If we have found the
     * start of a group, then we need to parse multiple elements
     * until we find the group end marker (indicated by both the
     * mailbox and the host being NIL), and create a group
     * InternetAddress instance from this.
     *
     * @return An InternetAddress instance parsed from the
     *         element.
     * @exception ResponseFormatException
     */
    public InternetAddress readAddress() throws MessagingException {
        // we recurse, expecting a null response back for sublists.
        if (peek().getType() != '(') {
            return null;
        }

        // must start with a paren
        checkLeftParen();

        // personal information
        String personal = readStringOrNil();
        // the domain routine information.
        String routing = readStringOrNil();
        // the target mailbox
        String mailbox = readStringOrNil();
        // and finally the host
        String host = readStringOrNil();
        // and validate the closing paren
        checkRightParen();

        // if this is a real address, we need to compose
        if (host != null) {
            StringBuffer address = new StringBuffer();
            if (routing != null) {
                address.append(routing);
                address.append(':');
            }
            address.append(mailbox);
            address.append('@');
            address.append(host);

            try {
                return new InternetAddress(address.toString(), personal);
            } catch (UnsupportedEncodingException e) {
                throw new ResponseFormatException("Invalid Internet address format");
            }
        }
        else {
            // we're going to recurse on this.  If the mailbox is null (the group name), this is the group item
            // terminator.
            if (mailbox == null) {
                return null;
            }

            StringBuffer groupAddress = new StringBuffer();

            groupAddress.append(mailbox);
            groupAddress.append(':');
            int count = 0;

            while (true) {
                // now recurse until we hit the end of the list
                InternetAddress member = readAddress();
                if (member == null) {
                    groupAddress.append(';');

                    try {
                        return new InternetAddress(groupAddress.toString(), personal);
                    } catch (UnsupportedEncodingException e) {
                        throw new ResponseFormatException("Invalid Internet address format");
                    }
                }
                else {
                    if (count != 0) {
                        groupAddress.append(',');
                    }
                    groupAddress.append(member.toString());
                    count++;
                }
            }
        }
    }


    /**
     * Parse out a list of addresses.  This list of addresses is
     * surrounded by parentheses, and each address is also
     * parenthized (SP?).
     *
     * @return An array of the parsed addresses.
     * @exception ResponseFormatException
     */
    public InternetAddress[] readAddressList() throws MessagingException {
        // must start with a paren, but can be NIL also.
        Token token = next(true);
        int type = token.getType();

        // either of these results in a null address.  The caller determines based on
        // context whether this was optional or not.
        if (type == Token.NIL) {
            return null;
        }
        // non-nil address and no paren.  This is a syntax error.
        else if (type != '(') {
            throw new ResponseFormatException("Missing '(' in response");
        }

        List addresses = new ArrayList();

        // we have a list, now parse it.
        while (notListEnd()) {
            // go read the next address.  If we had an address, add to the list.
            // an address ITEM cannot be NIL inside the parens.
            InternetAddress address = readAddress();
            addresses.add(address);
        }
        // we need to skip over the peeked token.
        checkRightParen();
        return (InternetAddress[])addresses.toArray(new InternetAddress[addresses.size()]);
    }


    /**
     * Check to see if we're at the end of a parenthized list
     * without advancing the parsing pointer.  If we are at the
     * end, then this will step over the closing paren.
     *
     * @return True if the next token is a closing list paren, false otherwise.
     * @exception ResponseFormatException
     */
    public boolean checkListEnd() throws MessagingException {
        Token token = peek(true);
        if (token.getType() == ')') {
            // step over this token.
            next();
            return true;
        }
        return false;
    }


    /**
     * Reads a string item which can be encoded either as a single
     * string-valued token or a parenthized list of string tokens.
     *
     * @return A List containing all of the strings.
     * @exception ResponseFormatException
     */
    public List readStringList() throws MessagingException {
        Token token = peek(true);

        if (token.getType() == '(') {
            List list = new ArrayList();

            next();

            while (notListEnd()) {
                String value = readString();
                // this can be NIL, technically
                if (value != null) {
                    list.add(value);
                }
            }
            // step over the closing paren
            next();

            return list;
        }
        else if (token != NIL) {
            List list = new ArrayList();

            // just a single string value.
            String value = readString();
            // this can be NIL, technically
            if (value != null) {
                list.add(value);
            }

            return list;
        } else {
            next();
        }
        return null;
    }


    /**
     * Reads all remaining tokens and returns them as a list of strings.
     * NIL values are not supported.
     *
     * @return A List containing all of the strings.
     * @exception ResponseFormatException
     */
    public List readStrings() throws MessagingException {
        List list = new ArrayList();

        while (hasMore()) {
            String value = readString();
            list.add(value);
        }
        return list;
    }


    /**
     * Skip over an extension item.  This may be either a string
     * token or a parenthized item (with potential nesting).
     *
     * At the point where this is called, we're looking for a closing
     * ')', but we know it is not that.  An EOF is an error, however,
     */
    public void skipExtensionItem() throws MessagingException {
        Token token = next();
        int type = token.getType();

        // list form?  Scan to find the correct list closure.
        if (type == '(') {
            skipNestedValue();
        }
        // found an EOF?  Big problem
        else if (type == Token.EOF) {
            throw new ResponseFormatException("Missing ')'");
        }
    }

    /**
     * Skip over a parenthized value that we're not interested in.
     * These lists may contain nested sublists, so we need to
     * handle the nesting properly.
     */
    public void skipNestedValue() throws MessagingException {
        Token token = next();

        while (true) {
            int type = token.getType();
            // list terminator?
            if (type == ')') {
                return;
            }
            // unexpected end of the tokens.
            else if (type == Token.EOF) {
                throw new ResponseFormatException("Missing ')'");
            }
            // encountered a nested list?
            else if (type == '(') {
                // recurse and finish this list.
                skipNestedValue();
            }
            // we're just skipping the token.
            token = next();
        }
    }

    /**
     * Get the next token and verify that it's of the expected type
     * for the context.
     *
     * @param type   The type of token we're expecting.
     */
    public void checkToken(int type) throws MessagingException {
        Token token = next();
        if (token.getType() != type) {
            throw new ResponseFormatException("Unexpected token: " + token.getValue());
        }
    }


    /**
     * Read the next token as binary data.  The next token can be a literal, a quoted string, or
     * the token NIL (which returns a null result).  Any other token throws a ResponseFormatException.
     *
     * @return A byte array representing the rest of the response data.
     */
    public byte[] readByteArray() throws MessagingException {
        return readData(true);
    }


    /**
     * Determine what type of token encoding needs to be
     * used for a string value.
     *
     * @param value  The string to test.
     *
     * @return Either Token.ATOM, Token.QUOTEDSTRING, or
     *         Token.LITERAL, depending on the characters contained
     *         in the value.
     */
    static public int getEncoding(byte[] value) {

        // a null string always needs to be represented as a quoted literal.
        if (value.length == 0) {
            return Token.QUOTEDSTRING;
        }

        for (int i = 0; i < value.length; i++) {
            int ch = value[i];
            // make sure the sign extension is eliminated
            ch = ch & 0xff;
            // check first for any characters that would
            // disqualify a quoted string
            // NULL
            if (ch == 0x00) {
                return Token.LITERAL;
            }
            // non-7bit ASCII
            if (ch > 0x7F) {
                return Token.LITERAL;
            }
            // carriage return
            if (ch == '\r') {
                return Token.LITERAL;
            }
            // linefeed
            if (ch == '\n') {
                return Token.LITERAL;
            }
            // now check for ATOM disqualifiers
            if (atomDelimiters.indexOf(ch) != -1) {
                return Token.QUOTEDSTRING;
            }
            // CTL character.  We've already eliminated the high characters
            if (ch < 0x20) {
                return Token.QUOTEDSTRING;
            }
        }
        // this can be an ATOM token
        return Token.ATOM;
    }


    /**
     * Read a ContentType or ContentDisposition parameter
     * list from an IMAP command response.
     *
     * @return A ParameterList instance containing the parameters.
     * @exception MessagingException
     */
    public ParameterList readParameterList() throws MessagingException {
        ParameterList params = new ParameterList();

        // read the tokens, taking NIL into account.
        Token token = next(true, false);

        // just return an empty list if this is NIL
        if (token.isType(token.NIL)) {
            return params;
        }

        // these are pairs of strings for each parameter value
        while (notListEnd()) {
            String name = readString();
            String value = readString();
            params.set(name, value);
        }
        // we need to consume the list terminator
        checkRightParen();
        return params;
    }


    /**
     * Test if we have more data in the response buffer.
     *
     * @return true if there are more tokens to process.  false if
     *         we've reached the end of the stream.
     */
    public boolean hasMore() throws MessagingException {
        // we need to eat any white space that might be in the stream.
        eatWhiteSpace();
        return pos < response.length;
    }


    /**
     * Tests if we've reached the end of a parenthetical
     * list in our parsing stream.
     *
     * @return true if the next token will be a ')'.  false if the
     *         next token is anything else.
     * @exception MessagingException
     */
    public boolean notListEnd() throws MessagingException {
        return peek().getType() != ')';
    }

    /**
     * Read a list of Flag values from an IMAP response,
     * returning a Flags instance containing the appropriate
     * pieces.
     *
     * @return A Flags instance with the flag values.
     * @exception MessagingException
     */
    public Flags readFlagList() throws MessagingException {
        Flags flags = new Flags();

        // this should be a list here
        checkLeftParen();

        // run through the flag list
        while (notListEnd()) {
            // the flags are a bit of a pain.  The flag names include "\" in the name, which
            // is not a character allowed in an atom.  This requires a bit of customized parsing
            // to handle this.
            Token token = next();
            // flags can be specified as just atom tokens, so allow this as a user flag.
            if (token.isType(token.ATOM)) {
                // append the atom as a raw name
                flags.add(token.getValue());
            }
            // all of the system flags start with a '\' followed by
            // an atom.  They also can be extension flags.  IMAP has a special
            // case of "\*" that we need to check for.
            else if (token.isType('\\')) {
                token = next();
                // the next token is the real bit we need to process.
                if (token.isType('*')) {
                    // this indicates USER flags are allowed.
                    flags.add(Flags.Flag.USER);
                }
                // if this is an atom name, handle as a system flag
                else if (token.isType(Token.ATOM)) {
                    String name = token.getValue();
                    if (name.equalsIgnoreCase("Seen")) {
                        flags.add(Flags.Flag.SEEN);
                    }
                    else if (name.equalsIgnoreCase("RECENT")) {
                        flags.add(Flags.Flag.RECENT);
                    }
                    else if (name.equalsIgnoreCase("DELETED")) {
                        flags.add(Flags.Flag.DELETED);
                    }
                    else if (name.equalsIgnoreCase("ANSWERED")) {
                        flags.add(Flags.Flag.ANSWERED);
                    }
                    else if (name.equalsIgnoreCase("DRAFT")) {
                        flags.add(Flags.Flag.DRAFT);
                    }
                    else if (name.equalsIgnoreCase("FLAGGED")) {
                        flags.add(Flags.Flag.FLAGGED);
                    }
                    else {
                        // this is a server defined flag....just add the name with the
                        // flag thingy prepended.
                        flags.add("\\" + name);
                    }
                }
                else {
                    throw new MessagingException("Invalid Flag: " + token.getValue());
                }
            }
            else {
                throw new MessagingException("Invalid Flag: " + token.getValue());
            }
        }

        // step over this for good practice.
        checkRightParen();

        return flags;
    }


    /**
     * Read a list of Flag values from an IMAP response,
     * returning a Flags instance containing the appropriate
     * pieces.
     *
     * @return A Flags instance with the flag values.
     * @exception MessagingException
     */
    public List readSystemNameList() throws MessagingException {
        List flags = new ArrayList();

        // this should be a list here
        checkLeftParen();

        // run through the flag list
        while (notListEnd()) {
            // the flags are a bit of a pain.  The flag names include "\" in the name, which
            // is not a character allowed in an atom.  This requires a bit of customized parsing
            // to handle this.
            Token token = next();
            // all of the system flags start with a '\' followed by
            // an atom.  They also can be extension flags.  IMAP has a special
            // case of "\*" that we need to check for.
            if (token.isType('\\')) {
                token = next();
                // if this is an atom name, handle as a system flag
                if (token.isType(Token.ATOM)) {
                    // add the token value to the list WITH the
                    // flag indicator included.  The attributes method returns
                    // these flag indicators, so we need to include it.
                    flags.add("\\" + token.getValue());
                }
                else {
                    throw new MessagingException("Invalid Flag: " + token.getValue());
                }
            }
            else {
                throw new MessagingException("Invalid Flag: " + token.getValue());
            }
        }

        // step over this for good practice.
        checkRightParen();

        return flags;
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



geronimo-javamail_1.6/geronimo-javamail_1.6_provider/src/main/java/org/apache/geronimo/javamail/store/imap/connection/IMAPResponseTokenizer.java [37:1466]:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
public class IMAPResponseTokenizer {
    /*
     * set up the decoding table.
     */
    protected static final byte[] decodingTable = new byte[256];

    protected static void initializeDecodingTable()
    {
        for (int i = 0; i < IMAPCommand.encodingTable.length; i++)
        {
            decodingTable[IMAPCommand.encodingTable[i]] = (byte)i;
        }
    }


    static {
        initializeDecodingTable();
    }

    // a singleton formatter for header dates.
    protected static MailDateFormat dateParser = new MailDateFormat();


    public static class Token {
        // Constant values from J2SE 1.4 API Docs (Constant values)
        public static final int ATOM = -1;
        public static final int QUOTEDSTRING = -2;
        public static final int LITERAL = -3;
        public static final int NUMERIC = -4;
        public static final int EOF = -5;
        public static final int NIL = -6;
        // special single character markers
        public static final int CONTINUATION = '+';
        public static final int UNTAGGED = '*';

        /**
         * The type indicator.  This will be either a specific type, represented by
         * a negative number, or the actual character value.
         */
        private int type;
        /**
         * The String value associated with this token.  All tokens have a String value,
         * except for the EOF and NIL tokens.
         */
        private String value;

        public Token(int type, String value) {
            this.type = type;
            this.value = value;
        }

        public int getType() {
            return type;
        }

        public String getValue() {
            return value;
        }

        public boolean isType(int type) {
            return this.type == type;
        }

        /**
         * Return the token as an integer value.  If this can't convert, an exception is
         * thrown.
         *
         * @return The integer value of the token.
         * @exception ResponseFormatException
         */
        public int getInteger() throws MessagingException {
            if (value != null) {
                try {
                    return Integer.parseInt(value);
                } catch (NumberFormatException e) {
                }
            }

            throw new ResponseFormatException("Number value expected in response; fount: " + value);
        }

        /**
         * Return the token as a long value.  If it can't convert, an exception is
         * thrown.
         *
         * @return The token as a long value.
         * @exception ResponseFormatException
         */
        public long getLong() throws MessagingException {
            if (value != null) {
                try {
                    return Long.parseLong(value);
                } catch (NumberFormatException e) {
                }
            }
            throw new ResponseFormatException("Number value expected in response; fount: " + value);
        }

        /**
         * Handy debugging toString() method for token.
         *
         * @return The string value of the token.
         */
        public String toString() {
            if (type == NIL) {
                return "NIL";
            }
            else if (type == EOF) {
                return "EOF";
            }

            if (value == null) {
                return "";
            }
            return value;
        }
    }

    public static final Token EOF = new Token(Token.EOF, null);
    public static final Token NIL = new Token(Token.NIL, null);

    private static final String WHITE = " \t\n\r";
    // The list of delimiter characters we process when
    // handling parsing of ATOMs.
    private static final String atomDelimiters = "(){}%*\"\\" + WHITE;
    // this set of tokens is a slighly expanded set used for
    // specific response parsing.  When dealing with Body
    // section names, there are sub pieces to the name delimited
    // by "[", "]", ".", "<", ">" and SPACE, so reading these using
    // a superset of the ATOM processing makes for easier parsing.
    private static final String tokenDelimiters = "<>[].(){}%*\"\\" + WHITE;

    // the response data read from the connection
    private byte[] response;
    // current parsing position
    private int pos;

    public IMAPResponseTokenizer(byte [] response) {
        this.response = response;
    }

    /**
     * Get the remainder of the response as a string.
     *
     * @return A string representing the remainder of the response.
     */
    public String getRemainder() {
        // make sure we're still in range
        if (pos >= response.length) {
            return "";
        }

        try {
            return new String(response, pos, response.length - pos, "ISO8859-1");
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }


    public Token next() throws MessagingException {
        return next(false);
    }

    public Token next(boolean nilAllowed) throws MessagingException {
        return readToken(nilAllowed, false);
    }

    public Token next(boolean nilAllowed, boolean expandedDelimiters) throws MessagingException {
        return readToken(nilAllowed, expandedDelimiters);
    }

    public Token peek() throws MessagingException {
        return peek(false, false);
    }

    public Token peek(boolean nilAllowed) throws MessagingException {
        return peek(nilAllowed, false);
    }

    public Token peek(boolean nilAllowed, boolean expandedDelimiters) throws MessagingException {
        int start = pos;
        try {
            return readToken(nilAllowed, expandedDelimiters);
        } finally {
            pos = start;
        }
    }

    /**
     * Read an ATOM token from the parsed response.
     *
     * @return A token containing the value of the atom token.
     */
    private Token readAtomicToken(String delimiters) {
        // skip to next delimiter
        int start = pos;
        while (++pos < response.length) {
            // break on the first non-atom character.
            byte ch = response[pos];
            if (delimiters.indexOf(response[pos]) != -1 || ch < 32 || ch >= 127) {
                break;
            }
        }

        try {
            // Numeric tokens we store as a different type.
            String value = new String(response, start, pos - start, "ISO8859-1");
            try {
                int intValue = Integer.parseInt(value);
                return new Token(Token.NUMERIC, value);
            } catch (NumberFormatException e) {
            }
            return new Token(Token.ATOM, value);
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }

    /**
     * Read the next token from the response.
     *
     * @return The next token from the response.  White space is skipped, and comment
     *         tokens are also skipped if indicated.
     * @exception ResponseFormatException
     */
    private Token readToken(boolean nilAllowed, boolean expandedDelimiters) throws MessagingException {
        String delimiters = expandedDelimiters ? tokenDelimiters : atomDelimiters;

        if (pos >= response.length) {
            return EOF;
        } else {
            byte ch = response[pos];
            if (ch == '\"') {
                return readQuotedString();
            // beginning of a length-specified literal?
            } else if (ch == '{') {
                return readLiteral();
            // white space, eat this and find a real token.
            } else if (WHITE.indexOf(ch) != -1) {
                eatWhiteSpace();
                return readToken(nilAllowed, expandedDelimiters);
            // either a CTL or special.  These characters have a self-defining token type.
            } else if (ch < 32 || ch >= 127 || delimiters.indexOf(ch) != -1) {
                pos++;
                return new Token((int)ch, String.valueOf((char)ch));
            } else {
                // start of an atom, parse it off.
                Token token = readAtomicToken(delimiters);
                // now, if we've been asked to look at NIL tokens, check to see if it is one,
                // and return that instead of the ATOM.
                if (nilAllowed) {
                    if (token.getValue().equalsIgnoreCase("NIL")) {
                        return NIL;
                    }
                }
                return token;
            }
        }
    }

    /**
     * Read the next token from the response, returning it as a byte array value.
     *
     * @return The next token from the response.  White space is skipped, and comment
     *         tokens are also skipped if indicated.
     * @exception ResponseFormatException
     */
    private byte[] readData(boolean nilAllowed) throws MessagingException {
        if (pos >= response.length) {
            return null;
        } else {
            byte ch = response[pos];
            if (ch == '\"') {
                return readQuotedStringData();
            // beginning of a length-specified literal?
            } else if (ch == '{') {
                return readLiteralData();
            // white space, eat this and find a real token.
            } else if (WHITE.indexOf(ch) != -1) {
                eatWhiteSpace();
                return readData(nilAllowed);
            // either a CTL or special.  These characters have a self-defining token type.
            } else if (ch < 32 || ch >= 127 || atomDelimiters.indexOf(ch) != -1) {
                throw new ResponseFormatException("Invalid string value: " + ch);
            } else {
                // only process this if we're allowing NIL as an option.
                if (nilAllowed) {
                    // start of an atom, parse it off.
                    Token token = next(true);
                    if (token.isType(Token.NIL)) {
                        return null;
                    }
                    // invalid token type.
                    throw new ResponseFormatException("Invalid string value: " + token.getValue());
                }
                // invalid token type.
                throw new ResponseFormatException("Invalid string value: " + ch);
            }
        }
    }

    /**
     * Extract a substring from the response string and apply any
     * escaping/folding rules to the string.
     *
     * @param start  The starting offset in the response.
     * @param end    The response end offset + 1.
     *
     * @return The processed string value.
     * @exception ResponseFormatException
     */
    private byte[] getEscapedValue(int start, int end) throws MessagingException {
        ByteArrayOutputStream value = new ByteArrayOutputStream();

        for (int i = start; i < end; i++) {
            byte ch = response[i];
            // is this an escape character?
            if (ch == '\\') {
                i++;
                if (i == end) {
                    throw new ResponseFormatException("Invalid escape character");
                }
                value.write(response[i]);
            }
            // line breaks are ignored, except for naked '\n' characters, which are consider
            // parts of linear whitespace.
            else if (ch == '\r') {
                // see if this is a CRLF sequence, and skip the second if it is.
                if (i < end - 1 && response[i + 1] == '\n') {
                    i++;
                }
            }
            else {
                // just append the ch value.
                value.write(ch);
            }
        }
        return value.toByteArray();
    }

    /**
     * Parse out a quoted string from the response, applying escaping
     * rules to the value.
     *
     * @return The QUOTEDSTRING token with the value.
     * @exception ResponseFormatException
     */
    private Token readQuotedString() throws MessagingException {
        try {
            String value = new String(readQuotedStringData(), "ISO8859-1");
            return new Token(Token.QUOTEDSTRING, value);
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }

    /**
     * Parse out a quoted string from the response, applying escaping
     * rules to the value.
     *
     * @return The byte array with the resulting string bytes.
     * @exception ResponseFormatException
     */
    private byte[] readQuotedStringData() throws MessagingException {
        int start = pos + 1;
        boolean requiresEscaping = false;

        // skip to end of comment/string
        while (++pos < response.length) {
            byte ch = response[pos];
            if (ch == '"') {
                byte[] value;
                if (requiresEscaping) {
                    value = getEscapedValue(start, pos);
                }
                else {
                    value = subarray(start, pos);
                }
                // step over the delimiter for all cases.
                pos++;
                return value;
            }
            else if (ch == '\\') {
                pos++;
                requiresEscaping = true;
            }
            // we need to process line breaks also
            else if (ch == '\r') {
                requiresEscaping = true;
            }
        }

        throw new ResponseFormatException("Missing '\"'");
    }


    /**
     * Parse out a literal string from the response, using the length
     * encoded before the listeral.
     *
     * @return The LITERAL token with the value.
     * @exception ResponseFormatException
     */
    protected Token readLiteral() throws MessagingException {
        try {
            String value = new String(readLiteralData(), "ISO8859-1");
            return new Token(Token.LITERAL, value);
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }


    /**
     * Parse out a literal string from the response, using the length
     * encoded before the listeral.
     *
     * @return The byte[] array with the value.
     * @exception ResponseFormatException
     */
    protected byte[] readLiteralData() throws MessagingException {
        int lengthStart = pos + 1;

        // see if we have a close marker.
        int lengthEnd = indexOf("}\r\n", lengthStart);
        if (lengthEnd == -1) {
            throw new ResponseFormatException("Missing terminator on literal length");
        }

        int count = 0;
        try {
            count = Integer.parseInt(substring(lengthStart, lengthEnd));
        } catch (NumberFormatException e) {
            throw new ResponseFormatException("Invalid literal length " + substring(lengthStart, lengthEnd));
        }

        // step over the length
        pos = lengthEnd + 3;

        // too long?
        if (pos + count > response.length) {
            throw new ResponseFormatException("Invalid literal length: " + count);
        }

        byte[] value = subarray(pos, pos + count);
        pos += count;

        return value;
    }


    /**
     * Extract a substring from the response buffer.
     *
     * @param start  The starting offset.
     * @param end    The end offset (+ 1).
     *
     * @return A String extracted from the buffer.
     */
    protected String substring(int start, int end ) {
        try {
            return new String(response, start, end - start, "ISO8859-1");
        } catch (UnsupportedEncodingException e) {
            return null; 
        }
    }


    /**
     * Extract a subarray from the response buffer.
     *
     * @param start  The starting offset.
     * @param end    The end offset (+ 1).
     *
     * @return A byte array string extracted rom the buffer.
     */
    protected byte[] subarray(int start, int end ) {
        byte[] result = new byte[end - start];
        System.arraycopy(response, start, result, 0, end - start);
        return result;
    }


    /**
     * Test if the bytes in the response buffer match a given
     * string value.
     *
     * @param position The compare position.
     * @param needle   The needle string we're testing for.
     *
     * @return True if the bytes match the needle value, false for any
     *         mismatch.
     */
    public boolean match(int position, String needle) {
        int length = needle.length();

        if (response.length - position < length) {
            return false;
        }

        for (int i = 0; i < length; i++) {
            if (response[position + i ] != needle.charAt(i)) {
                return false;
            }
        }
        return true;
    }


    /**
     * Search for a given string starting from the current position
     * cursor.
     *
     * @param needle The search string.
     *
     * @return The index of a match (in absolute byte position in the
     *         response buffer).
     */
    public int indexOf(String needle) {
        return indexOf(needle, pos);
    }

    /**
     * Search for a string in the response buffer starting from the
     * indicated position.
     *
     * @param needle   The search string.
     * @param position The starting buffer position.
     *
     * @return The index of the match position.  Returns -1 for no match.
     */
    public int indexOf(String needle, int position) {
        // get the last possible match position
        int last = response.length - needle.length();
        // no match possible
        if (last < position) {
            return -1;
        }

        for (int i = position; i <= last; i++) {
            if (match(i, needle)) {
                return i;
            }
        }
        return -1;
    }



    /**
     * Skip white space in the token string.
     */
    private void eatWhiteSpace() {
        // skip to end of whitespace
        while (++pos < response.length
                && WHITE.indexOf(response[pos]) != -1)
            ;
    }


    /**
     * Ensure that the next token in the parsed response is a
     * '(' character.
     *
     * @exception ResponseFormatException
     */
    public void checkLeftParen() throws MessagingException {
        Token token = next();
        if (token.getType() != '(') {
            throw new ResponseFormatException("Missing '(' in response");
        }
    }


    /**
     * Ensure that the next token in the parsed response is a
     * ')' character.
     *
     * @exception ResponseFormatException
     */
    public void checkRightParen() throws MessagingException {
        Token token = next();
        if (token.getType() != ')') {
            throw new ResponseFormatException("Missing ')' in response");
        }
    }


    /**
     * Read a string-valued token from the response.  A string
     * valued token can be either a quoted string, a literal value,
     * or an atom.  Any other token type is an error.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readString() throws MessagingException {
        Token token = next(true);
        int type = token.getType();
        if (type == Token.NIL) {
            return null;
        }
        if (type != Token.ATOM && type != Token.QUOTEDSTRING && type != Token.LITERAL && type != Token.NUMERIC) {
            throw new ResponseFormatException("String token expected in response: " + token.getValue());
        }
        return token.getValue();
    }


    /**
     * Read an encoded string-valued token from the response.  A string
     * valued token can be either a quoted string, a literal value,
     * or an atom.  Any other token type is an error.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readEncodedString() throws MessagingException {
        String value = readString();
        return decode(value);
    }


    /**
     * Decode a Base 64 encoded string value.
     *
     * @param original The original encoded string.
     *
     * @return The decoded string.
     * @exception MessagingException
     */
    public String decode(String original) throws MessagingException {
        StringBuffer result = new StringBuffer();

        for (int i = 0; i < original.length(); i++) {
            char ch = original.charAt(i);

            if (ch == '&') {
                i = decode(original, i, result);
            }
            else {
                result.append(ch);
            }
        }

        return result.toString();
    }


    /**
     * Decode a section of an encoded string value.
     *
     * @param original The original source string.
     * @param index    The current working index.
     * @param result   The StringBuffer used for the decoded result.
     *
     * @return The new index for the decoding operation.
     * @exception MessagingException
     */
    public static int decode(String original, int index, StringBuffer result) throws MessagingException {
        // look for the section terminator
        int terminator = original.indexOf('-', index);

        // unmatched?
        if (terminator == -1) {
            throw new MessagingException("Invalid UTF-7 encoded string");
        }

        // is this just an escaped "&"?
        if (terminator == index + 1) {
            // append and skip over this.
            result.append('&');
            return index + 2;
        }

        // step over the starting char
        index++;

        int chars = terminator - index;
        int quads = chars / 4;
        int residual = chars % 4;

        // buffer for decoded characters
        byte[] buffer = new byte[4];
        int bufferCount = 0;

        // process each of the full triplet pairs
        for (int i = 0; i < quads; i++) {
            byte b1 = decodingTable[original.charAt(index++) & 0xff];
            byte b2 = decodingTable[original.charAt(index++) & 0xff];
            byte b3 = decodingTable[original.charAt(index++) & 0xff];
            byte b4 = decodingTable[original.charAt(index++) & 0xff];

            buffer[bufferCount++] = (byte)((b1 << 2) | (b2 >> 4));
            buffer[bufferCount++] = (byte)((b2 << 4) | (b3 >> 2));
            buffer[bufferCount++] = (byte)((b3 << 6) | b4);

            // we've written 3 bytes to the buffer, but we might have a residual from a previous
            // iteration to deal with.
            if (bufferCount == 4) {
                // two complete chars here
                b1 = buffer[0];
                b2 = buffer[1];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                b1 = buffer[2];
                b2 = buffer[3];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                bufferCount = 0;
            }
            else {
                // we need to save the 3rd byte for the next go around
                b1 = buffer[0];
                b2 = buffer[1];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                buffer[0] = buffer[2];
                bufferCount = 1;
            }
        }

        // properly encoded, we should have an even number of bytes left.

        switch (residual) {
            // no residual...so we better not have an extra in the buffer
            case 0:
                // this is invalid...we have an odd number of bytes so far,
                if (bufferCount == 1) {
                    throw new MessagingException("Invalid UTF-7 encoded string");
                }
            // one byte left.  This shouldn't be valid.  We need at least 2 bytes to
            // encode one unprintable char.
            case 1:
                throw new MessagingException("Invalid UTF-7 encoded string");

            // ok, we have two bytes left, which can only encode a single byte.  We must have
            // a dangling unhandled char.
            case 2:
            {
                if (bufferCount != 1) {
                    throw new MessagingException("Invalid UTF-7 encoded string");
                }
                byte b1 = decodingTable[original.charAt(index++) & 0xff];
                byte b2 = decodingTable[original.charAt(index++) & 0xff];
                buffer[bufferCount++] = (byte)((b1 << 2) | (b2 >> 4));

                b1 = buffer[0];
                b2 = buffer[1];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                break;
            }

            // we have 2 encoded chars.  In this situation, we can't have a leftover.
            case 3:
            {
                // this is invalid...we have an odd number of bytes so far,
                if (bufferCount == 1) {
                    throw new MessagingException("Invalid UTF-7 encoded string");
                }
                byte b1 = decodingTable[original.charAt(index++) & 0xff];
                byte b2 = decodingTable[original.charAt(index++) & 0xff];
                byte b3 = decodingTable[original.charAt(index++) & 0xff];

                buffer[bufferCount++] = (byte)((b1 << 2) | (b2 >> 4));
                buffer[bufferCount++] = (byte)((b2 << 4) | (b3 >> 2));

                b1 = buffer[0];
                b2 = buffer[1];
                result.append((char)((b1 << 8) + (b2 & 0xff)));
                break;
            }
        }

        // return the new scan location
        return terminator + 1;
    }

    /**
     * Read a string-valued token from the response, verifying this is an ATOM token.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readAtom() throws MessagingException {
        return readAtom(false);
    }


    /**
     * Read a string-valued token from the response, verifying this is an ATOM token.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readAtom(boolean expandedDelimiters) throws MessagingException {
        Token token = next(false, expandedDelimiters);
        int type = token.getType();

        if (type != Token.ATOM) {
            throw new ResponseFormatException("ATOM token expected in response: " + token.getValue());
        }
        return token.getValue();
    }


    /**
     * Read a number-valued token from the response.  This must be an ATOM
     * token.
     *
     * @return The integer value of the source token.
     * @exception ResponseFormatException
     */
    public int readInteger() throws MessagingException {
        Token token = next();
        return token.getInteger();
    }


    /**
     * Read a number-valued token from the response.  This must be an ATOM
     * token.
     *
     * @return The long value of the source token.
     * @exception ResponseFormatException
     */
    public int readLong() throws MessagingException {
        Token token = next();
        return token.getInteger();
    }


    /**
     * Read a string-valued token from the response.  A string
     * valued token can be either a quoted string, a literal value,
     * or an atom.  Any other token type is an error.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    public String readStringOrNil() throws MessagingException {
        // we need to recognize the NIL token.
        Token token = next(true);
        int type = token.getType();

        if (type != Token.ATOM && type != Token.QUOTEDSTRING && type != Token.LITERAL && type != Token.NIL) {
            throw new ResponseFormatException("String token or NIL expected in response: " + token.getValue());
        }
        // this returns null if the token is the NIL token.
        return token.getValue();
    }


    /**
     * Read a quoted string-valued token from the response.
     * Any other token type other than NIL is an error.
     *
     * @return The string value of the source token.
     * @exception ResponseFormatException
     */
    protected String readQuotedStringOrNil() throws MessagingException {
        // we need to recognize the NIL token.
        Token token = next(true);
        int type = token.getType();

        if (type != Token.QUOTEDSTRING && type != Token.NIL) {
            throw new ResponseFormatException("String token or NIL expected in response");
        }
        // this returns null if the token is the NIL token.
        return token.getValue();
    }


    /**
     * Read a date from a response string.  This is expected to be in
     * Internet Date format, but there's a lot of variation implemented
     * out there.  If we're unable to format this correctly, we'll
     * just return null.
     *
     * @return A Date object created from the source date.
     */
    public Date readDate() throws MessagingException {
        String value = readString();

        try {
            return dateParser.parse(value);
        } catch (Exception e) {
            // we're just skipping over this, so return null
            return null;
        }
    }


    /**
     * Read a date from a response string.  This is expected to be in
     * Internet Date format, but there's a lot of variation implemented
     * out there.  If we're unable to format this correctly, we'll
     * just return null.
     *
     * @return A Date object created from the source date.
     */
    public Date readDateOrNil() throws MessagingException {
        String value = readStringOrNil();
        // this might be optional
        if (value == null) {
            return null;
        }

        try {
            return dateParser.parse(value);
        } catch (Exception e) {
            // we're just skipping over this, so return null
            return null;
        }
    }

    /**
     * Read an internet address from a Fetch response.  The
     * addresses are returned as a set of string tokens in the
     * order "personal list mailbox host".  Any of these tokens
     * can be NIL.
     *
     * The address may also be the start of a group list, which
     * is indicated by the host being NIL.  If we have found the
     * start of a group, then we need to parse multiple elements
     * until we find the group end marker (indicated by both the
     * mailbox and the host being NIL), and create a group
     * InternetAddress instance from this.
     *
     * @return An InternetAddress instance parsed from the
     *         element.
     * @exception ResponseFormatException
     */
    public InternetAddress readAddress() throws MessagingException {
        // we recurse, expecting a null response back for sublists.
        if (peek().getType() != '(') {
            return null;
        }

        // must start with a paren
        checkLeftParen();

        // personal information
        String personal = readStringOrNil();
        // the domain routine information.
        String routing = readStringOrNil();
        // the target mailbox
        String mailbox = readStringOrNil();
        // and finally the host
        String host = readStringOrNil();
        // and validate the closing paren
        checkRightParen();

        // if this is a real address, we need to compose
        if (host != null) {
            StringBuffer address = new StringBuffer();
            if (routing != null) {
                address.append(routing);
                address.append(':');
            }
            address.append(mailbox);
            address.append('@');
            address.append(host);

            try {
                return new InternetAddress(address.toString(), personal);
            } catch (UnsupportedEncodingException e) {
                throw new ResponseFormatException("Invalid Internet address format");
            }
        }
        else {
            // we're going to recurse on this.  If the mailbox is null (the group name), this is the group item
            // terminator.
            if (mailbox == null) {
                return null;
            }

            StringBuffer groupAddress = new StringBuffer();

            groupAddress.append(mailbox);
            groupAddress.append(':');
            int count = 0;

            while (true) {
                // now recurse until we hit the end of the list
                InternetAddress member = readAddress();
                if (member == null) {
                    groupAddress.append(';');

                    try {
                        return new InternetAddress(groupAddress.toString(), personal);
                    } catch (UnsupportedEncodingException e) {
                        throw new ResponseFormatException("Invalid Internet address format");
                    }
                }
                else {
                    if (count != 0) {
                        groupAddress.append(',');
                    }
                    groupAddress.append(member.toString());
                    count++;
                }
            }
        }
    }


    /**
     * Parse out a list of addresses.  This list of addresses is
     * surrounded by parentheses, and each address is also
     * parenthized (SP?).
     *
     * @return An array of the parsed addresses.
     * @exception ResponseFormatException
     */
    public InternetAddress[] readAddressList() throws MessagingException {
        // must start with a paren, but can be NIL also.
        Token token = next(true);
        int type = token.getType();

        // either of these results in a null address.  The caller determines based on
        // context whether this was optional or not.
        if (type == Token.NIL) {
            return null;
        }
        // non-nil address and no paren.  This is a syntax error.
        else if (type != '(') {
            throw new ResponseFormatException("Missing '(' in response");
        }

        List addresses = new ArrayList();

        // we have a list, now parse it.
        while (notListEnd()) {
            // go read the next address.  If we had an address, add to the list.
            // an address ITEM cannot be NIL inside the parens.
            InternetAddress address = readAddress();
            addresses.add(address);
        }
        // we need to skip over the peeked token.
        checkRightParen();
        return (InternetAddress[])addresses.toArray(new InternetAddress[addresses.size()]);
    }


    /**
     * Check to see if we're at the end of a parenthized list
     * without advancing the parsing pointer.  If we are at the
     * end, then this will step over the closing paren.
     *
     * @return True if the next token is a closing list paren, false otherwise.
     * @exception ResponseFormatException
     */
    public boolean checkListEnd() throws MessagingException {
        Token token = peek(true);
        if (token.getType() == ')') {
            // step over this token.
            next();
            return true;
        }
        return false;
    }


    /**
     * Reads a string item which can be encoded either as a single
     * string-valued token or a parenthized list of string tokens.
     *
     * @return A List containing all of the strings.
     * @exception ResponseFormatException
     */
    public List readStringList() throws MessagingException {
        Token token = peek(true);

        if (token.getType() == '(') {
            List list = new ArrayList();

            next();

            while (notListEnd()) {
                String value = readString();
                // this can be NIL, technically
                if (value != null) {
                    list.add(value);
                }
            }
            // step over the closing paren
            next();

            return list;
        }
        else if (token != NIL) {
            List list = new ArrayList();

            // just a single string value.
            String value = readString();
            // this can be NIL, technically
            if (value != null) {
                list.add(value);
            }

            return list;
        } else {
            next();
        }
        return null;
    }


    /**
     * Reads all remaining tokens and returns them as a list of strings.
     * NIL values are not supported.
     *
     * @return A List containing all of the strings.
     * @exception ResponseFormatException
     */
    public List readStrings() throws MessagingException {
        List list = new ArrayList();

        while (hasMore()) {
            String value = readString();
            list.add(value);
        }
        return list;
    }


    /**
     * Skip over an extension item.  This may be either a string
     * token or a parenthized item (with potential nesting).
     *
     * At the point where this is called, we're looking for a closing
     * ')', but we know it is not that.  An EOF is an error, however,
     */
    public void skipExtensionItem() throws MessagingException {
        Token token = next();
        int type = token.getType();

        // list form?  Scan to find the correct list closure.
        if (type == '(') {
            skipNestedValue();
        }
        // found an EOF?  Big problem
        else if (type == Token.EOF) {
            throw new ResponseFormatException("Missing ')'");
        }
    }

    /**
     * Skip over a parenthized value that we're not interested in.
     * These lists may contain nested sublists, so we need to
     * handle the nesting properly.
     */
    public void skipNestedValue() throws MessagingException {
        Token token = next();

        while (true) {
            int type = token.getType();
            // list terminator?
            if (type == ')') {
                return;
            }
            // unexpected end of the tokens.
            else if (type == Token.EOF) {
                throw new ResponseFormatException("Missing ')'");
            }
            // encountered a nested list?
            else if (type == '(') {
                // recurse and finish this list.
                skipNestedValue();
            }
            // we're just skipping the token.
            token = next();
        }
    }

    /**
     * Get the next token and verify that it's of the expected type
     * for the context.
     *
     * @param type   The type of token we're expecting.
     */
    public void checkToken(int type) throws MessagingException {
        Token token = next();
        if (token.getType() != type) {
            throw new ResponseFormatException("Unexpected token: " + token.getValue());
        }
    }


    /**
     * Read the next token as binary data.  The next token can be a literal, a quoted string, or
     * the token NIL (which returns a null result).  Any other token throws a ResponseFormatException.
     *
     * @return A byte array representing the rest of the response data.
     */
    public byte[] readByteArray() throws MessagingException {
        return readData(true);
    }


    /**
     * Determine what type of token encoding needs to be
     * used for a string value.
     *
     * @param value  The string to test.
     *
     * @return Either Token.ATOM, Token.QUOTEDSTRING, or
     *         Token.LITERAL, depending on the characters contained
     *         in the value.
     */
    static public int getEncoding(byte[] value) {

        // a null string always needs to be represented as a quoted literal.
        if (value.length == 0) {
            return Token.QUOTEDSTRING;
        }

        for (int i = 0; i < value.length; i++) {
            int ch = value[i];
            // make sure the sign extension is eliminated
            ch = ch & 0xff;
            // check first for any characters that would
            // disqualify a quoted string
            // NULL
            if (ch == 0x00) {
                return Token.LITERAL;
            }
            // non-7bit ASCII
            if (ch > 0x7F) {
                return Token.LITERAL;
            }
            // carriage return
            if (ch == '\r') {
                return Token.LITERAL;
            }
            // linefeed
            if (ch == '\n') {
                return Token.LITERAL;
            }
            // now check for ATOM disqualifiers
            if (atomDelimiters.indexOf(ch) != -1) {
                return Token.QUOTEDSTRING;
            }
            // CTL character.  We've already eliminated the high characters
            if (ch < 0x20) {
                return Token.QUOTEDSTRING;
            }
        }
        // this can be an ATOM token
        return Token.ATOM;
    }


    /**
     * Read a ContentType or ContentDisposition parameter
     * list from an IMAP command response.
     *
     * @return A ParameterList instance containing the parameters.
     * @exception MessagingException
     */
    public ParameterList readParameterList() throws MessagingException {
        ParameterList params = new ParameterList();

        // read the tokens, taking NIL into account.
        Token token = next(true, false);

        // just return an empty list if this is NIL
        if (token.isType(token.NIL)) {
            return params;
        }

        // these are pairs of strings for each parameter value
        while (notListEnd()) {
            String name = readString();
            String value = readString();
            params.set(name, value);
        }
        // we need to consume the list terminator
        checkRightParen();
        return params;
    }


    /**
     * Test if we have more data in the response buffer.
     *
     * @return true if there are more tokens to process.  false if
     *         we've reached the end of the stream.
     */
    public boolean hasMore() throws MessagingException {
        // we need to eat any white space that might be in the stream.
        eatWhiteSpace();
        return pos < response.length;
    }


    /**
     * Tests if we've reached the end of a parenthetical
     * list in our parsing stream.
     *
     * @return true if the next token will be a ')'.  false if the
     *         next token is anything else.
     * @exception MessagingException
     */
    public boolean notListEnd() throws MessagingException {
        return peek().getType() != ')';
    }

    /**
     * Read a list of Flag values from an IMAP response,
     * returning a Flags instance containing the appropriate
     * pieces.
     *
     * @return A Flags instance with the flag values.
     * @exception MessagingException
     */
    public Flags readFlagList() throws MessagingException {
        Flags flags = new Flags();

        // this should be a list here
        checkLeftParen();

        // run through the flag list
        while (notListEnd()) {
            // the flags are a bit of a pain.  The flag names include "\" in the name, which
            // is not a character allowed in an atom.  This requires a bit of customized parsing
            // to handle this.
            Token token = next();
            // flags can be specified as just atom tokens, so allow this as a user flag.
            if (token.isType(token.ATOM)) {
                // append the atom as a raw name
                flags.add(token.getValue());
            }
            // all of the system flags start with a '\' followed by
            // an atom.  They also can be extension flags.  IMAP has a special
            // case of "\*" that we need to check for.
            else if (token.isType('\\')) {
                token = next();
                // the next token is the real bit we need to process.
                if (token.isType('*')) {
                    // this indicates USER flags are allowed.
                    flags.add(Flags.Flag.USER);
                }
                // if this is an atom name, handle as a system flag
                else if (token.isType(Token.ATOM)) {
                    String name = token.getValue();
                    if (name.equalsIgnoreCase("Seen")) {
                        flags.add(Flags.Flag.SEEN);
                    }
                    else if (name.equalsIgnoreCase("RECENT")) {
                        flags.add(Flags.Flag.RECENT);
                    }
                    else if (name.equalsIgnoreCase("DELETED")) {
                        flags.add(Flags.Flag.DELETED);
                    }
                    else if (name.equalsIgnoreCase("ANSWERED")) {
                        flags.add(Flags.Flag.ANSWERED);
                    }
                    else if (name.equalsIgnoreCase("DRAFT")) {
                        flags.add(Flags.Flag.DRAFT);
                    }
                    else if (name.equalsIgnoreCase("FLAGGED")) {
                        flags.add(Flags.Flag.FLAGGED);
                    }
                    else {
                        // this is a server defined flag....just add the name with the
                        // flag thingy prepended.
                        flags.add("\\" + name);
                    }
                }
                else {
                    throw new MessagingException("Invalid Flag: " + token.getValue());
                }
            }
            else {
                throw new MessagingException("Invalid Flag: " + token.getValue());
            }
        }

        // step over this for good practice.
        checkRightParen();

        return flags;
    }


    /**
     * Read a list of Flag values from an IMAP response,
     * returning a Flags instance containing the appropriate
     * pieces.
     *
     * @return A Flags instance with the flag values.
     * @exception MessagingException
     */
    public List readSystemNameList() throws MessagingException {
        List flags = new ArrayList();

        // this should be a list here
        checkLeftParen();

        // run through the flag list
        while (notListEnd()) {
            // the flags are a bit of a pain.  The flag names include "\" in the name, which
            // is not a character allowed in an atom.  This requires a bit of customized parsing
            // to handle this.
            Token token = next();
            // all of the system flags start with a '\' followed by
            // an atom.  They also can be extension flags.  IMAP has a special
            // case of "\*" that we need to check for.
            if (token.isType('\\')) {
                token = next();
                // if this is an atom name, handle as a system flag
                if (token.isType(Token.ATOM)) {
                    // add the token value to the list WITH the
                    // flag indicator included.  The attributes method returns
                    // these flag indicators, so we need to include it.
                    flags.add("\\" + token.getValue());
                }
                else {
                    throw new MessagingException("Invalid Flag: " + token.getValue());
                }
            }
            else {
                throw new MessagingException("Invalid Flag: " + token.getValue());
            }
        }

        // step over this for good practice.
        checkRightParen();

        return flags;
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -



