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 018package org.apache.commons.net.imap; 019 020import java.io.BufferedReader; 021import java.io.BufferedWriter; 022import java.io.EOFException; 023import java.io.InputStreamReader; 024import java.io.IOException; 025import java.io.OutputStreamWriter; 026import java.util.ArrayList; 027import java.util.List; 028 029import org.apache.commons.net.SocketClient; 030import org.apache.commons.net.io.CRLFLineReader; 031 032 033/** 034 * The IMAP class provides the basic the functionality necessary to implement your 035 * own IMAP client. 036 */ 037public class IMAP extends SocketClient 038{ 039 /** The default IMAP port (RFC 3501). */ 040 public static final int DEFAULT_PORT = 143; 041 042 public enum IMAPState 043 { 044 /** A constant representing the state where the client is not yet connected to a server. */ 045 DISCONNECTED_STATE, 046 /** A constant representing the "not authenticated" state. */ 047 NOT_AUTH_STATE, 048 /** A constant representing the "authenticated" state. */ 049 AUTH_STATE, 050 /** A constant representing the "logout" state. */ 051 LOGOUT_STATE; 052 } 053 054 // RFC 3501, section 5.1.3. It should be "modified UTF-7". 055 /** 056 * The default control socket ecoding. 057 */ 058 protected static final String __DEFAULT_ENCODING = "ISO-8859-1"; 059 060 private IMAPState __state; 061 protected BufferedWriter __writer; 062 063 protected BufferedReader _reader; 064 private int _replyCode; 065 private final List<String> _replyLines; 066 067 /** 068 * Implement this interface and register it via {@link #setChunkListener(IMAPChunkListener)} 069 * in order to get access to multi-line partial command responses. 070 * Useful when processing large FETCH responses. 071 */ 072 public interface IMAPChunkListener { 073 /** 074 * Called when a multi-line partial response has been received. 075 * @param imap the instance, get the response 076 * by calling {@link #getReplyString()} or {@link #getReplyStrings()} 077 * @return {@code true} if the reply buffer is to be cleared on return 078 */ 079 boolean chunkReceived(IMAP imap); 080 } 081 082 /** 083 * <p> 084 * Implementation of IMAPChunkListener that returns {@code true} 085 * but otherwise does nothing. 086 * </p> 087 * <p> 088 * This is intended for use with a suitable ProtocolCommandListener. 089 * If the IMAP response contains multiple-line data, the protocol listener 090 * will be called for each multi-line chunk. 091 * The accumulated reply data will be cleared after calling the listener. 092 * If the response is very long, this can significantly reduce memory requirements. 093 * The listener will also start receiving response data earlier, as it does not have 094 * to wait for the entire response to be read. 095 * </p> 096 * <p> 097 * The ProtocolCommandListener must be prepared to accept partial responses. 098 * This should not be a problem for listeners that just log the input. 099 * </p> 100 * @see #setChunkListener(IMAPChunkListener) 101 * @since 3.4 102 */ 103 public static final IMAPChunkListener TRUE_CHUNK_LISTENER = new IMAPChunkListener(){ 104 @Override 105 public boolean chunkReceived(IMAP imap) { 106 return true; 107 } 108 109 }; 110 private volatile IMAPChunkListener __chunkListener; 111 112 private final char[] _initialID = { 'A', 'A', 'A', 'A' }; 113 114 /** 115 * The default IMAPClient constructor. Initializes the state 116 * to <code>DISCONNECTED_STATE</code>. 117 */ 118 public IMAP() 119 { 120 setDefaultPort(DEFAULT_PORT); 121 __state = IMAPState.DISCONNECTED_STATE; 122 _reader = null; 123 __writer = null; 124 _replyLines = new ArrayList<String>(); 125 createCommandSupport(); 126 } 127 128 /** 129 * Get the reply for a command that expects a tagged response. 130 * 131 * @throws IOException 132 */ 133 private void __getReply() throws IOException 134 { 135 __getReply(true); // tagged response 136 } 137 138 /** 139 * Get the reply for a command, reading the response until the 140 * reply is found. 141 * 142 * @param wantTag {@code true} if the command expects a tagged response. 143 * @throws IOException 144 */ 145 private void __getReply(boolean wantTag) throws IOException 146 { 147 _replyLines.clear(); 148 String line = _reader.readLine(); 149 150 if (line == null) { 151 throw new EOFException("Connection closed without indication."); 152 } 153 154 _replyLines.add(line); 155 156 if (wantTag) { 157 while(IMAPReply.isUntagged(line)) { 158 int literalCount = IMAPReply.literalCount(line); 159 final boolean isMultiLine = literalCount >= 0; 160 while (literalCount >= 0) { 161 line=_reader.readLine(); 162 if (line == null) { 163 throw new EOFException("Connection closed without indication."); 164 } 165 _replyLines.add(line); 166 literalCount -= (line.length() + 2); // Allow for CRLF 167 } 168 if (isMultiLine) { 169 IMAPChunkListener il = __chunkListener; 170 if (il != null) { 171 boolean clear = il.chunkReceived(this); 172 if (clear) { 173 fireReplyReceived(IMAPReply.PARTIAL, getReplyString()); 174 _replyLines.clear(); 175 } 176 } 177 } 178 line = _reader.readLine(); // get next chunk or final tag 179 if (line == null) { 180 throw new EOFException("Connection closed without indication."); 181 } 182 _replyLines.add(line); 183 } 184 // check the response code on the last line 185 _replyCode = IMAPReply.getReplyCode(line); 186 } else { 187 _replyCode = IMAPReply.getUntaggedReplyCode(line); 188 } 189 190 fireReplyReceived(_replyCode, getReplyString()); 191 } 192 193 /** 194 * Overrides {@link SocketClient#fireReplyReceived(int, String)} so as to 195 * avoid creating the reply string if there are no listeners to invoke. 196 * 197 * @param replyCode passed to the listeners 198 * @param ignored the string is only created if there are listeners defined. 199 * @see #getReplyString() 200 * @since 3.4 201 */ 202 @Override 203 protected void fireReplyReceived(int replyCode, String ignored) { 204 if (getCommandSupport().getListenerCount() > 0) { 205 getCommandSupport().fireReplyReceived(replyCode, getReplyString()); 206 } 207 } 208 209 /** 210 * Performs connection initialization and sets state to 211 * {@link IMAPState#NOT_AUTH_STATE}. 212 */ 213 @Override 214 protected void _connectAction_() throws IOException 215 { 216 super._connectAction_(); 217 _reader = 218 new CRLFLineReader(new InputStreamReader(_input_, 219 __DEFAULT_ENCODING)); 220 __writer = 221 new BufferedWriter(new OutputStreamWriter(_output_, 222 __DEFAULT_ENCODING)); 223 int tmo = getSoTimeout(); 224 if (tmo <= 0) { // none set currently 225 setSoTimeout(connectTimeout); // use connect timeout to ensure we don't block forever 226 } 227 __getReply(false); // untagged response 228 if (tmo <= 0) { 229 setSoTimeout(tmo); // restore the original value 230 } 231 setState(IMAPState.NOT_AUTH_STATE); 232 } 233 234 /** 235 * Sets IMAP client state. This must be one of the 236 * <code>_STATE</code> constants. 237 * 238 * @param state The new state. 239 */ 240 protected void setState(IMAP.IMAPState state) 241 { 242 __state = state; 243 } 244 245 246 /** 247 * Returns the current IMAP client state. 248 * 249 * @return The current IMAP client state. 250 */ 251 public IMAP.IMAPState getState() 252 { 253 return __state; 254 } 255 256 /** 257 * Disconnects the client from the server, and sets the state to 258 * <code> DISCONNECTED_STATE </code>. The reply text information 259 * from the last issued command is voided to allow garbage collection 260 * of the memory used to store that information. 261 * 262 * @exception IOException If there is an error in disconnecting. 263 */ 264 @Override 265 public void disconnect() throws IOException 266 { 267 super.disconnect(); 268 _reader = null; 269 __writer = null; 270 _replyLines.clear(); 271 setState(IMAPState.DISCONNECTED_STATE); 272 } 273 274 275 /** 276 * Sends a command an arguments to the server and returns the reply code. 277 * 278 * @param commandID The ID (tag) of the command. 279 * @param command The IMAP command to send. 280 * @param args The command arguments. 281 * @return The server reply code (either IMAPReply.OK, IMAPReply.NO or IMAPReply.BAD). 282 */ 283 private int sendCommandWithID(String commandID, String command, String args) throws IOException 284 { 285 StringBuilder __commandBuffer = new StringBuilder(); 286 if (commandID != null) 287 { 288 __commandBuffer.append(commandID); 289 __commandBuffer.append(' '); 290 } 291 __commandBuffer.append(command); 292 293 if (args != null) 294 { 295 __commandBuffer.append(' '); 296 __commandBuffer.append(args); 297 } 298 __commandBuffer.append(SocketClient.NETASCII_EOL); 299 300 String message = __commandBuffer.toString(); 301 __writer.write(message); 302 __writer.flush(); 303 304 fireCommandSent(command, message); 305 306 __getReply(); 307 return _replyCode; 308 } 309 310 /** 311 * Sends a command an arguments to the server and returns the reply code. 312 * 313 * @param command The IMAP command to send. 314 * @param args The command arguments. 315 * @return The server reply code (see IMAPReply). 316 * @throws IOException on error 317 */ 318 public int sendCommand(String command, String args) throws IOException 319 { 320 return sendCommandWithID(generateCommandID(), command, args); 321 } 322 323 /** 324 * Sends a command with no arguments to the server and returns the 325 * reply code. 326 * 327 * @param command The IMAP command to send. 328 * @return The server reply code (see IMAPReply). 329 * @throws IOException on error 330 */ 331 public int sendCommand(String command) throws IOException 332 { 333 return sendCommand(command, null); 334 } 335 336 /** 337 * Sends a command and arguments to the server and returns the reply code. 338 * 339 * @param command The IMAP command to send 340 * (one of the IMAPCommand constants). 341 * @param args The command arguments. 342 * @return The server reply code (see IMAPReply). 343 * @throws IOException on error 344 */ 345 public int sendCommand(IMAPCommand command, String args) throws IOException 346 { 347 return sendCommand(command.getIMAPCommand(), args); 348 } 349 350 /** 351 * Sends a command and arguments to the server and return whether successful. 352 * 353 * @param command The IMAP command to send 354 * (one of the IMAPCommand constants). 355 * @param args The command arguments. 356 * @return {@code true} if the command was successful 357 * @throws IOException on error 358 */ 359 public boolean doCommand(IMAPCommand command, String args) throws IOException 360 { 361 return IMAPReply.isSuccess(sendCommand(command, args)); 362 } 363 364 /** 365 * Sends a command with no arguments to the server and returns the 366 * reply code. 367 * 368 * @param command The IMAP command to send 369 * (one of the IMAPCommand constants). 370 * @return The server reply code (see IMAPReply). 371 * @throws IOException on error 372 **/ 373 public int sendCommand(IMAPCommand command) throws IOException 374 { 375 return sendCommand(command, null); 376 } 377 378 /** 379 * Sends a command to the server and return whether successful. 380 * 381 * @param command The IMAP command to send 382 * (one of the IMAPCommand constants). 383 * @return {@code true} if the command was successful 384 * @throws IOException on error 385 */ 386 public boolean doCommand(IMAPCommand command) throws IOException 387 { 388 return IMAPReply.isSuccess(sendCommand(command)); 389 } 390 391 /** 392 * Sends data to the server and returns the reply code. 393 * 394 * @param command The IMAP command to send. 395 * @return The server reply code (see IMAPReply). 396 * @throws IOException on error 397 */ 398 public int sendData(String command) throws IOException 399 { 400 return sendCommandWithID(null, command, null); 401 } 402 403 /** 404 * Returns an array of lines received as a reply to the last command 405 * sent to the server. The lines have end of lines truncated. 406 * @return The last server response. 407 */ 408 public String[] getReplyStrings() 409 { 410 return _replyLines.toArray(new String[_replyLines.size()]); 411 } 412 413 /** 414 * Returns the reply to the last command sent to the server. 415 * The value is a single string containing all the reply lines including 416 * newlines. 417 * 418 * @return The last server response. 419 */ 420 public String getReplyString() 421 { 422 StringBuilder buffer = new StringBuilder(256); 423 for (String s : _replyLines) 424 { 425 buffer.append(s); 426 buffer.append(SocketClient.NETASCII_EOL); 427 } 428 429 return buffer.toString(); 430 } 431 432 /** 433 * Sets the current chunk listener. 434 * If a listener is registered and the implementation returns true, 435 * then any registered 436 * {@link org.apache.commons.net.PrintCommandListener PrintCommandListener} 437 * instances will be invoked with the partial response and a status of 438 * {@link IMAPReply#PARTIAL} to indicate that the final reply code is not yet known. 439 * @param listener the class to use, or {@code null} to disable 440 * @see #TRUE_CHUNK_LISTENER 441 * @since 3.4 442 */ 443 public void setChunkListener(IMAPChunkListener listener) { 444 __chunkListener = listener; 445 } 446 447 /** 448 * Generates a new command ID (tag) for a command. 449 * @return a new command ID (tag) for an IMAP command. 450 */ 451 protected String generateCommandID() 452 { 453 String res = new String (_initialID); 454 // "increase" the ID for the next call 455 boolean carry = true; // want to increment initially 456 for (int i = _initialID.length-1; carry && i>=0; i--) 457 { 458 if (_initialID[i] == 'Z') 459 { 460 _initialID[i] = 'A'; 461 } 462 else 463 { 464 _initialID[i]++; 465 carry = false; // did not wrap round 466 } 467 } 468 return res; 469 } 470} 471/* kate: indent-width 4; replace-tabs on; */