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