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.IOException;
022 import java.io.InputStream;
023 import java.util.ArrayList;
024 import java.util.HashMap;
025 import java.util.Map;
026
027 import javax.mail.MessagingException;
028 import javax.mail.event.FolderEvent;
029
030 import org.apache.geronimo.javamail.store.imap.connection.IMAPResponseTokenizer.Token;
031 import org.apache.geronimo.javamail.util.ConnectionException;
032
033 public class IMAPResponseStream {
034 protected final int BUFFER_SIZE = 1024;
035
036 // our source input stream
037 protected InputStream in;
038 // The response buffer
039 IMAPResponseBuffer out;
040 // the buffer array
041 protected byte[] buffer = new byte[BUFFER_SIZE];
042 // the current buffer position
043 int position;
044 // the current buffer read length
045 int length;
046
047 public IMAPResponseStream(InputStream in) {
048 this.in = in;
049 out = new IMAPResponseBuffer();
050 }
051
052 public int read() throws IOException {
053 // if we can't read any more, that's an EOF condition.
054 if (!fillBufferIfNeeded()) {
055 return -1;
056 }
057 // just grab the next character
058 return buffer[position++];
059 }
060
061 protected boolean fillBufferIfNeeded() throws IOException {
062 // used up all of the data in the buffer?
063 if (position >= length) {
064 int readLength = 0;
065 // a read from a network connection can return 0 bytes,
066 // so we need to be prepared to handle a spin loop.
067 while (readLength == 0) {
068 readLength = in.read(buffer, 0, buffer.length);
069 }
070 // we may have hit the EOF. Indicate the read failure
071 if (readLength == -1) {
072 return false;
073 }
074 // set our new buffer positions.
075 position = 0;
076 length = readLength;
077 }
078 return true;
079 }
080
081
082 /**
083 * Read a single response line from the input stream, returning
084 * a parsed and processed response line.
085 *
086 * @return A parsed IMAPResponse item using the response data.
087 * @exception MessagingException
088 */
089 public IMAPResponse readResponse() throws MessagingException
090 {
091 // reset our accumulator
092 out.reset();
093 // now read a buffer of data
094 byte[] data = readData();
095
096 // and create a tokenizer for parsing this down.
097 IMAPResponseTokenizer tokenizer = new IMAPResponseTokenizer(data);
098 // get the first token.
099 Token token = tokenizer.next();
100
101 int type = token.getType();
102
103 // a continuation response. This will terminate a response set.
104 if (type == Token.CONTINUATION) {
105 return new IMAPContinuationResponse(data);
106 }
107 // unsolicited response. There are multiple forms of these, which might actually be
108 // part of the response for the last issued command.
109 else if (type == Token.UNTAGGED) {
110 // step to the next token, which will give us the type
111 token = tokenizer.next();
112 // if the token is numeric, then this is a size response in the
113 // form "* nn type"
114 if (token.isType(Token.NUMERIC)) {
115 int size = token.getInteger();
116
117 token = tokenizer.next();
118
119 String keyword = token.getValue();
120
121 // FETCH responses require fairly complicated parsing. Other
122 // size/message updates are fairly generic.
123 if (keyword.equals("FETCH")) {
124 return new IMAPFetchResponse(size, data, tokenizer);
125 }
126 return new IMAPSizeResponse(keyword, size, data);
127 }
128
129 // this needs to be an ATOM type, which will tell us what format this untagged
130 // response is in. There are many different untagged formats, some general, some
131 // specific to particular command types.
132 if (token.getType() != Token.ATOM) {
133 throw new MessagingException("Unknown server response: " + new String(data));
134 }
135
136 String keyword = token.getValue();
137 // many response are in the form "* OK [keyword value] message".
138 if (keyword.equals("OK")) {
139 return parseUntaggedOkResponse(data, tokenizer);
140 }
141 // preauth status response
142 else if (keyword.equals("PREAUTH")) {
143 return new IMAPServerStatusResponse("PREAUTH", tokenizer.getRemainder(), data);
144 }
145 // preauth status response
146 else if (keyword.equals("BYE")) {
147 return new IMAPServerStatusResponse("BYE", tokenizer.getRemainder(), data);
148 }
149 else if (keyword.equals("BAD")) {
150 // these are generally ignored.
151 return new IMAPServerStatusResponse("BAD", tokenizer.getRemainder(), data);
152 }
153 else if (keyword.equals("NO")) {
154 // these are generally ignored.
155 return new IMAPServerStatusResponse("NO", tokenizer.getRemainder(), data);
156 }
157 // a complex CAPABILITY response
158 else if (keyword.equals("CAPABILITY")) {
159 return new IMAPCapabilityResponse(tokenizer, data);
160 }
161 // a complex LIST response
162 else if (keyword.equals("LIST")) {
163 return new IMAPListResponse("LIST", data, tokenizer);
164 }
165 // a complex FLAGS response
166 else if (keyword.equals("FLAGS")) {
167 // parse this into a flags set.
168 return new IMAPFlagsResponse(data, tokenizer);
169 }
170 // a complex LSUB response (identical in format to LIST)
171 else if (keyword.equals("LSUB")) {
172 return new IMAPListResponse("LSUB", data, tokenizer);
173 }
174 // a STATUS response, which will contain a list of elements
175 else if (keyword.equals("STATUS")) {
176 return new IMAPStatusResponse(data, tokenizer);
177 }
178 // SEARCH requests return an variable length list of message matches.
179 else if (keyword.equals("SEARCH")) {
180 return new IMAPSearchResponse(data, tokenizer);
181 }
182 // ACL requests return an variable length list of ACL values .
183 else if (keyword.equals("ACL")) {
184 return new IMAPACLResponse(data, tokenizer);
185 }
186 // LISTRIGHTS requests return a variable length list of RIGHTS values .
187 else if (keyword.equals("LISTRIGHTS")) {
188 return new IMAPListRightsResponse(data, tokenizer);
189 }
190 // MYRIGHTS requests return a list of user rights for a mailbox name.
191 else if (keyword.equals("MYRIGHTS")) {
192 return new IMAPMyRightsResponse(data, tokenizer);
193 }
194 // QUOTAROOT requests return a list of mailbox quota root names
195 else if (keyword.equals("QUOTAROOT")) {
196 return new IMAPQuotaRootResponse(data, tokenizer);
197 }
198 // QUOTA requests return a list of quota values for a root name
199 else if (keyword.equals("QUOTA")) {
200 return new IMAPQuotaResponse(data, tokenizer);
201 }
202 else if (keyword.equals("NAMESPACE")) {
203 return new IMAPNamespaceResponse(data, tokenizer);
204 }
205 }
206 // begins with a word, this should be the tagged response from the last command.
207 else if (type == Token.ATOM) {
208 String tag = token.getValue();
209 token = tokenizer.next();
210 String status = token.getValue();
211 // primary information in one of these is the status field, which hopefully
212 // is 'OK'
213 return new IMAPTaggedResponse(tag, status, tokenizer.getRemainder(), data);
214 }
215 throw new MessagingException("Unknown server response: " + new String(data));
216 }
217
218 /**
219 * Parse an unsolicited OK status response. These
220 * responses are of the form:
221 *
222 * * OK [keyword arguments ...] message
223 *
224 * The part in the brackets are optional, but
225 * most OK messages will have some sort of update.
226 *
227 * @param data The raw message data
228 * @param tokenizer The tokenizer being used for this message.
229 *
230 * @return An IMAPResponse instance for this message.
231 */
232 private IMAPResponse parseUntaggedOkResponse(byte [] data, IMAPResponseTokenizer tokenizer) throws MessagingException {
233 Token token = tokenizer.peek();
234 // we might have an optional value here
235 if (token.getType() != '[') {
236 // this has no tagging item, so there's nothing to be processed
237 // later.
238 return new IMAPOkResponse("OK", null, tokenizer.getRemainder(), data);
239 }
240 // skip over the "[" token
241 tokenizer.next();
242 token = tokenizer.next();
243 String keyword = token.getValue();
244
245 // Permanent flags gets special handling
246 if (keyword.equals("PERMANENTFLAGS")) {
247 return new IMAPPermanentFlagsResponse(data, tokenizer);
248 }
249
250 ArrayList arguments = new ArrayList();
251
252 // strip off all of the argument tokens until the "]" list terminator.
253 token = tokenizer.next();
254 while (token.getType() != ']') {
255 arguments.add(token);
256 token = tokenizer.next();
257 }
258 // this has a tagged keyword and arguments that will be processed later.
259 return new IMAPOkResponse(keyword, arguments, tokenizer.getRemainder(), data);
260 }
261
262
263 /**
264 * Read a "line" of server response data. An individual line
265 * may span multiple line breaks, depending on syntax implications.
266 *
267 * @return
268 * @exception MessagingException
269 */
270 public byte[] readData() throws MessagingException {
271 // reset out buffer accumulator
272 out.reset();
273 // read until the end of the response into our buffer.
274 readBuffer();
275 // get the accumulated data.
276 return out.toByteArray();
277 }
278
279 /**
280 * Read a buffer of data. This accumulates the data into a
281 * ByteArrayOutputStream, terminating the processing at a line
282 * break. This also handles line breaks that are the result
283 * of literal continuations in the stream.
284 *
285 * @exception MessagingException
286 * @exception IOException
287 */
288 public void readBuffer() throws MessagingException {
289 while (true) {
290 int ch = nextByte();
291 // potential end of line? Check the next character, and if it is an end of line,
292 // we need to do literal processing.
293 if (ch == '\r') {
294 int next = nextByte();
295 if (next == '\n') {
296 // had a line break, which might be part of a literal marker. Check for the signature,
297 // and if we found it, continue with the next line. In any case, we're done with processing here.
298 checkLiteral();
299 return;
300 }
301 }
302 // write this to the buffer.
303 out.write(ch);
304 }
305 }
306
307
308 /**
309 * Check the line just read to see if we're processing a line
310 * with a literal value. Literals are encoded as "{length}\r\n",
311 * so if we've read up to the line break, we can check to see
312 * if we need to continue reading.
313 *
314 * If a literal marker is found, we read that many characters
315 * from the reader without looking for line breaks. Once we've
316 * read the literal data, we just read the rest of the line
317 * as normal (which might also end with a literal marker).
318 *
319 * @exception MessagingException
320 */
321 public void checkLiteral() throws MessagingException {
322 try {
323 // see if we have a literal length signature at the end.
324 int length = out.getLiteralLength();
325
326 // -1 means no literal length, so we're done reading this particular response.
327 if (length == -1) {
328 return;
329 }
330
331 // we need to write out the literal line break marker.
332 out.write('\r');
333 out.write('\n');
334
335 // have something we're supposed to read for the literal?
336 if (length > 0) {
337 byte[] bytes = new byte[length];
338
339 int offset = 0;
340
341 // The InputStream can return less than the requested length if it needs to block.
342 // This may take a couple iterations to get everything, particularly if it's long.
343 while (length > 0) {
344 int read = -1;
345 try {
346 read = in.read(bytes, offset, length);
347 } catch (IOException e) {
348 throw new MessagingException("Unexpected read error on server connection", e);
349 }
350 // premature EOF we can't ignore.
351 if (read == -1) {
352 throw new MessagingException("Unexpected end of stream");
353 }
354 length -= read;
355 offset += read;
356 }
357
358 // write this out to the output stream.
359 out.write(bytes);
360 }
361 // Now that we have the literal data, we need to read the rest of the response line (which might contain
362 // additional literals). Just recurse on the line reading logic.
363 readBuffer();
364 } catch (IOException e) {
365 e.printStackTrace();
366 // this is a byte array output stream...should never happen
367 }
368 }
369
370
371 /**
372 * Get the next byte from the input stream, handling read errors
373 * and EOF conditions as MessagingExceptions.
374 *
375 * @return The next byte read from the stream.
376 * @exception MessagingException
377 */
378 protected int nextByte() throws MessagingException {
379 try {
380 int next = in.read();
381 if (next == -1) {
382 throw new MessagingException("Read error on IMAP server connection");
383 }
384 return next;
385 } catch (IOException e) {
386 throw new MessagingException("Unexpected error on server stream", e);
387 }
388 }
389
390
391 }
392