| /** |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.geronimo.javamail.store.imap.connection; |
| |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataOutputStream; |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.io.UnsupportedEncodingException; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Vector; |
| |
| import javax.mail.FetchProfile; |
| import javax.mail.Flags; |
| import javax.mail.Message; |
| import javax.mail.MessagingException; |
| import javax.mail.Quota; |
| import javax.mail.UIDFolder; |
| |
| import javax.mail.search.AddressTerm; |
| import javax.mail.search.AndTerm; |
| import javax.mail.search.BodyTerm; |
| import javax.mail.search.ComparisonTerm; |
| import javax.mail.search.DateTerm; |
| import javax.mail.search.FlagTerm; |
| import javax.mail.search.FromTerm; |
| import javax.mail.search.FromStringTerm; |
| import javax.mail.search.HeaderTerm; |
| import javax.mail.search.MessageIDTerm; |
| import javax.mail.search.MessageNumberTerm; |
| import javax.mail.search.NotTerm; |
| import javax.mail.search.OrTerm; |
| import javax.mail.search.ReceivedDateTerm; |
| import javax.mail.search.RecipientTerm; |
| import javax.mail.search.RecipientStringTerm; |
| import javax.mail.search.SearchException; |
| import javax.mail.search.SearchTerm; |
| import javax.mail.search.SentDateTerm; |
| import javax.mail.search.SizeTerm; |
| import javax.mail.search.StringTerm; |
| import javax.mail.search.SubjectTerm; |
| |
| import org.apache.geronimo.javamail.store.imap.ACL; |
| import org.apache.geronimo.javamail.store.imap.IMAPFolder; |
| import org.apache.geronimo.javamail.store.imap.Rights; |
| import org.apache.geronimo.javamail.store.imap.connection.IMAPResponseTokenizer.Token; |
| |
| import org.apache.geronimo.javamail.util.CommandFailedException; |
| |
| |
| /** |
| * Utility class for building up what might be complex arguments |
| * to a command. This includes the ability to directly write out |
| * binary arrays of data and have them constructed as IMAP |
| * literals. |
| */ |
| public class IMAPCommand { |
| |
| // digits table for encoding IMAP modified Base64. Note that this differs |
| // from "normal" base 64 by using ',' instead of '/' for the last digit. |
| public static final char[] encodingTable = { |
| 'A', 'B', 'C', 'D', 'E', 'F', 'G', |
| 'H', 'I', 'J', 'K', 'L', 'M', 'N', |
| 'O', 'P', 'Q', 'R', 'S', 'T', 'U', |
| 'V', 'W', 'X', 'Y', 'Z', |
| 'a', 'b', 'c', 'd', 'e', 'f', 'g', |
| 'h', 'i', 'j', 'k', 'l', 'm', 'n', |
| 'o', 'p', 'q', 'r', 's', 't', 'u', |
| 'v', 'w', 'x', 'y', 'z', |
| '0', '1', '2', '3', '4', '5', '6', |
| '7', '8', '9', |
| '+', ',' |
| }; |
| |
| protected boolean needWhiteSpace = false; |
| |
| // our utility writer stream |
| protected DataOutputStream out; |
| // the real output target |
| protected ByteArrayOutputStream sink; |
| // our command segment set. If the command contains literals, then the literal |
| // data must be sent after receiving an continue response back from the server. |
| protected List segments = null; |
| // the append tag for the response |
| protected String tag; |
| |
| // our counter used to generate command tags. |
| static protected int tagCounter = 0; |
| |
| /** |
| * Create an empty command. |
| */ |
| public IMAPCommand() { |
| try { |
| sink = new ByteArrayOutputStream(); |
| out = new DataOutputStream(sink); |
| |
| // write the tag data at the beginning of the command. |
| out.writeBytes(getTag()); |
| // need a blank separator |
| out.write(' '); |
| } catch (IOException e ) { |
| } |
| } |
| |
| /** |
| * Create a command with an initial command string. |
| * |
| * @param command The command string used to start this command. |
| */ |
| public IMAPCommand(String command) { |
| this(); |
| append(command); |
| } |
| |
| public String getTag() { |
| if (tag == null) { |
| // the tag needs to be non-numeric, so tack a convenient alpha character on the front. |
| tag = "a" + tagCounter++; |
| } |
| return tag; |
| } |
| |
| |
| /** |
| * Save the current segment of the command we've accumulated. This |
| * generally occurs because we have a literal element in the command |
| * that's going to require a continuation response from the server before |
| * we can send it. |
| */ |
| private void saveCurrentSegment() |
| { |
| try { |
| out.flush(); // make sure everything is written |
| // get the data so far and reset the sink |
| byte[] segment = sink.toByteArray(); |
| sink.reset(); |
| // most commands don't have segments, so don't create the list until we do. |
| if (segments == null) { |
| segments = new ArrayList(); |
| } |
| // ok, we need to issue this command as a conversation. |
| segments.add(segment); |
| } catch (IOException e) { |
| } |
| } |
| |
| |
| /** |
| * Write all of the command data to the stream. This includes the |
| * leading tag data. |
| * |
| * @param outStream |
| * @param connection |
| * |
| * @exception IOException |
| * @exception MessagingException |
| */ |
| public void writeTo(OutputStream outStream, IMAPConnection connection) throws IOException, MessagingException |
| { |
| |
| // just a simple, single string-encoded command? |
| if (segments == null) { |
| // make sure the output stream is flushed |
| out.flush(); |
| // just copy the command data to the output stream |
| sink.writeTo(outStream); |
| // we need to end the command with a CRLF sequence. |
| outStream.write('\r'); |
| outStream.write('\n'); |
| } |
| // multiple-segment mode, which means we need to deal with continuation responses at |
| // each of the literal boundaries. |
| else { |
| // at this point, we have a list of command pieces that must be written out, then a |
| // continuation response checked for after each write. Once each of these pieces is |
| // written out, we still have command stuff pending in the out stream, which we'll tack |
| // on to the end. |
| for (int i = 0; i < segments.size(); i++) { |
| outStream.write((byte [])segments.get(i)); |
| // now wait for a response from the connection. We should be getting a |
| // continuation response back (and might have also received some asynchronous |
| // replies, which we'll leave in the queue for now. If we get some status back |
| // other than than a continue, we've got an error in our command somewhere. |
| IMAPTaggedResponse response = connection.receiveResponse(); |
| if (!response.isContinuation()) { |
| throw new CommandFailedException("Error response received on a IMAP continued command: " + response); |
| } |
| } |
| out.flush(); |
| // all leading segments written with the appropriate continuation received in reply. |
| // just copy the command data to the output stream |
| sink.writeTo(outStream); |
| // we need to end the command with a CRLF sequence. |
| outStream.write('\r'); |
| outStream.write('\n'); |
| } |
| } |
| |
| |
| /** |
| * Directly append a value to the buffer without attempting |
| * to insert whitespace or figure out any format encodings. |
| * |
| * @param value The value to append. |
| */ |
| public void append(String value) { |
| try { |
| // add the bytes direcly |
| out.writeBytes(value); |
| // assume we're needing whitespace after this (pretty much unknown). |
| needWhiteSpace = true; |
| } catch (IOException e) { |
| } |
| } |
| |
| |
| /** |
| * Append a string value to a command buffer. This sorts out |
| * what form the string needs to be appended in (LITERAL, QUOTEDSTRING, |
| * or ATOM). |
| * |
| * @param target The target buffer for appending the string. |
| * @param value The value to append. |
| */ |
| public void appendString(String value) { |
| try { |
| // work off the byte values |
| appendString(value.getBytes("ISO8859-1")); |
| } catch (UnsupportedEncodingException e) { |
| } |
| } |
| |
| |
| /** |
| * Append a string value to a command buffer. This always appends as |
| * a QUOTEDSTRING |
| * |
| * @param value The value to append. |
| */ |
| public void appendQuotedString(String value) { |
| try { |
| // work off the byte values |
| appendQuotedString(value.getBytes("ISO8859-1")); |
| } catch (UnsupportedEncodingException e) { |
| } |
| } |
| |
| |
| /** |
| * Append a string value to a command buffer, with encoding. This sorts out |
| * what form the string needs to be appended in (LITERAL, QUOTEDSTRING, |
| * or ATOM). |
| * |
| * @param target The target buffer for appending the string. |
| * @param value The value to append. |
| */ |
| public void appendEncodedString(String value) { |
| // encode first. |
| value = encode(value); |
| try { |
| // work off the byte values |
| appendString(value.getBytes("ISO8859-1")); |
| } catch (UnsupportedEncodingException e) { |
| } |
| } |
| |
| |
| /** |
| * Encode a string using the modified UTF-7 encoding. |
| * |
| * @param original The original string. |
| * |
| * @return The original string encoded with modified UTF-7 encoding. |
| */ |
| public String encode(String original) { |
| |
| // buffer for encoding sections of data |
| byte[] buffer = new byte[4]; |
| int bufferCount = 0; |
| |
| StringBuffer result = new StringBuffer(); |
| |
| // state flag for the type of section we're in. |
| boolean encoding = false; |
| |
| for (int i = 0; i < original.length(); i++) { |
| char ch = original.charAt(i); |
| |
| // processing an encoded section? |
| if (encoding) { |
| // is this a printable character? |
| if (ch > 31 && ch < 127) { |
| // encode anything in the buffer |
| encode(buffer, bufferCount, result); |
| // add the section terminator char |
| result.append('-'); |
| encoding = false; |
| // we now fall through to the printable character section. |
| } |
| // still an unprintable |
| else { |
| // add this char to the working buffer? |
| buffer[++bufferCount] = (byte)(ch >> 8); |
| buffer[++bufferCount] = (byte)(ch & 0xff); |
| // if we have enough to encode something, do it now. |
| if (bufferCount >= 3) { |
| bufferCount = encode(buffer, bufferCount, result); |
| } |
| // go back to the top of the loop. |
| continue; |
| } |
| } |
| // is this the special printable? |
| if (ch == '&') { |
| // this is the special null escape sequence |
| result.append('&'); |
| result.append('-'); |
| } |
| // is this a printable character? |
| else if (ch > 31 && ch < 127) { |
| // just add to the result |
| result.append(ch); |
| } |
| else { |
| // write the escape character |
| result.append('&'); |
| |
| // non-printable ASCII character, we need to switch modes |
| // both bytes of this character need to be encoded. Each |
| // encoded digit will basically be a "character-and-a-half". |
| buffer[0] = (byte)(ch >> 8); |
| buffer[1] = (byte)(ch & 0xff); |
| bufferCount = 2; |
| encoding = true; |
| } |
| } |
| // were we in a non-printable section at the end? |
| if (encoding) { |
| // take care of any remaining characters |
| encode(buffer, bufferCount, result); |
| // add the section terminator char |
| result.append('-'); |
| } |
| // convert the encoded string. |
| return result.toString(); |
| } |
| |
| |
| /** |
| * Encode a single buffer of characters. This buffer will have |
| * between 0 and 4 bytes to encode. |
| * |
| * @param buffer The buffer to encode. |
| * @param count The number of characters in the buffer. |
| * @param result The accumulator for appending the result. |
| * |
| * @return The remaining number of bytes remaining in the buffer (return 0 |
| * unless the count was 4 at the beginning). |
| */ |
| protected static int encode(byte[] buffer, int count, StringBuffer result) { |
| byte b1 = 0; |
| byte b2 = 0; |
| byte b3 = 0; |
| |
| // different processing based on how much we have in the buffer |
| switch (count) { |
| // ended at a boundary. This is cool, not much to do. |
| case 0: |
| // no residual in the buffer |
| return 0; |
| |
| // just a single left over byte from the last encoding op. |
| case 1: |
| b1 = buffer[0]; |
| result.append(encodingTable[(b1 >>> 2) & 0x3f]); |
| result.append(encodingTable[(b1 << 4) & 0x30]); |
| return 0; |
| |
| // one complete char to encode |
| case 2: |
| b1 = buffer[0]; |
| b2 = buffer[1]; |
| result.append(encodingTable[(b1 >>> 2) & 0x3f]); |
| result.append(encodingTable[((b1 << 4) & 0x30) + ((b2 >>>4) & 0x0f)]); |
| result.append(encodingTable[((b2 << 2) & (0x3c))]); |
| return 0; |
| |
| // at least a full triplet of bytes to encode |
| case 3: |
| case 4: |
| b1 = buffer[0]; |
| b2 = buffer[1]; |
| b3 = buffer[2]; |
| result.append(encodingTable[(b1 >>> 2) & 0x3f]); |
| result.append(encodingTable[((b1 << 4) & 0x30) + ((b2 >>>4) & 0x0f)]); |
| result.append(encodingTable[((b2 << 2) & 0x3c) + ((b3 >>> 6) & 0x03)]); |
| result.append(encodingTable[b3 & 0x3f]); |
| |
| // if we have more than the triplet, we need to move the extra one into the first |
| // position and return the residual indicator |
| if (count == 4) { |
| buffer[0] = buffer[4]; |
| return 1; |
| } |
| return 0; |
| } |
| return 0; |
| } |
| |
| |
| /** |
| * Append a string value to a command buffer. This sorts out |
| * what form the string needs to be appended in (LITERAL, QUOTEDSTRING, |
| * or ATOM). |
| * |
| * @param target The target buffer for appending the string. |
| * @param value The value to append. |
| */ |
| public void appendString(String value, String charset) throws MessagingException { |
| if (charset == null) { |
| try { |
| // work off the byte values |
| appendString(value.getBytes("ISO8859-1")); |
| } catch (UnsupportedEncodingException e) { |
| } |
| } |
| else { |
| try { |
| // use the charset to extract the bytes |
| appendString(value.getBytes(charset)); |
| throw new MessagingException("Invalid text encoding"); |
| } catch (UnsupportedEncodingException e) { |
| } |
| } |
| } |
| |
| |
| /** |
| * Append a value in a byte array to a command buffer. This sorts out |
| * what form the string needs to be appended in (LITERAL, QUOTEDSTRING, |
| * or ATOM). |
| * |
| * @param target The target buffer for appending the string. |
| * @param value The value to append. |
| */ |
| public void appendString(byte[] value) { |
| // sort out how we need to append this |
| switch (IMAPResponseTokenizer.getEncoding(value)) { |
| case Token.LITERAL: |
| appendLiteral(value); |
| break; |
| case Token.QUOTEDSTRING: |
| appendQuotedString(value); |
| break; |
| case Token.ATOM: |
| appendAtom(value); |
| break; |
| } |
| } |
| |
| |
| /** |
| * Append an integer value to the command, converting |
| * the integer into string form. |
| * |
| * @param value The value to append. |
| */ |
| public void appendInteger(int value) { |
| appendAtom(Integer.toString(value)); |
| } |
| |
| |
| /** |
| * Append a long value to the command, converting |
| * the integer into string form. |
| * |
| * @param value The value to append. |
| */ |
| public void appendLong(long value) { |
| appendAtom(Long.toString(value)); |
| } |
| |
| |
| /** |
| * Append an atom value to the command. Atoms are directly |
| * appended without using literal encodings. |
| * |
| * @param value The value to append. |
| */ |
| public void appendAtom(String value) { |
| try { |
| appendAtom(value.getBytes("ISO8859-1")); |
| } catch (UnsupportedEncodingException e) { |
| } |
| } |
| |
| |
| |
| /** |
| * Append an atom to the command buffer. Atoms are directly |
| * appended without using literal encodings. White space is |
| * accounted for with the append operation. |
| * |
| * @param value The value to append. |
| */ |
| public void appendAtom(byte[] value) { |
| try { |
| // give a token separator |
| conditionalWhitespace(); |
| // ATOMs are easy |
| out.write(value); |
| } catch (IOException e) { |
| } |
| } |
| |
| |
| /** |
| * Append an IMAP literal values to the command. |
| * literals are written using a header with the length |
| * specified, followed by a CRLF sequence, followed |
| * by the literal data. |
| * |
| * @param value The literal data to write. |
| */ |
| public void appendLiteral(byte[] value) { |
| try { |
| appendLiteralHeader(value.length); |
| out.write(value); |
| } catch (IOException e) { |
| } |
| } |
| |
| /** |
| * Add a literal header to the buffer. The literal |
| * header is the literal length enclosed in a |
| * "{n}" pair, followed by a CRLF sequence. |
| * |
| * @param size The size of the literal value. |
| */ |
| protected void appendLiteralHeader(int size) { |
| try { |
| conditionalWhitespace(); |
| out.writeByte('{'); |
| out.writeBytes(Integer.toString(size)); |
| out.writeBytes("}\r\n"); |
| // the IMAP client is required to send literal data to the server by |
| // writing the command up to the header, then waiting for a continuation |
| // response to send the rest. |
| saveCurrentSegment(); |
| } catch (IOException e) { |
| } |
| } |
| |
| |
| /** |
| * Append literal data to the command where the |
| * literal sourcd is a ByteArrayOutputStream. |
| * |
| * @param value The source of the literal data. |
| */ |
| public void appendLiteral(ByteArrayOutputStream value) { |
| try { |
| appendLiteralHeader(value.size()); |
| // have this output stream write directly into our stream |
| value.writeTo(out); |
| } catch (IOException e) { |
| } |
| } |
| |
| /** |
| * Write out a string of literal data, taking into |
| * account the need to escape both '"' and '\' |
| * characters. |
| * |
| * @param value The bytes of the string to write. |
| */ |
| public void appendQuotedString(byte[] value) { |
| try { |
| conditionalWhitespace(); |
| out.writeByte('"'); |
| |
| // look for chars requiring escaping |
| for (int i = 0; i < value.length; i++) { |
| byte ch = value[i]; |
| |
| if (ch == '"' || ch == '\\') { |
| out.writeByte('\\'); |
| } |
| out.writeByte(ch); |
| } |
| |
| out.writeByte('"'); |
| } catch (IOException e) { |
| } |
| } |
| |
| /** |
| * Mark the start of a list value being written to |
| * the command. A list is a sequences of different |
| * tokens enclosed in "(" ")" pairs. Lists can |
| * be nested. |
| */ |
| public void startList() { |
| try { |
| conditionalWhitespace(); |
| out.writeByte('('); |
| needWhiteSpace = false; |
| } catch (IOException e) { |
| } |
| } |
| |
| /** |
| * Write out the end of the list. |
| */ |
| public void endList() { |
| try { |
| out.writeByte(')'); |
| needWhiteSpace = true; |
| } catch (IOException e) { |
| } |
| } |
| |
| |
| /** |
| * Add a whitespace character to the command if the |
| * previous token was a type that required a |
| * white space character to mark the boundary. |
| */ |
| protected void conditionalWhitespace() { |
| try { |
| if (needWhiteSpace) { |
| out.writeByte(' '); |
| } |
| // all callers of this are writing a token that will need white space following, so turn this on |
| // every time we're called. |
| needWhiteSpace = true; |
| } catch (IOException e) { |
| } |
| } |
| |
| |
| /** |
| * Append a body section specification to a command string. Body |
| * section specifications are of the form "[section]<start.count>". |
| * |
| * @param section The section numeric identifier. |
| * @param partName The name of the body section we want (e.g. "TEST", "HEADERS"). |
| */ |
| public void appendBodySection(String section, String partName) { |
| try { |
| // we sometimes get called from the top level |
| if (section == null) { |
| appendBodySection(partName); |
| return; |
| } |
| |
| out.writeByte('['); |
| out.writeBytes(section); |
| if (partName != null) { |
| out.writeByte('.'); |
| out.writeBytes(partName); |
| } |
| out.writeByte(']'); |
| needWhiteSpace = true; |
| } catch (IOException e) { |
| } |
| } |
| |
| |
| /** |
| * Append a body section specification to a command string. Body |
| * section specifications are of the form "[section]". |
| * |
| * @param partName The partname we require. |
| */ |
| public void appendBodySection(String partName) { |
| try { |
| out.writeByte('['); |
| out.writeBytes(partName); |
| out.writeByte(']'); |
| needWhiteSpace = true; |
| } catch (IOException e) { |
| } |
| } |
| |
| |
| /** |
| * Append a set of flags to a command buffer. |
| * |
| * @param flags The flag set to append. |
| */ |
| public void appendFlags(Flags flags) { |
| startList(); |
| |
| Flags.Flag[] systemFlags = flags.getSystemFlags(); |
| |
| // process each of the system flag names |
| for (int i = 0; i < systemFlags.length; i++) { |
| Flags.Flag flag = systemFlags[i]; |
| |
| if (flag == Flags.Flag.ANSWERED) { |
| appendAtom("\\Answered"); |
| } |
| else if (flag == Flags.Flag.DELETED) { |
| appendAtom("\\Deleted"); |
| } |
| else if (flag == Flags.Flag.DRAFT) { |
| appendAtom("\\Draft"); |
| } |
| else if (flag == Flags.Flag.FLAGGED) { |
| appendAtom("\\Flagged"); |
| } |
| else if (flag == Flags.Flag.RECENT) { |
| appendAtom("\\Recent"); |
| } |
| else if (flag == Flags.Flag.SEEN) { |
| appendAtom("\\Seen"); |
| } |
| } |
| |
| // now process the user flags, which just get appended as is. |
| String[] userFlags = flags.getUserFlags(); |
| |
| for (int i = 0; i < userFlags.length; i++) { |
| appendAtom(userFlags[i]); |
| } |
| |
| // close the list off |
| endList(); |
| } |
| |
| |
| /** |
| * Format a date into the form required for IMAP commands. |
| * |
| * @param d The source Date. |
| */ |
| public void appendDate(Date d) { |
| // get a formatter to create IMAP dates. Use the US locale, as the dates are not localized. |
| IMAPDateFormat formatter = new IMAPDateFormat(); |
| // date_time strings need to be done as quoted strings because they contain blanks. |
| appendString(formatter.format(d)); |
| } |
| |
| |
| /** |
| * Format a date into the form required for IMAP search commands. |
| * |
| * @param d The source Date. |
| */ |
| public void appendSearchDate(Date d) { |
| // get a formatter to create IMAP dates. Use the US locale, as the dates are not localized. |
| IMAPSearchDateFormat formatter = new IMAPSearchDateFormat(); |
| // date_time strings need to be done as quoted strings because they contain blanks. |
| appendString(formatter.format(d)); |
| } |
| |
| |
| /** |
| * append an IMAP search sequence from a SearchTerm. SearchTerms |
| * terms can be complex sets of terms in a tree form, so this |
| * may involve some recursion to completely translate. |
| * |
| * @param term The search term we're processing. |
| * @param charset The charset we need to use when generating the sequence. |
| * |
| * @exception MessagingException |
| */ |
| public void appendSearchTerm(SearchTerm term, String charset) throws MessagingException { |
| // we need to do this manually, by inspecting the term object against the various SearchTerm types |
| // defined by the javamail spec. |
| |
| // Flag searches are used internally by other operations, so this is a good one to check first. |
| if (term instanceof FlagTerm) { |
| appendFlag((FlagTerm)term, charset); |
| } |
| // after that, I'm not sure there's any optimal order to these. Let's start with the conditional |
| // modifiers (AND, OR, NOT), then just hit each of the header types |
| else if (term instanceof AndTerm) { |
| appendAnd((AndTerm)term, charset); |
| } |
| else if (term instanceof OrTerm) { |
| appendOr((OrTerm)term, charset); |
| } |
| else if (term instanceof NotTerm) { |
| appendNot((NotTerm)term, charset); |
| } |
| // multiple forms of From: search |
| else if (term instanceof FromTerm) { |
| appendFrom((FromTerm)term, charset); |
| } |
| else if (term instanceof FromStringTerm) { |
| appendFrom((FromStringTerm)term, charset); |
| } |
| else if (term instanceof HeaderTerm) { |
| appendHeader((HeaderTerm)term, charset); |
| } |
| else if (term instanceof RecipientTerm) { |
| appendRecipient((RecipientTerm)term, charset); |
| } |
| else if (term instanceof RecipientStringTerm) { |
| appendRecipient((RecipientStringTerm)term, charset); |
| } |
| else if (term instanceof SubjectTerm) { |
| appendSubject((SubjectTerm)term, charset); |
| } |
| else if (term instanceof BodyTerm) { |
| appendBody((BodyTerm)term, charset); |
| } |
| else if (term instanceof SizeTerm) { |
| appendSize((SizeTerm)term, charset); |
| } |
| else if (term instanceof SentDateTerm) { |
| appendSentDate((SentDateTerm)term, charset); |
| } |
| else if (term instanceof ReceivedDateTerm) { |
| appendReceivedDate((ReceivedDateTerm)term, charset); |
| } |
| else if (term instanceof MessageIDTerm) { |
| appendMessageID((MessageIDTerm)term, charset); |
| } |
| else { |
| // don't know what this is |
| throw new SearchException("Unsupported search type"); |
| } |
| } |
| |
| /** |
| * append IMAP search term information from a FlagTerm item. |
| * |
| * @param term The source FlagTerm |
| * @param charset target charset for the search information (can be null). |
| * @param out The target command buffer. |
| */ |
| protected void appendFlag(FlagTerm term, String charset) { |
| // decide which one we need to test for |
| boolean set = term.getTestSet(); |
| |
| Flags flags = term.getFlags(); |
| Flags.Flag[] systemFlags = flags.getSystemFlags(); |
| |
| String[] userFlags = flags.getUserFlags(); |
| |
| // empty search term? not sure if this is an error. The default search implementation would |
| // not consider this an error, so we'll just ignore this. |
| if (systemFlags.length == 0 && userFlags.length == 0) { |
| return; |
| } |
| |
| if (set) { |
| for (int i = 0; i < systemFlags.length; i++) { |
| Flags.Flag flag = systemFlags[i]; |
| |
| if (flag == Flags.Flag.ANSWERED) { |
| appendAtom("ANSWERED"); |
| } |
| else if (flag == Flags.Flag.DELETED) { |
| appendAtom("DELETED"); |
| } |
| else if (flag == Flags.Flag.DRAFT) { |
| appendAtom("DRAFT"); |
| } |
| else if (flag == Flags.Flag.FLAGGED) { |
| appendAtom("FLAGGED"); |
| } |
| else if (flag == Flags.Flag.RECENT) { |
| appendAtom("RECENT"); |
| } |
| else if (flag == Flags.Flag.SEEN) { |
| appendAtom("SEEN"); |
| } |
| } |
| } |
| else { |
| for (int i = 0; i < systemFlags.length; i++) { |
| Flags.Flag flag = systemFlags[i]; |
| |
| if (flag == Flags.Flag.ANSWERED) { |
| appendAtom("UNANSWERED"); |
| } |
| else if (flag == Flags.Flag.DELETED) { |
| appendAtom("UNDELETED"); |
| } |
| else if (flag == Flags.Flag.DRAFT) { |
| appendAtom("UNDRAFT"); |
| } |
| else if (flag == Flags.Flag.FLAGGED) { |
| appendAtom("UNFLAGGED"); |
| } |
| else if (flag == Flags.Flag.RECENT) { |
| // not UNRECENT? |
| appendAtom("OLD"); |
| } |
| else if (flag == Flags.Flag.SEEN) { |
| appendAtom("UNSEEN"); |
| } |
| } |
| } |
| |
| |
| // User flags are done as either "KEYWORD name" or "UNKEYWORD name" |
| for (int i = 0; i < userFlags.length; i++) { |
| appendAtom(set ? "KEYWORD" : "UNKEYWORD"); |
| appendAtom(userFlags[i]); |
| } |
| } |
| |
| |
| /** |
| * append IMAP search term information from an AndTerm item. |
| * |
| * @param term The source AndTerm |
| * @param charset target charset for the search information (can be null). |
| * @param out The target command buffer. |
| */ |
| protected void appendAnd(AndTerm term, String charset) throws MessagingException { |
| // ANDs are pretty easy. Just append all of the terms directly to the |
| // command as is. |
| |
| SearchTerm[] terms = term.getTerms(); |
| |
| for (int i = 0; i < terms.length; i++) { |
| appendSearchTerm(terms[i], charset); |
| } |
| } |
| |
| |
| /** |
| * append IMAP search term information from an OrTerm item. |
| * |
| * @param term The source OrTerm |
| * @param charset target charset for the search information (can be null). |
| * @param out The target command buffer. |
| */ |
| protected void appendOr(OrTerm term, String charset) throws MessagingException { |
| SearchTerm[] terms = term.getTerms(); |
| |
| // OrTerms are a bit of a pain to translate to IMAP semantics. The IMAP OR operation only allows 2 |
| // search keys, while OrTerms can have n keys (including, it appears, just one! If we have more than |
| // 2, it's easiest to convert this into a tree of OR keys and let things generate that way. The |
| // resulting IMAP operation would be OR (key1) (OR (key2) (key3)) |
| |
| // silly rabbit...somebody doesn't know how to use OR |
| if (terms.length == 1) { |
| // just append the singleton in place without the OR operation. |
| appendSearchTerm(terms[0], charset); |
| return; |
| } |
| |
| // is this a more complex operation? |
| if (terms.length > 2) { |
| // have to chain these together (shazbat). |
| SearchTerm current = terms[0]; |
| |
| for (int i = 1; i < terms.length; i++) { |
| current = new OrTerm(current, terms[i]); |
| } |
| |
| // replace the term array with the newly generated top array |
| terms = ((OrTerm)current).getTerms(); |
| } |
| |
| // we're going to generate this with parenthetical search keys, even if it is just a simple term. |
| appendAtom("OR"); |
| startList(); |
| // generated OR argument 1 |
| appendSearchTerm(terms[0], charset); |
| endList(); |
| startList(); |
| // generated OR argument 2 |
| appendSearchTerm(terms[0], charset); |
| // and the closing parens |
| endList(); |
| } |
| |
| |
| /** |
| * append IMAP search term information from a NotTerm item. |
| * |
| * @param term The source NotTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendNot(NotTerm term, String charset) throws MessagingException { |
| // we're goint to generate this with parenthetical search keys, even if it is just a simple term. |
| appendAtom("NOT"); |
| startList(); |
| // generated the NOT expression |
| appendSearchTerm(term.getTerm(), charset); |
| // and the closing parens |
| endList(); |
| } |
| |
| |
| /** |
| * append IMAP search term information from a FromTerm item. |
| * |
| * @param term The source FromTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendFrom(FromTerm term, String charset) throws MessagingException { |
| appendAtom("FROM"); |
| // this may require encoding |
| appendString(term.getAddress().toString(), charset); |
| } |
| |
| |
| /** |
| * append IMAP search term information from a FromStringTerm item. |
| * |
| * @param term The source FromStringTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendFrom(FromStringTerm term, String charset) throws MessagingException { |
| appendAtom("FROM"); |
| // this may require encoding |
| appendString(term.getPattern(), charset); |
| } |
| |
| |
| /** |
| * append IMAP search term information from a RecipientTerm item. |
| * |
| * @param term The source RecipientTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendRecipient(RecipientTerm term, String charset) throws MessagingException { |
| appendAtom(recipientType(term.getRecipientType())); |
| // this may require encoding |
| appendString(term.getAddress().toString(), charset); |
| } |
| |
| |
| /** |
| * append IMAP search term information from a RecipientStringTerm item. |
| * |
| * @param term The source RecipientStringTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendRecipient(RecipientStringTerm term, String charset) throws MessagingException { |
| appendAtom(recipientType(term.getRecipientType())); |
| // this may require encoding |
| appendString(term.getPattern(), charset); |
| } |
| |
| |
| /** |
| * Translate a recipient type into it's string name equivalent. |
| * |
| * @param type The source recipient type |
| * |
| * @return A string name matching the recipient type. |
| */ |
| protected String recipientType(Message.RecipientType type) throws MessagingException { |
| if (type == Message.RecipientType.TO) { |
| return "TO"; |
| } |
| if (type == Message.RecipientType.CC) { |
| return "CC"; |
| } |
| if (type == Message.RecipientType.BCC) { |
| return "BCC"; |
| } |
| |
| throw new SearchException("Unsupported RecipientType"); |
| } |
| |
| |
| /** |
| * append IMAP search term information from a HeaderTerm item. |
| * |
| * @param term The source HeaderTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendHeader(HeaderTerm term, String charset) throws MessagingException { |
| appendAtom("HEADER"); |
| appendString(term.getHeaderName()); |
| appendString(term.getPattern(), charset); |
| } |
| |
| |
| |
| /** |
| * append IMAP search term information from a SubjectTerm item. |
| * |
| * @param term The source SubjectTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendSubject(SubjectTerm term, String charset) throws MessagingException { |
| appendAtom("SUBJECT"); |
| appendString(term.getPattern(), charset); |
| } |
| |
| |
| /** |
| * append IMAP search term information from a BodyTerm item. |
| * |
| * @param term The source BodyTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendBody(BodyTerm term, String charset) throws MessagingException { |
| appendAtom("BODY"); |
| appendString(term.getPattern(), charset); |
| } |
| |
| |
| /** |
| * append IMAP search term information from a SizeTerm item. |
| * |
| * @param term The source SizeTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendSize(SizeTerm term, String charset) throws MessagingException { |
| |
| // these comparisons can be a real pain. IMAP only supports LARGER and SMALLER. So comparisons |
| // other than GT and LT have to be composed of complex sequences of these. For example, an EQ |
| // comparison becomes NOT LARGER size NOT SMALLER size |
| |
| if (term.getComparison() == ComparisonTerm.GT) { |
| appendAtom("LARGER"); |
| appendInteger(term.getNumber()); |
| } |
| else if (term.getComparison() == ComparisonTerm.LT) { |
| appendAtom("SMALLER"); |
| appendInteger(term.getNumber()); |
| } |
| else if (term.getComparison() == ComparisonTerm.EQ) { |
| appendAtom("NOT"); |
| appendAtom("LARGER"); |
| appendInteger(term.getNumber()); |
| |
| appendAtom("NOT"); |
| appendAtom("SMALLER"); |
| // it's just right <g> |
| appendInteger(term.getNumber()); |
| } |
| else if (term.getComparison() == ComparisonTerm.NE) { |
| // this needs to be an OR comparison |
| appendAtom("OR"); |
| appendAtom("LARGER"); |
| appendInteger(term.getNumber()); |
| |
| appendAtom("SMALLER"); |
| appendInteger(term.getNumber()); |
| } |
| else if (term.getComparison() == ComparisonTerm.LE) { |
| // just the inverse of LARGER |
| appendAtom("NOT"); |
| appendAtom("LARGER"); |
| appendInteger(term.getNumber()); |
| } |
| else if (term.getComparison() == ComparisonTerm.GE) { |
| // and the reverse. |
| appendAtom("NOT"); |
| appendAtom("SMALLER"); |
| appendInteger(term.getNumber()); |
| } |
| } |
| |
| |
| /** |
| * append IMAP search term information from a MessageIDTerm item. |
| * |
| * @param term The source MessageIDTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendMessageID(MessageIDTerm term, String charset) throws MessagingException { |
| |
| // not directly supported by IMAP, but we can compare on the header information. |
| appendAtom("HEADER"); |
| appendString("Message-ID"); |
| appendString(term.getPattern(), charset); |
| } |
| |
| |
| /** |
| * append IMAP search term information from a SendDateTerm item. |
| * |
| * @param term The source SendDateTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendSentDate(SentDateTerm term, String charset) throws MessagingException { |
| Date date = term.getDate(); |
| |
| switch (term.getComparison()) { |
| case ComparisonTerm.EQ: |
| appendAtom("SENTON"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.LT: |
| appendAtom("SENTBEFORE"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.GT: |
| appendAtom("SENTSINCE"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.GE: |
| appendAtom("OR"); |
| appendAtom("SENTSINCE"); |
| appendSearchDate(date); |
| appendAtom("SENTON"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.LE: |
| appendAtom("OR"); |
| appendAtom("SENTBEFORE"); |
| appendSearchDate(date); |
| appendAtom("SENTON"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.NE: |
| appendAtom("NOT"); |
| appendAtom("SENTON"); |
| appendSearchDate(date); |
| break; |
| default: |
| throw new SearchException("Unsupported date comparison type"); |
| } |
| } |
| |
| |
| /** |
| * append IMAP search term information from a ReceivedDateTerm item. |
| * |
| * @param term The source ReceivedDateTerm |
| * @param charset target charset for the search information (can be null). |
| */ |
| protected void appendReceivedDate(ReceivedDateTerm term, String charset) throws MessagingException { |
| Date date = term.getDate(); |
| |
| switch (term.getComparison()) { |
| case ComparisonTerm.EQ: |
| appendAtom("ON"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.LT: |
| appendAtom("BEFORE"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.GT: |
| appendAtom("SINCE"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.GE: |
| appendAtom("OR"); |
| appendAtom("SINCE"); |
| appendSearchDate(date); |
| appendAtom("ON"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.LE: |
| appendAtom("OR"); |
| appendAtom("BEFORE"); |
| appendSearchDate(date); |
| appendAtom("ON"); |
| appendSearchDate(date); |
| break; |
| case ComparisonTerm.NE: |
| appendAtom("NOT"); |
| appendAtom("ON"); |
| appendSearchDate(date); |
| break; |
| default: |
| throw new SearchException("Unsupported date comparison type"); |
| } |
| } |
| |
| |
| /** |
| * Run the tree of search terms, checking for problems with |
| * the terms that may require specifying a CHARSET modifier |
| * on a SEARCH command sent to the server. |
| * |
| * @param term The term to check. |
| * |
| * @return True if there are 7-bit problems, false if the terms contain |
| * only 7-bit ASCII characters. |
| */ |
| static public boolean checkSearchEncoding(SearchTerm term) { |
| // StringTerm is the basis of most of the string-valued terms, and are most important ones to check. |
| if (term instanceof StringTerm) { |
| return checkStringEncoding(((StringTerm)term).getPattern()); |
| } |
| // Address terms are basically string terms also, but we need to check the string value of the |
| // addresses, since that's what we're sending along. This covers a lot of the TO/FROM, etc. searches. |
| else if (term instanceof AddressTerm) { |
| return checkStringEncoding(((AddressTerm)term).getAddress().toString()); |
| } |
| // the NOT contains a term itself, so recurse on that. The NOT does not directly have string values |
| // to check. |
| else if (term instanceof NotTerm) { |
| return checkSearchEncoding(((NotTerm)term).getTerm()); |
| } |
| // AND terms and OR terms have lists of subterms that must be checked. |
| else if (term instanceof AndTerm) { |
| return checkSearchEncoding(((AndTerm)term).getTerms()); |
| } |
| else if (term instanceof OrTerm) { |
| return checkSearchEncoding(((OrTerm)term).getTerms()); |
| } |
| |
| // non of the other term types (FlagTerm, SentDateTerm, etc.) pose a problem, so we'll give them |
| // a free pass. |
| return false; |
| } |
| |
| |
| /** |
| * Run an array of search term items to check each one for ASCII |
| * encoding problems. |
| * |
| * @param terms The array of terms to check. |
| * |
| * @return True if any of the search terms contains a 7-bit ASCII problem, |
| * false otherwise. |
| */ |
| static public boolean checkSearchEncoding(SearchTerm[] terms) { |
| for (int i = 0; i < terms.length; i++) { |
| if (checkSearchEncoding(terms[i])) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| |
| /** |
| * Check a string to see if this can be processed using just |
| * 7-bit ASCII. |
| * |
| * @param s The string to check |
| * |
| * @return true if the string contains characters outside the 7-bit ascii range, |
| * false otherwise. |
| */ |
| static public boolean checkStringEncoding(String s) { |
| for (int i = 0; i < s.length(); i++) { |
| // any value greater that 0x7f is a problem char. We're not worried about |
| // lower ctl chars (chars < 32) since those are still expressible in 7-bit. |
| if (s.charAt(i) > 127) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| |
| /** |
| * Append a FetchProfile information to an IMAPCommand |
| * that's to be issued. |
| * |
| * @param profile The fetch profile we're using. |
| * |
| * @exception MessagingException |
| */ |
| public void appendFetchProfile(FetchProfile profile) throws MessagingException { |
| // the fetch profile items are a parenthtical list passed on a |
| // FETCH command. |
| startList(); |
| if (profile.contains(UIDFolder.FetchProfileItem.UID)) { |
| appendAtom("UID"); |
| } |
| if (profile.contains(FetchProfile.Item.ENVELOPE)) { |
| // fetching the envelope involves several items |
| appendAtom("ENVELOPE"); |
| appendAtom("INTERNALDATE"); |
| appendAtom("RFC822.SIZE"); |
| } |
| if (profile.contains(FetchProfile.Item.FLAGS)) { |
| appendAtom("FLAGS"); |
| } |
| if (profile.contains(FetchProfile.Item.CONTENT_INFO)) { |
| appendAtom("BODYSTRUCTURE"); |
| } |
| if (profile.contains(IMAPFolder.FetchProfileItem.SIZE)) { |
| appendAtom("RFC822.SIZE"); |
| } |
| // There are two choices here, that are sort of redundant. |
| // if all headers have been requested, there's no point in |
| // adding any specifically requested one. |
| if (profile.contains(IMAPFolder.FetchProfileItem.HEADERS)) { |
| appendAtom("BODY.PEEK[HEADER]"); |
| } |
| else { |
| String[] headers = profile.getHeaderNames(); |
| // have an actual list to retrieve? need to craft this as a sublist |
| // of identified fields. |
| if (headers.length > 0) { |
| appendAtom("BODY.PEEK[HEADER.FIELDS]"); |
| startList(); |
| for (int i = 0; i < headers.length; i++) { |
| appendAtom(headers[i]); |
| } |
| endList(); |
| } |
| } |
| // end the list. |
| endList(); |
| } |
| |
| |
| /** |
| * Append an ACL value to a command. The ACL is the writes string name, |
| * followed by the rights value. This version uses no +/- modifier. |
| * |
| * @param acl The ACL to append. |
| */ |
| public void appendACL(ACL acl) { |
| appendACL(acl, null); |
| } |
| |
| /** |
| * Append an ACL value to a command. The ACL is the writes string name, |
| * followed by the rights value. A +/- modifier can be added to the |
| * // result. |
| * |
| * @param acl The ACL to append. |
| * @param modifier The modifer string (can be null). |
| */ |
| public void appendACL(ACL acl, String modifier) { |
| appendString(acl.getName()); |
| String rights = acl.getRights().toString(); |
| |
| if (modifier != null) { |
| rights = modifier + rights; |
| } |
| appendString(rights); |
| } |
| |
| |
| /** |
| * Append a quota specification to an IMAP command. |
| * |
| * @param quota The quota value to append. |
| */ |
| public void appendQuota(Quota quota) { |
| appendString(quota.quotaRoot); |
| startList(); |
| for (int i = 0; i < quota.resources.length; i++) { |
| appendQuotaResource(quota.resources[i]); |
| } |
| endList(); |
| } |
| |
| /** |
| * Append a Quota.Resource element to an IMAP command. This converts as |
| * the resoure name, the usage value and limit value). |
| * |
| * @param resource The resource element we're appending. |
| */ |
| public void appendQuotaResource(Quota.Resource resource) { |
| appendAtom(resource.name); |
| // NB: For command purposes, only the limit is used. |
| appendLong(resource.limit); |
| } |
| } |
| |