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