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