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