View Javadoc

1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.geronimo.javamail.store.imap.connection;
19  
20  import java.io.ByteArrayOutputStream;
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.util.ArrayList;
24  import java.util.HashMap;
25  import java.util.Map;
26  
27  import javax.mail.MessagingException; 
28  import javax.mail.event.FolderEvent; 
29  
30  import org.apache.geronimo.javamail.store.imap.connection.IMAPResponseTokenizer.Token;
31  import org.apache.geronimo.javamail.util.ConnectionException;
32  
33  public class IMAPResponseStream {
34      protected final int BUFFER_SIZE = 1024;   
35      
36      // our source input stream
37      protected InputStream in;
38      // The response buffer 
39      IMAPResponseBuffer out; 
40      // the buffer array 
41      protected byte[] buffer = new byte[BUFFER_SIZE]; 
42      // the current buffer position 
43      int position; 
44      // the current buffer read length 
45      int length; 
46  
47      public IMAPResponseStream(InputStream in) {
48          this.in = in;
49          out = new IMAPResponseBuffer();
50      }
51      
52      public int read() throws IOException {
53          // if we can't read any more, that's an EOF condition. 
54          if (!fillBufferIfNeeded()) {
55              return -1; 
56          }
57          // just grab the next character 
58          return buffer[position++]; 
59      }
60      
61      protected boolean fillBufferIfNeeded() throws IOException {
62          // used up all of the data in the buffer?
63          if (position >= length) {
64              int readLength = 0; 
65              // a read from a network connection can return 0 bytes, 
66              // so we need to be prepared to handle a spin loop.  
67              while (readLength == 0) {
68                  readLength = in.read(buffer, 0, buffer.length); 
69              }
70              // we may have hit the EOF.  Indicate the read failure   
71              if (readLength == -1) {
72                  return false; 
73              }
74              // set our new buffer positions. 
75              position = 0; 
76              length = readLength; 
77          }
78          return true; 
79      }
80  
81  
82      /**
83       * Read a single response line from the input stream, returning
84       * a parsed and processed response line.
85       *
86       * @return A parsed IMAPResponse item using the response data.
87       * @exception MessagingException
88       */
89      public IMAPResponse readResponse() throws MessagingException  
90        {  
91          // reset our accumulator 
92          out.reset(); 
93          // now read a buffer of data
94          byte[] data = readData();
95          
96          // and create a tokenizer for parsing this down.
97          IMAPResponseTokenizer tokenizer = new IMAPResponseTokenizer(data);
98          // get the first token.
99          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