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