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