View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.geronimo.javamail.store.imap.connection;
19  
20  import java.io.ByteArrayOutputStream;
21  import java.io.DataOutputStream;
22  import java.io.IOException;
23  import java.io.OutputStream;
24  import java.io.UnsupportedEncodingException;
25  import java.text.SimpleDateFormat;
26  import java.util.ArrayList;
27  import java.util.Date;
28  import java.util.List;
29  import java.util.Vector;
30  
31  import javax.mail.FetchProfile; 
32  import javax.mail.Flags;
33  import javax.mail.Message;
34  import javax.mail.MessagingException;
35  import javax.mail.Quota;
36  import javax.mail.UIDFolder;
37  
38  import javax.mail.search.AddressTerm;
39  import javax.mail.search.AndTerm;
40  import javax.mail.search.BodyTerm;
41  import javax.mail.search.ComparisonTerm;
42  import javax.mail.search.DateTerm;
43  import javax.mail.search.FlagTerm;
44  import javax.mail.search.FromTerm;
45  import javax.mail.search.FromStringTerm;
46  import javax.mail.search.HeaderTerm;
47  import javax.mail.search.MessageIDTerm;
48  import javax.mail.search.MessageNumberTerm;
49  import javax.mail.search.NotTerm;
50  import javax.mail.search.OrTerm;
51  import javax.mail.search.ReceivedDateTerm;
52  import javax.mail.search.RecipientTerm;
53  import javax.mail.search.RecipientStringTerm;
54  import javax.mail.search.SearchException;
55  import javax.mail.search.SearchTerm;
56  import javax.mail.search.SentDateTerm;
57  import javax.mail.search.SizeTerm;
58  import javax.mail.search.StringTerm;
59  import javax.mail.search.SubjectTerm;
60  
61  import org.apache.geronimo.javamail.store.imap.ACL; 
62  import org.apache.geronimo.javamail.store.imap.IMAPFolder;
63  import org.apache.geronimo.javamail.store.imap.Rights; 
64  import org.apache.geronimo.javamail.store.imap.connection.IMAPResponseTokenizer.Token; 
65  
66  import org.apache.geronimo.javamail.util.CommandFailedException;
67  
68  
69  /**
70   * Utility class for building up what might be complex arguments
71   * to a command.  This includes the ability to directly write out
72   * binary arrays of data and have them constructed as IMAP
73   * literals.
74   */
75  public class IMAPCommand {
76  
77      // digits table for encoding IMAP modified Base64.  Note that this differs
78      // from "normal" base 64 by using ',' instead of '/' for the last digit.
79      public static final char[] encodingTable = {
80          'A', 'B', 'C', 'D', 'E', 'F', 'G',
81          'H', 'I', 'J', 'K', 'L', 'M', 'N',
82          'O', 'P', 'Q', 'R', 'S', 'T', 'U',
83          'V', 'W', 'X', 'Y', 'Z',
84          'a', 'b', 'c', 'd', 'e', 'f', 'g',
85          'h', 'i', 'j', 'k', 'l', 'm', 'n',
86          'o', 'p', 'q', 'r', 's', 't', 'u',
87          'v', 'w', 'x', 'y', 'z',
88          '0', '1', '2', '3', '4', '5', '6',
89          '7', '8', '9',
90          '+', ','
91      };
92      
93      protected boolean needWhiteSpace = false;
94  
95      // our utility writer stream
96      protected DataOutputStream out;
97      // the real output target
98      protected ByteArrayOutputStream sink;
99      // 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