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.DataOutputStream; 022 import java.io.IOException; 023 import java.io.OutputStream; 024 import java.io.UnsupportedEncodingException; 025 import java.text.SimpleDateFormat; 026 import java.util.ArrayList; 027 import java.util.Date; 028 import java.util.List; 029 import java.util.Vector; 030 031 import javax.mail.FetchProfile; 032 import javax.mail.Flags; 033 import javax.mail.Message; 034 import javax.mail.MessagingException; 035 import javax.mail.Quota; 036 import javax.mail.UIDFolder; 037 038 import javax.mail.search.AddressTerm; 039 import javax.mail.search.AndTerm; 040 import javax.mail.search.BodyTerm; 041 import javax.mail.search.ComparisonTerm; 042 import javax.mail.search.DateTerm; 043 import javax.mail.search.FlagTerm; 044 import javax.mail.search.FromTerm; 045 import javax.mail.search.FromStringTerm; 046 import javax.mail.search.HeaderTerm; 047 import javax.mail.search.MessageIDTerm; 048 import javax.mail.search.MessageNumberTerm; 049 import javax.mail.search.NotTerm; 050 import javax.mail.search.OrTerm; 051 import javax.mail.search.ReceivedDateTerm; 052 import javax.mail.search.RecipientTerm; 053 import javax.mail.search.RecipientStringTerm; 054 import javax.mail.search.SearchException; 055 import javax.mail.search.SearchTerm; 056 import javax.mail.search.SentDateTerm; 057 import javax.mail.search.SizeTerm; 058 import javax.mail.search.StringTerm; 059 import javax.mail.search.SubjectTerm; 060 061 import org.apache.geronimo.javamail.store.imap.ACL; 062 import org.apache.geronimo.javamail.store.imap.IMAPFolder; 063 import org.apache.geronimo.javamail.store.imap.Rights; 064 import org.apache.geronimo.javamail.store.imap.connection.IMAPResponseTokenizer.Token; 065 066 import org.apache.geronimo.javamail.util.CommandFailedException; 067 068 069 /** 070 * Utility class for building up what might be complex arguments 071 * to a command. This includes the ability to directly write out 072 * binary arrays of data and have them constructed as IMAP 073 * literals. 074 */ 075 public class IMAPCommand { 076 077 // digits table for encoding IMAP modified Base64. Note that this differs 078 // from "normal" base 64 by using ',' instead of '/' for the last digit. 079 public static final char[] encodingTable = { 080 'A', 'B', 'C', 'D', 'E', 'F', 'G', 081 'H', 'I', 'J', 'K', 'L', 'M', 'N', 082 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 083 'V', 'W', 'X', 'Y', 'Z', 084 'a', 'b', 'c', 'd', 'e', 'f', 'g', 085 'h', 'i', 'j', 'k', 'l', 'm', 'n', 086 'o', 'p', 'q', 'r', 's', 't', 'u', 087 'v', 'w', 'x', 'y', 'z', 088 '0', '1', '2', '3', '4', '5', '6', 089 '7', '8', '9', 090 '+', ',' 091 }; 092 093 protected boolean needWhiteSpace = false; 094 095 // our utility writer stream 096 protected DataOutputStream out; 097 // the real output target 098 protected ByteArrayOutputStream sink; 099 // our command segment set. If the command contains literals, then the literal 100 // data must be sent after receiving an continue response back from the server. 101 protected List segments = null; 102 // the append tag for the response 103 protected String tag; 104 105 // our counter used to generate command tags. 106 static protected int tagCounter = 0; 107 108 /** 109 * Create an empty command. 110 */ 111 public IMAPCommand() { 112 try { 113 sink = new ByteArrayOutputStream(); 114 out = new DataOutputStream(sink); 115 116 // write the tag data at the beginning of the command. 117 out.writeBytes(getTag()); 118 // need a blank separator 119 out.write(' '); 120 } catch (IOException e ) { 121 } 122 } 123 124 /** 125 * Create a command with an initial command string. 126 * 127 * @param command The command string used to start this command. 128 */ 129 public IMAPCommand(String command) { 130 this(); 131 append(command); 132 } 133 134 public String getTag() { 135 if (tag == null) { 136 // the tag needs to be non-numeric, so tack a convenient alpha character on the front. 137 tag = "a" + tagCounter++; 138 } 139 return tag; 140 } 141 142 143 /** 144 * Save the current segment of the command we've accumulated. This 145 * generally occurs because we have a literal element in the command 146 * that's going to require a continuation response from the server before 147 * we can send it. 148 */ 149 private void saveCurrentSegment() 150 { 151 try { 152 out.flush(); // make sure everything is written 153 // get the data so far and reset the sink 154 byte[] segment = sink.toByteArray(); 155 sink.reset(); 156 // most commands don't have segments, so don't create the list until we do. 157 if (segments == null) { 158 segments = new ArrayList(); 159 } 160 // ok, we need to issue this command as a conversation. 161 segments.add(segment); 162 } catch (IOException e) { 163 } 164 } 165 166 167 /** 168 * Write all of the command data to the stream. This includes the 169 * leading tag data. 170 * 171 * @param outStream 172 * @param connection 173 * 174 * @exception IOException 175 * @exception MessagingException 176 */ 177 public void writeTo(OutputStream outStream, IMAPConnection connection) throws IOException, MessagingException 178 { 179 180 // just a simple, single string-encoded command? 181 if (segments == null) { 182 // make sure the output stream is flushed 183 out.flush(); 184 // just copy the command data to the output stream 185 sink.writeTo(outStream); 186 // we need to end the command with a CRLF sequence. 187 outStream.write('\r'); 188 outStream.write('\n'); 189 } 190 // multiple-segment mode, which means we need to deal with continuation responses at 191 // each of the literal boundaries. 192 else { 193 // at this point, we have a list of command pieces that must be written out, then a 194 // continuation response checked for after each write. Once each of these pieces is 195 // written out, we still have command stuff pending in the out stream, which we'll tack 196 // on to the end. 197 for (int i = 0; i < segments.size(); i++) { 198 outStream.write((byte [])segments.get(i)); 199 // now wait for a response from the connection. We should be getting a 200 // continuation response back (and might have also received some asynchronous 201 // replies, which we'll leave in the queue for now. If we get some status back 202 // other than than a continue, we've got an error in our command somewhere. 203 IMAPTaggedResponse response = connection.receiveResponse(); 204 if (!response.isContinuation()) { 205 throw new CommandFailedException("Error response received on a IMAP continued command: " + response); 206 } 207 } 208 out.flush(); 209 // all leading segments written with the appropriate continuation received in reply. 210 // just copy the command data to the output stream 211 sink.writeTo(outStream); 212 // we need to end the command with a CRLF sequence. 213 outStream.write('\r'); 214 outStream.write('\n'); 215 } 216 } 217 218 219 /** 220 * Directly append a value to the buffer without attempting 221 * to insert whitespace or figure out any format encodings. 222 * 223 * @param value The value to append. 224 */ 225 public void append(String value) { 226 try { 227 // add the bytes direcly 228 out.writeBytes(value); 229 // assume we're needing whitespace after this (pretty much unknown). 230 needWhiteSpace = true; 231 } catch (IOException e) { 232 } 233 } 234 235 236 /** 237 * Append a string value to a command buffer. This sorts out 238 * what form the string needs to be appended in (LITERAL, QUOTEDSTRING, 239 * or ATOM). 240 * 241 * @param target The target buffer for appending the string. 242 * @param value The value to append. 243 */ 244 public void appendString(String value) { 245 // work off the byte values 246 appendString(value.getBytes()); 247 } 248 249 250 /** 251 * Append a string value to a command buffer. This always appends as 252 * a QUOTEDSTRING 253 * 254 * @param value The value to append. 255 */ 256 public void appendQuotedString(String value) { 257 // work off the byte values 258 appendQuotedString(value.getBytes()); 259 } 260 261 262 /** 263 * Append a string value to a command buffer, with encoding. This sorts out 264 * what form the string needs to be appended in (LITERAL, QUOTEDSTRING, 265 * or ATOM). 266 * 267 * @param target The target buffer for appending the string. 268 * @param value The value to append. 269 */ 270 public void appendEncodedString(String value) { 271 // encode first. 272 value = encode(value); 273 // work off the byte values 274 appendString(value.getBytes()); 275 } 276 277 278 /** 279 * Encode a string using the modified UTF-7 encoding. 280 * 281 * @param original The original string. 282 * 283 * @return The original string encoded with modified UTF-7 encoding. 284 */ 285 public String encode(String original) { 286 287 // buffer for encoding sections of data 288 byte[] buffer = new byte[4]; 289 int bufferCount = 0; 290 291 StringBuffer result = new StringBuffer(); 292 293 // state flag for the type of section we're in. 294 boolean encoding = false; 295 296 for (int i = 0; i < original.length(); i++) { 297 char ch = original.charAt(i); 298 299 // processing an encoded section? 300 if (encoding) { 301 // is this a printable character? 302 if (ch > 31 && ch < 127) { 303 // encode anything in the buffer 304 encode(buffer, bufferCount, result); 305 // add the section terminator char 306 result.append('-'); 307 encoding = false; 308 // we now fall through to the printable character section. 309 } 310 // still an unprintable 311 else { 312 // add this char to the working buffer? 313 buffer[++bufferCount] = (byte)(ch >> 8); 314 buffer[++bufferCount] = (byte)(ch & 0xff); 315 // if we have enough to encode something, do it now. 316 if (bufferCount >= 3) { 317 bufferCount = encode(buffer, bufferCount, result); 318 } 319 // go back to the top of the loop. 320 continue; 321 } 322 } 323 // is this the special printable? 324 if (ch == '&') { 325 // this is the special null escape sequence 326 result.append('&'); 327 result.append('-'); 328 } 329 // is this a printable character? 330 else if (ch > 31 && ch < 127) { 331 // just add to the result 332 result.append(ch); 333 } 334 else { 335 // write the escape character 336 result.append('&'); 337 338 // non-printable ASCII character, we need to switch modes 339 // both bytes of this character need to be encoded. Each 340 // encoded digit will basically be a "character-and-a-half". 341 buffer[0] = (byte)(ch >> 8); 342 buffer[1] = (byte)(ch & 0xff); 343 bufferCount = 2; 344 encoding = true; 345 } 346 } 347 // were we in a non-printable section at the end? 348 if (encoding) { 349 // take care of any remaining characters 350 encode(buffer, bufferCount, result); 351 // add the section terminator char 352 result.append('-'); 353 } 354 // convert the encoded string. 355 return result.toString(); 356 } 357 358 359 /** 360 * Encode a single buffer of characters. This buffer will have 361 * between 0 and 4 bytes to encode. 362 * 363 * @param buffer The buffer to encode. 364 * @param count The number of characters in the buffer. 365 * @param result The accumulator for appending the result. 366 * 367 * @return The remaining number of bytes remaining in the buffer (return 0 368 * unless the count was 4 at the beginning). 369 */ 370 protected static int encode(byte[] buffer, int count, StringBuffer result) { 371 byte b1 = 0; 372 byte b2 = 0; 373 byte b3 = 0; 374 375 // different processing based on how much we have in the buffer 376 switch (count) { 377 // ended at a boundary. This is cool, not much to do. 378 case 0: 379 // no residual in the buffer 380 return 0; 381 382 // just a single left over byte from the last encoding op. 383 case 1: 384 b1 = buffer[0]; 385 result.append(encodingTable[(b1 >>> 2) & 0x3f]); 386 result.append(encodingTable[(b1 << 4) & 0x30]); 387 return 0; 388 389 // one complete char to encode 390 case 2: 391 b1 = buffer[0]; 392 b2 = buffer[1]; 393 result.append(encodingTable[(b1 >>> 2) & 0x3f]); 394 result.append(encodingTable[((b1 << 4) & 0x30) + ((b2 >>>4) & 0x0f)]); 395 result.append(encodingTable[((b2 << 2) & (0x3c))]); 396 return 0; 397 398 // at least a full triplet of bytes to encode 399 case 3: 400 case 4: 401 b1 = buffer[0]; 402 b2 = buffer[1]; 403 b3 = buffer[2]; 404 result.append(encodingTable[(b1 >>> 2) & 0x3f]); 405 result.append(encodingTable[((b1 << 4) & 0x30) + ((b2 >>>4) & 0x0f)]); 406 result.append(encodingTable[((b2 << 2) & 0x3c) + ((b3 >>> 6) & 0x03)]); 407 result.append(encodingTable[b3 & 0x3f]); 408 409 // if we have more than the triplet, we need to move the extra one into the first 410 // position and return the residual indicator 411 if (count == 4) { 412 buffer[0] = buffer[4]; 413 return 1; 414 } 415 return 0; 416 } 417 return 0; 418 } 419 420 421 /** 422 * Append a string value to a command buffer. This sorts out 423 * what form the string needs to be appended in (LITERAL, QUOTEDSTRING, 424 * or ATOM). 425 * 426 * @param target The target buffer for appending the string. 427 * @param value The value to append. 428 */ 429 public void appendString(String value, String charset) throws MessagingException { 430 if (charset == null) { 431 // work off the byte values 432 appendString(value.getBytes()); 433 } 434 else { 435 try { 436 // use the charset to extract the bytes 437 appendString(value.getBytes(charset)); 438 } catch (UnsupportedEncodingException e) { 439 throw new MessagingException("Invalid text encoding"); 440 } 441 } 442 } 443 444 445 /** 446 * Append a value in a byte array to a command buffer. This sorts out 447 * what form the string needs to be appended in (LITERAL, QUOTEDSTRING, 448 * or ATOM). 449 * 450 * @param target The target buffer for appending the string. 451 * @param value The value to append. 452 */ 453 public void appendString(byte[] value) { 454 // sort out how we need to append this 455 switch (IMAPResponseTokenizer.getEncoding(value)) { 456 case Token.LITERAL: 457 appendLiteral(value); 458 break; 459 case Token.QUOTEDSTRING: 460 appendQuotedString(value); 461 break; 462 case Token.ATOM: 463 appendAtom(value); 464 break; 465 } 466 } 467 468 469 /** 470 * Append an integer value to the command, converting 471 * the integer into string form. 472 * 473 * @param value The value to append. 474 */ 475 public void appendInteger(int value) { 476 appendAtom(Integer.toString(value)); 477 } 478 479 480 /** 481 * Append a long value to the command, converting 482 * the integer into string form. 483 * 484 * @param value The value to append. 485 */ 486 public void appendLong(long value) { 487 appendAtom(Long.toString(value)); 488 } 489 490 491 /** 492 * Append an atom value to the command. Atoms are directly 493 * appended without using literal encodings. 494 * 495 * @param value The value to append. 496 */ 497 public void appendAtom(String value) { 498 appendAtom(value.getBytes()); 499 } 500 501 502 503 /** 504 * Append an atom to the command buffer. Atoms are directly 505 * appended without using literal encodings. White space is 506 * accounted for with the append operation. 507 * 508 * @param value The value to append. 509 */ 510 public void appendAtom(byte[] value) { 511 try { 512 // give a token separator 513 conditionalWhitespace(); 514 // ATOMs are easy 515 out.write(value); 516 } catch (IOException e) { 517 } 518 } 519 520 521 /** 522 * Append an IMAP literal values to the command. 523 * literals are written using a header with the length 524 * specified, followed by a CRLF sequence, followed 525 * by the literal data. 526 * 527 * @param value The literal data to write. 528 */ 529 public void appendLiteral(byte[] value) { 530 try { 531 appendLiteralHeader(value.length); 532 out.write(value); 533 } catch (IOException e) { 534 } 535 } 536 537 /** 538 * Add a literal header to the buffer. The literal 539 * header is the literal length enclosed in a 540 * "{n}" pair, followed by a CRLF sequence. 541 * 542 * @param size The size of the literal value. 543 */ 544 protected void appendLiteralHeader(int size) { 545 try { 546 conditionalWhitespace(); 547 out.writeByte('{'); 548 out.writeBytes(Integer.toString(size)); 549 out.writeBytes("}\r\n"); 550 // the IMAP client is required to send literal data to the server by 551 // writing the command up to the header, then waiting for a continuation 552 // response to send the rest. 553 saveCurrentSegment(); 554 } catch (IOException e) { 555 } 556 } 557 558 559 /** 560 * Append literal data to the command where the 561 * literal sourcd is a ByteArrayOutputStream. 562 * 563 * @param value The source of the literal data. 564 */ 565 public void appendLiteral(ByteArrayOutputStream value) { 566 try { 567 appendLiteralHeader(value.size()); 568 // have this output stream write directly into our stream 569 value.writeTo(out); 570 } catch (IOException e) { 571 } 572 } 573 574 /** 575 * Write out a string of literal data, taking into 576 * account the need to escape both '"' and '\' 577 * characters. 578 * 579 * @param value The bytes of the string to write. 580 */ 581 public void appendQuotedString(byte[] value) { 582 try { 583 conditionalWhitespace(); 584 out.writeByte('"'); 585 586 // look for chars requiring escaping 587 for (int i = 0; i < value.length; i++) { 588 byte ch = value[i]; 589 590 if (ch == '"' || ch == '\\') { 591 out.writeByte('\\'); 592 } 593 out.writeByte(ch); 594 } 595 596 out.writeByte('"'); 597 } catch (IOException e) { 598 } 599 } 600 601 /** 602 * Mark the start of a list value being written to 603 * the command. A list is a sequences of different 604 * tokens enclosed in "(" ")" pairs. Lists can 605 * be nested. 606 */ 607 public void startList() { 608 try { 609 conditionalWhitespace(); 610 out.writeByte('('); 611 needWhiteSpace = false; 612 } catch (IOException e) { 613 } 614 } 615 616 /** 617 * Write out the end of the list. 618 */ 619 public void endList() { 620 try { 621 out.writeByte(')'); 622 needWhiteSpace = true; 623 } catch (IOException e) { 624 } 625 } 626 627 628 /** 629 * Add a whitespace character to the command if the 630 * previous token was a type that required a 631 * white space character to mark the boundary. 632 */ 633 protected void conditionalWhitespace() { 634 try { 635 if (needWhiteSpace) { 636 out.writeByte(' '); 637 } 638 // all callers of this are writing a token that will need white space following, so turn this on 639 // every time we're called. 640 needWhiteSpace = true; 641 } catch (IOException e) { 642 } 643 } 644 645 646 /** 647 * Append a body section specification to a command string. Body 648 * section specifications are of the form "[section]<start.count>". 649 * 650 * @param section The section numeric identifier. 651 * @param partName The name of the body section we want (e.g. "TEST", "HEADERS"). 652 */ 653 public void appendBodySection(String section, String partName) { 654 try { 655 // we sometimes get called from the top level 656 if (section == null) { 657 appendBodySection(partName); 658 return; 659 } 660 661 out.writeByte('['); 662 out.writeBytes(section); 663 if (partName != null) { 664 out.writeByte('.'); 665 out.writeBytes(partName); 666 } 667 out.writeByte(']'); 668 needWhiteSpace = true; 669 } catch (IOException e) { 670 } 671 } 672 673 674 /** 675 * Append a body section specification to a command string. Body 676 * section specifications are of the form "[section]". 677 * 678 * @param partName The partname we require. 679 */ 680 public void appendBodySection(String partName) { 681 try { 682 out.writeByte('['); 683 out.writeBytes(partName); 684 out.writeByte(']'); 685 needWhiteSpace = true; 686 } catch (IOException e) { 687 } 688 } 689 690 691 /** 692 * Append a set of flags to a command buffer. 693 * 694 * @param flags The flag set to append. 695 */ 696 public void appendFlags(Flags flags) { 697 startList(); 698 699 Flags.Flag[] systemFlags = flags.getSystemFlags(); 700 701 // process each of the system flag names 702 for (int i = 0; i < systemFlags.length; i++) { 703 Flags.Flag flag = systemFlags[i]; 704 705 if (flag == Flags.Flag.ANSWERED) { 706 appendAtom("\\Answered"); 707 } 708 else if (flag == Flags.Flag.DELETED) { 709 appendAtom("\\Deleted"); 710 } 711 else if (flag == Flags.Flag.DRAFT) { 712 appendAtom("\\Draft"); 713 } 714 else if (flag == Flags.Flag.FLAGGED) { 715 appendAtom("\\Flagged"); 716 } 717 else if (flag == Flags.Flag.RECENT) { 718 appendAtom("\\Recent"); 719 } 720 else if (flag == Flags.Flag.SEEN) { 721 appendAtom("\\Seen"); 722 } 723 } 724 725 // now process the user flags, which just get appended as is. 726 String[] userFlags = flags.getUserFlags(); 727 728 for (int i = 0; i < userFlags.length; i++) { 729 appendAtom(userFlags[i]); 730 } 731 732 // close the list off 733 endList(); 734 } 735 736 737 /** 738 * Format a date into the form required for IMAP commands. 739 * 740 * @param d The source Date. 741 */ 742 public void appendDate(Date d) { 743 // get a formatter to create IMAP dates. Use the US locale, as the dates are not localized. 744 IMAPDateFormat formatter = new IMAPDateFormat(); 745 // date_time strings need to be done as quoted strings because they contain blanks. 746 appendString(formatter.format(d)); 747 } 748 749 750 /** 751 * Format a date into the form required for IMAP search commands. 752 * 753 * @param d The source Date. 754 */ 755 public void appendSearchDate(Date d) { 756 // get a formatter to create IMAP dates. Use the US locale, as the dates are not localized. 757 IMAPSearchDateFormat formatter = new IMAPSearchDateFormat(); 758 // date_time strings need to be done as quoted strings because they contain blanks. 759 appendString(formatter.format(d)); 760 } 761 762 763 /** 764 * append an IMAP search sequence from a SearchTerm. SearchTerms 765 * terms can be complex sets of terms in a tree form, so this 766 * may involve some recursion to completely translate. 767 * 768 * @param term The search term we're processing. 769 * @param charset The charset we need to use when generating the sequence. 770 * 771 * @exception MessagingException 772 */ 773 public void appendSearchTerm(SearchTerm term, String charset) throws MessagingException { 774 // we need to do this manually, by inspecting the term object against the various SearchTerm types 775 // defined by the javamail spec. 776 777 // Flag searches are used internally by other operations, so this is a good one to check first. 778 if (term instanceof FlagTerm) { 779 appendFlag((FlagTerm)term, charset); 780 } 781 // after that, I'm not sure there's any optimal order to these. Let's start with the conditional 782 // modifiers (AND, OR, NOT), then just hit each of the header types 783 else if (term instanceof AndTerm) { 784 appendAnd((AndTerm)term, charset); 785 } 786 else if (term instanceof OrTerm) { 787 appendOr((OrTerm)term, charset); 788 } 789 else if (term instanceof NotTerm) { 790 appendNot((NotTerm)term, charset); 791 } 792 // multiple forms of From: search 793 else if (term instanceof FromTerm) { 794 appendFrom((FromTerm)term, charset); 795 } 796 else if (term instanceof FromStringTerm) { 797 appendFrom((FromStringTerm)term, charset); 798 } 799 else if (term instanceof HeaderTerm) { 800 appendHeader((HeaderTerm)term, charset); 801 } 802 else if (term instanceof RecipientTerm) { 803 appendRecipient((RecipientTerm)term, charset); 804 } 805 else if (term instanceof RecipientStringTerm) { 806 appendRecipient((RecipientStringTerm)term, charset); 807 } 808 else if (term instanceof SubjectTerm) { 809 appendSubject((SubjectTerm)term, charset); 810 } 811 else if (term instanceof BodyTerm) { 812 appendBody((BodyTerm)term, charset); 813 } 814 else if (term instanceof SizeTerm) { 815 appendSize((SizeTerm)term, charset); 816 } 817 else if (term instanceof SentDateTerm) { 818 appendSentDate((SentDateTerm)term, charset); 819 } 820 else if (term instanceof ReceivedDateTerm) { 821 appendReceivedDate((ReceivedDateTerm)term, charset); 822 } 823 else if (term instanceof MessageIDTerm) { 824 appendMessageID((MessageIDTerm)term, charset); 825 } 826 else { 827 // don't know what this is 828 throw new SearchException("Unsupported search type"); 829 } 830 } 831 832 /** 833 * append IMAP search term information from a FlagTerm item. 834 * 835 * @param term The source FlagTerm 836 * @param charset target charset for the search information (can be null). 837 * @param out The target command buffer. 838 */ 839 protected void appendFlag(FlagTerm term, String charset) { 840 // decide which one we need to test for 841 boolean set = term.getTestSet(); 842 843 Flags flags = term.getFlags(); 844 Flags.Flag[] systemFlags = flags.getSystemFlags(); 845 846 String[] userFlags = flags.getUserFlags(); 847 848 // empty search term? not sure if this is an error. The default search implementation would 849 // not consider this an error, so we'll just ignore this. 850 if (systemFlags.length == 0 && userFlags.length == 0) { 851 return; 852 } 853 854 if (set) { 855 for (int i = 0; i < systemFlags.length; i++) { 856 Flags.Flag flag = systemFlags[i]; 857 858 if (flag == Flags.Flag.ANSWERED) { 859 appendAtom("ANSWERED"); 860 } 861 else if (flag == Flags.Flag.DELETED) { 862 appendAtom("DELETED"); 863 } 864 else if (flag == Flags.Flag.DRAFT) { 865 appendAtom("DRAFT"); 866 } 867 else if (flag == Flags.Flag.FLAGGED) { 868 appendAtom("FLAGGED"); 869 } 870 else if (flag == Flags.Flag.RECENT) { 871 appendAtom("RECENT"); 872 } 873 else if (flag == Flags.Flag.SEEN) { 874 appendAtom("SEEN"); 875 } 876 } 877 } 878 else { 879 for (int i = 0; i < systemFlags.length; i++) { 880 Flags.Flag flag = systemFlags[i]; 881 882 if (flag == Flags.Flag.ANSWERED) { 883 appendAtom("UNANSWERED"); 884 } 885 else if (flag == Flags.Flag.DELETED) { 886 appendAtom("UNDELETED"); 887 } 888 else if (flag == Flags.Flag.DRAFT) { 889 appendAtom("UNDRAFT"); 890 } 891 else if (flag == Flags.Flag.FLAGGED) { 892 appendAtom("UNFLAGGED"); 893 } 894 else if (flag == Flags.Flag.RECENT) { 895 // not UNRECENT? 896 appendAtom("OLD"); 897 } 898 else if (flag == Flags.Flag.SEEN) { 899 appendAtom("UNSEEN"); 900 } 901 } 902 } 903 904 905 // User flags are done as either "KEYWORD name" or "UNKEYWORD name" 906 for (int i = 0; i < userFlags.length; i++) { 907 appendAtom(set ? "KEYWORD" : "UNKEYWORD"); 908 appendAtom(userFlags[i]); 909 } 910 } 911 912 913 /** 914 * append IMAP search term information from an AndTerm item. 915 * 916 * @param term The source AndTerm 917 * @param charset target charset for the search information (can be null). 918 * @param out The target command buffer. 919 */ 920 protected void appendAnd(AndTerm term, String charset) throws MessagingException { 921 // ANDs are pretty easy. Just append all of the terms directly to the 922 // command as is. 923 924 SearchTerm[] terms = term.getTerms(); 925 926 for (int i = 0; i < terms.length; i++) { 927 appendSearchTerm(terms[i], charset); 928 } 929 } 930 931 932 /** 933 * append IMAP search term information from an OrTerm item. 934 * 935 * @param term The source OrTerm 936 * @param charset target charset for the search information (can be null). 937 * @param out The target command buffer. 938 */ 939 protected void appendOr(OrTerm term, String charset) throws MessagingException { 940 SearchTerm[] terms = term.getTerms(); 941 942 // OrTerms are a bit of a pain to translate to IMAP semantics. The IMAP OR operation only allows 2 943 // search keys, while OrTerms can have n keys (including, it appears, just one! If we have more than 944 // 2, it's easiest to convert this into a tree of OR keys and let things generate that way. The 945 // resulting IMAP operation would be OR (key1) (OR (key2) (key3)) 946 947 // silly rabbit...somebody doesn't know how to use OR 948 if (terms.length == 1) { 949 // just append the singleton in place without the OR operation. 950 appendSearchTerm(terms[0], charset); 951 return; 952 } 953 954 // is this a more complex operation? 955 if (terms.length > 2) { 956 // have to chain these together (shazbat). 957 SearchTerm current = terms[0]; 958 959 for (int i = 1; i < terms.length; i++) { 960 current = new OrTerm(current, terms[i]); 961 } 962 963 // replace the term array with the newly generated top array 964 terms = ((OrTerm)current).getTerms(); 965 } 966 967 // we're going to generate this with parenthetical search keys, even if it is just a simple term. 968 appendAtom("OR"); 969 startList(); 970 // generated OR argument 1 971 appendSearchTerm(terms[0], charset); 972 endList(); 973 startList(); 974 // generated OR argument 2 975 appendSearchTerm(terms[0], charset); 976 // and the closing parens 977 endList(); 978 } 979 980 981 /** 982 * append IMAP search term information from a NotTerm item. 983 * 984 * @param term The source NotTerm 985 * @param charset target charset for the search information (can be null). 986 */ 987 protected void appendNot(NotTerm term, String charset) throws MessagingException { 988 // we're goint to generate this with parenthetical search keys, even if it is just a simple term. 989 appendAtom("NOT"); 990 startList(); 991 // generated the NOT expression 992 appendSearchTerm(term.getTerm(), charset); 993 // and the closing parens 994 endList(); 995 } 996 997 998 /** 999 * append IMAP search term information from a FromTerm item. 1000 * 1001 * @param term The source FromTerm 1002 * @param charset target charset for the search information (can be null). 1003 */ 1004 protected void appendFrom(FromTerm term, String charset) throws MessagingException { 1005 appendAtom("FROM"); 1006 // this may require encoding 1007 appendString(term.getAddress().toString(), charset); 1008 } 1009 1010 1011 /** 1012 * append IMAP search term information from a FromStringTerm item. 1013 * 1014 * @param term The source FromStringTerm 1015 * @param charset target charset for the search information (can be null). 1016 */ 1017 protected void appendFrom(FromStringTerm term, String charset) throws MessagingException { 1018 appendAtom("FROM"); 1019 // this may require encoding 1020 appendString(term.getPattern(), charset); 1021 } 1022 1023 1024 /** 1025 * append IMAP search term information from a RecipientTerm item. 1026 * 1027 * @param term The source RecipientTerm 1028 * @param charset target charset for the search information (can be null). 1029 */ 1030 protected void appendRecipient(RecipientTerm term, String charset) throws MessagingException { 1031 appendAtom(recipientType(term.getRecipientType())); 1032 // this may require encoding 1033 appendString(term.getAddress().toString(), charset); 1034 } 1035 1036 1037 /** 1038 * append IMAP search term information from a RecipientStringTerm item. 1039 * 1040 * @param term The source RecipientStringTerm 1041 * @param charset target charset for the search information (can be null). 1042 */ 1043 protected void appendRecipient(RecipientStringTerm term, String charset) throws MessagingException { 1044 appendAtom(recipientType(term.getRecipientType())); 1045 // this may require encoding 1046 appendString(term.getPattern(), charset); 1047 } 1048 1049 1050 /** 1051 * Translate a recipient type into it's string name equivalent. 1052 * 1053 * @param type The source recipient type 1054 * 1055 * @return A string name matching the recipient type. 1056 */ 1057 protected String recipientType(Message.RecipientType type) throws MessagingException { 1058 if (type == Message.RecipientType.TO) { 1059 return "TO"; 1060 } 1061 if (type == Message.RecipientType.CC) { 1062 return "CC"; 1063 } 1064 if (type == Message.RecipientType.BCC) { 1065 return "BCC"; 1066 } 1067 1068 throw new SearchException("Unsupported RecipientType"); 1069 } 1070 1071 1072 /** 1073 * append IMAP search term information from a HeaderTerm item. 1074 * 1075 * @param term The source HeaderTerm 1076 * @param charset target charset for the search information (can be null). 1077 */ 1078 protected void appendHeader(HeaderTerm term, String charset) throws MessagingException { 1079 appendAtom("HEADER"); 1080 appendString(term.getHeaderName()); 1081 appendString(term.getPattern(), charset); 1082 } 1083 1084 1085 1086 /** 1087 * append IMAP search term information from a SubjectTerm item. 1088 * 1089 * @param term The source SubjectTerm 1090 * @param charset target charset for the search information (can be null). 1091 */ 1092 protected void appendSubject(SubjectTerm term, String charset) throws MessagingException { 1093 appendAtom("SUBJECT"); 1094 appendString(term.getPattern(), charset); 1095 } 1096 1097 1098 /** 1099 * append IMAP search term information from a BodyTerm item. 1100 * 1101 * @param term The source BodyTerm 1102 * @param charset target charset for the search information (can be null). 1103 */ 1104 protected void appendBody(BodyTerm term, String charset) throws MessagingException { 1105 appendAtom("BODY"); 1106 appendString(term.getPattern(), charset); 1107 } 1108 1109 1110 /** 1111 * append IMAP search term information from a SizeTerm item. 1112 * 1113 * @param term The source SizeTerm 1114 * @param charset target charset for the search information (can be null). 1115 */ 1116 protected void appendSize(SizeTerm term, String charset) throws MessagingException { 1117 1118 // these comparisons can be a real pain. IMAP only supports LARGER and SMALLER. So comparisons 1119 // other than GT and LT have to be composed of complex sequences of these. For example, an EQ 1120 // comparison becomes NOT LARGER size NOT SMALLER size 1121 1122 if (term.getComparison() == ComparisonTerm.GT) { 1123 appendAtom("LARGER"); 1124 appendInteger(term.getNumber()); 1125 } 1126 else if (term.getComparison() == ComparisonTerm.LT) { 1127 appendAtom("SMALLER"); 1128 appendInteger(term.getNumber()); 1129 } 1130 else if (term.getComparison() == ComparisonTerm.EQ) { 1131 appendAtom("NOT"); 1132 appendAtom("LARGER"); 1133 appendInteger(term.getNumber()); 1134 1135 appendAtom("NOT"); 1136 appendAtom("SMALLER"); 1137 // it's just right <g> 1138 appendInteger(term.getNumber()); 1139 } 1140 else if (term.getComparison() == ComparisonTerm.NE) { 1141 // this needs to be an OR comparison 1142 appendAtom("OR"); 1143 appendAtom("LARGER"); 1144 appendInteger(term.getNumber()); 1145 1146 appendAtom("SMALLER"); 1147 appendInteger(term.getNumber()); 1148 } 1149 else if (term.getComparison() == ComparisonTerm.LE) { 1150 // just the inverse of LARGER 1151 appendAtom("NOT"); 1152 appendAtom("LARGER"); 1153 appendInteger(term.getNumber()); 1154 } 1155 else if (term.getComparison() == ComparisonTerm.GE) { 1156 // and the reverse. 1157 appendAtom("NOT"); 1158 appendAtom("SMALLER"); 1159 appendInteger(term.getNumber()); 1160 } 1161 } 1162 1163 1164 /** 1165 * append IMAP search term information from a MessageIDTerm item. 1166 * 1167 * @param term The source MessageIDTerm 1168 * @param charset target charset for the search information (can be null). 1169 */ 1170 protected void appendMessageID(MessageIDTerm term, String charset) throws MessagingException { 1171 1172 // not directly supported by IMAP, but we can compare on the header information. 1173 appendAtom("HEADER"); 1174 appendString("Message-ID"); 1175 appendString(term.getPattern(), charset); 1176 } 1177 1178 1179 /** 1180 * append IMAP search term information from a SendDateTerm item. 1181 * 1182 * @param term The source SendDateTerm 1183 * @param charset target charset for the search information (can be null). 1184 */ 1185 protected void appendSentDate(SentDateTerm term, String charset) throws MessagingException { 1186 Date date = term.getDate(); 1187 1188 switch (term.getComparison()) { 1189 case ComparisonTerm.EQ: 1190 appendAtom("SENTON"); 1191 appendSearchDate(date); 1192 break; 1193 case ComparisonTerm.LT: 1194 appendAtom("SENTBEFORE"); 1195 appendSearchDate(date); 1196 break; 1197 case ComparisonTerm.GT: 1198 appendAtom("SENTSINCE"); 1199 appendSearchDate(date); 1200 break; 1201 case ComparisonTerm.GE: 1202 appendAtom("OR"); 1203 appendAtom("SENTSINCE"); 1204 appendSearchDate(date); 1205 appendAtom("SENTON"); 1206 appendSearchDate(date); 1207 break; 1208 case ComparisonTerm.LE: 1209 appendAtom("OR"); 1210 appendAtom("SENTBEFORE"); 1211 appendSearchDate(date); 1212 appendAtom("SENTON"); 1213 appendSearchDate(date); 1214 break; 1215 case ComparisonTerm.NE: 1216 appendAtom("NOT"); 1217 appendAtom("SENTON"); 1218 appendSearchDate(date); 1219 break; 1220 default: 1221 throw new SearchException("Unsupported date comparison type"); 1222 } 1223 } 1224 1225 1226 /** 1227 * append IMAP search term information from a ReceivedDateTerm item. 1228 * 1229 * @param term The source ReceivedDateTerm 1230 * @param charset target charset for the search information (can be null). 1231 */ 1232 protected void appendReceivedDate(ReceivedDateTerm term, String charset) throws MessagingException { 1233 Date date = term.getDate(); 1234 1235 switch (term.getComparison()) { 1236 case ComparisonTerm.EQ: 1237 appendAtom("ON"); 1238 appendSearchDate(date); 1239 break; 1240 case ComparisonTerm.LT: 1241 appendAtom("BEFORE"); 1242 appendSearchDate(date); 1243 break; 1244 case ComparisonTerm.GT: 1245 appendAtom("SINCE"); 1246 appendSearchDate(date); 1247 break; 1248 case ComparisonTerm.GE: 1249 appendAtom("OR"); 1250 appendAtom("SINCE"); 1251 appendSearchDate(date); 1252 appendAtom("ON"); 1253 appendSearchDate(date); 1254 break; 1255 case ComparisonTerm.LE: 1256 appendAtom("OR"); 1257 appendAtom("BEFORE"); 1258 appendSearchDate(date); 1259 appendAtom("ON"); 1260 appendSearchDate(date); 1261 break; 1262 case ComparisonTerm.NE: 1263 appendAtom("NOT"); 1264 appendAtom("ON"); 1265 appendSearchDate(date); 1266 break; 1267 default: 1268 throw new SearchException("Unsupported date comparison type"); 1269 } 1270 } 1271 1272 1273 /** 1274 * Run the tree of search terms, checking for problems with 1275 * the terms that may require specifying a CHARSET modifier 1276 * on a SEARCH command sent to the server. 1277 * 1278 * @param term The term to check. 1279 * 1280 * @return True if there are 7-bit problems, false if the terms contain 1281 * only 7-bit ASCII characters. 1282 */ 1283 static public boolean checkSearchEncoding(SearchTerm term) { 1284 // StringTerm is the basis of most of the string-valued terms, and are most important ones to check. 1285 if (term instanceof StringTerm) { 1286 return checkStringEncoding(((StringTerm)term).getPattern()); 1287 } 1288 // Address terms are basically string terms also, but we need to check the string value of the 1289 // addresses, since that's what we're sending along. This covers a lot of the TO/FROM, etc. searches. 1290 else if (term instanceof AddressTerm) { 1291 return checkStringEncoding(((AddressTerm)term).getAddress().toString()); 1292 } 1293 // the NOT contains a term itself, so recurse on that. The NOT does not directly have string values 1294 // to check. 1295 else if (term instanceof NotTerm) { 1296 return checkSearchEncoding(((NotTerm)term).getTerm()); 1297 } 1298 // AND terms and OR terms have lists of subterms that must be checked. 1299 else if (term instanceof AndTerm) { 1300 return checkSearchEncoding(((AndTerm)term).getTerms()); 1301 } 1302 else if (term instanceof OrTerm) { 1303 return checkSearchEncoding(((OrTerm)term).getTerms()); 1304 } 1305 1306 // non of the other term types (FlagTerm, SentDateTerm, etc.) pose a problem, so we'll give them 1307 // a free pass. 1308 return false; 1309 } 1310 1311 1312 /** 1313 * Run an array of search term items to check each one for ASCII 1314 * encoding problems. 1315 * 1316 * @param terms The array of terms to check. 1317 * 1318 * @return True if any of the search terms contains a 7-bit ASCII problem, 1319 * false otherwise. 1320 */ 1321 static public boolean checkSearchEncoding(SearchTerm[] terms) { 1322 for (int i = 0; i < terms.length; i++) { 1323 if (checkSearchEncoding(terms[i])) { 1324 return true; 1325 } 1326 } 1327 return false; 1328 } 1329 1330 1331 /** 1332 * Check a string to see if this can be processed using just 1333 * 7-bit ASCII. 1334 * 1335 * @param s The string to check 1336 * 1337 * @return true if the string contains characters outside the 7-bit ascii range, 1338 * false otherwise. 1339 */ 1340 static public boolean checkStringEncoding(String s) { 1341 for (int i = 0; i < s.length(); i++) { 1342 // any value greater that 0x7f is a problem char. We're not worried about 1343 // lower ctl chars (chars < 32) since those are still expressible in 7-bit. 1344 if (s.charAt(i) > 127) { 1345 return true; 1346 } 1347 } 1348 1349 return false; 1350 } 1351 1352 1353 /** 1354 * Append a FetchProfile information to an IMAPCommand 1355 * that's to be issued. 1356 * 1357 * @param profile The fetch profile we're using. 1358 * 1359 * @exception MessagingException 1360 */ 1361 public void appendFetchProfile(FetchProfile profile) throws MessagingException { 1362 // the fetch profile items are a parenthtical list passed on a 1363 // FETCH command. 1364 startList(); 1365 if (profile.contains(UIDFolder.FetchProfileItem.UID)) { 1366 appendAtom("UID"); 1367 } 1368 if (profile.contains(FetchProfile.Item.ENVELOPE)) { 1369 // fetching the envelope involves several items 1370 appendAtom("ENVELOPE"); 1371 appendAtom("INTERNALDATE"); 1372 appendAtom("RFC822.SIZE"); 1373 } 1374 if (profile.contains(FetchProfile.Item.FLAGS)) { 1375 appendAtom("FLAGS"); 1376 } 1377 if (profile.contains(FetchProfile.Item.CONTENT_INFO)) { 1378 appendAtom("BODYSTRUCTURE"); 1379 } 1380 if (profile.contains(IMAPFolder.FetchProfileItem.SIZE)) { 1381 appendAtom("RFC822.SIZE"); 1382 } 1383 // There are two choices here, that are sort of redundant. 1384 // if all headers have been requested, there's no point in 1385 // adding any specifically requested one. 1386 if (profile.contains(IMAPFolder.FetchProfileItem.HEADERS)) { 1387 appendAtom("BODY.PEEK[HEADER]"); 1388 } 1389 else { 1390 String[] headers = profile.getHeaderNames(); 1391 // have an actual list to retrieve? need to craft this as a sublist 1392 // of identified fields. 1393 if (headers.length > 0) { 1394 appendAtom("BODY.PEEK[HEADER.FIELDS]"); 1395 startList(); 1396 for (int i = 0; i < headers.length; i++) { 1397 appendAtom(headers[i]); 1398 } 1399 endList(); 1400 } 1401 } 1402 // end the list. 1403 endList(); 1404 } 1405 1406 1407 /** 1408 * Append an ACL value to a command. The ACL is the writes string name, 1409 * followed by the rights value. This version uses no +/- modifier. 1410 * 1411 * @param acl The ACL to append. 1412 */ 1413 public void appendACL(ACL acl) { 1414 appendACL(acl, null); 1415 } 1416 1417 /** 1418 * Append an ACL value to a command. The ACL is the writes string name, 1419 * followed by the rights value. A +/- modifier can be added to the 1420 * // result. 1421 * 1422 * @param acl The ACL to append. 1423 * @param modifier The modifer string (can be null). 1424 */ 1425 public void appendACL(ACL acl, String modifier) { 1426 appendString(acl.getName()); 1427 String rights = acl.getRights().toString(); 1428 1429 if (modifier != null) { 1430 rights = modifier + rights; 1431 } 1432 appendString(rights); 1433 } 1434 1435 1436 /** 1437 * Append a quota specification to an IMAP command. 1438 * 1439 * @param quota The quota value to append. 1440 */ 1441 public void appendQuota(Quota quota) { 1442 appendString(quota.quotaRoot); 1443 startList(); 1444 for (int i = 0; i < quota.resources.length; i++) { 1445 appendQuotaResource(quota.resources[i]); 1446 } 1447 endList(); 1448 } 1449 1450 /** 1451 * Append a Quota.Resource element to an IMAP command. This converts as 1452 * the resoure name, the usage value and limit value). 1453 * 1454 * @param resource The resource element we're appending. 1455 */ 1456 public void appendQuotaResource(Quota.Resource resource) { 1457 appendAtom(resource.name); 1458 // NB: For command purposes, only the limit is used. 1459 appendLong(resource.limit); 1460 } 1461 } 1462