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