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.commons.net.imap; 19 20 import java.io.BufferedReader; 21 import java.io.BufferedWriter; 22 import java.io.EOFException; 23 import java.io.IOException; 24 import java.io.InputStreamReader; 25 import java.io.OutputStreamWriter; 26 import java.util.ArrayList; 27 import java.util.List; 28 29 import org.apache.commons.net.SocketClient; 30 import org.apache.commons.net.io.CRLFLineReader; 31 import org.apache.commons.net.util.NetConstants; 32 33 /** 34 * The IMAP class provides the basic the functionality necessary to implement your own IMAP client. 35 */ 36 public class IMAP extends SocketClient { 37 /** 38 * Implement this interface and register it via {@link #setChunkListener(IMAPChunkListener)} in order to get access to multi-line partial command responses. 39 * Useful when processing large FETCH responses. 40 */ 41 public interface IMAPChunkListener { 42 /** 43 * Called when a multi-line partial response has been received. 44 * 45 * @param imap the instance, get the response by calling {@link #getReplyString()} or {@link #getReplyStrings()} 46 * @return {@code true} if the reply buffer is to be cleared on return 47 */ 48 boolean chunkReceived(IMAP imap); 49 } 50 51 public enum IMAPState { 52 /** A constant representing the state where the client is not yet connected to a server. */ 53 DISCONNECTED_STATE, 54 /** A constant representing the "not authenticated" state. */ 55 NOT_AUTH_STATE, 56 /** A constant representing the "authenticated" state. */ 57 AUTH_STATE, 58 /** A constant representing the "logout" state. */ 59 LOGOUT_STATE 60 } 61 62 /** The default IMAP port (RFC 3501). */ 63 public static final int DEFAULT_PORT = 143; 64 65 // RFC 3501, section 5.1.3. It should be "modified UTF-7". 66 /** 67 * The default control socket encoding. 68 */ 69 protected static final String __DEFAULT_ENCODING = "ISO-8859-1"; 70 /** 71 * <p> 72 * Implementation of IMAPChunkListener that returns {@code true} but otherwise does nothing. 73 * </p> 74 * <p> 75 * This is intended for use with a suitable ProtocolCommandListener. If the IMAP response contains multiple-line data, the protocol listener will be called 76 * for each multi-line chunk. The accumulated reply data will be cleared after calling the listener. If the response is very long, this can significantly 77 * reduce memory requirements. The listener will also start receiving response data earlier, as it does not have to wait for the entire response to be read. 78 * </p> 79 * <p> 80 * The ProtocolCommandListener must be prepared to accept partial responses. This should not be a problem for listeners that just log the input. 81 * </p> 82 * 83 * @see #setChunkListener(IMAPChunkListener) 84 * @since 3.4 85 */ 86 public static final IMAPChunkListener TRUE_CHUNK_LISTENER = imap -> true; 87 88 /** 89 * Quote an input string if necessary. If the string is enclosed in double-quotes it is assumed to be quoted already and is returned unchanged. If it is the 90 * empty string, "" is returned. If it contains a space then it is enclosed in double quotes, escaping the characters backslash and double-quote. 91 * 92 * @param input the value to be quoted, may be null 93 * @return the quoted value 94 */ 95 static String quoteMailboxName(final String input) { 96 if (input == null) { // Don't throw NPE here 97 return null; 98 } 99 if (input.isEmpty()) { 100 return "\"\""; // return the string "" 101 } 102 // Length check is necessary to ensure a lone double-quote is quoted 103 if (input.length() > 1 && input.startsWith("\"") && input.endsWith("\"")) { 104 return input; // Assume already quoted 105 } 106 if (input.contains(" ")) { 107 // quoted strings must escape \ and " 108 return "\"" + input.replaceAll("([\\\\\"])", "\\\\$1") + "\""; 109 } 110 return input; 111 112 } 113 114 private IMAPState state; 115 protected BufferedWriter __writer; 116 117 protected BufferedReader _reader; 118 119 private int replyCode; 120 private final List<String> replyLines; 121 122 private volatile IMAPChunkListener chunkListener; 123 124 private final char[] initialID = { 'A', 'A', 'A', 'A' }; 125 126 /** 127 * The default IMAPClient constructor. Initializes the state to <code>DISCONNECTED_STATE</code>. 128 */ 129 public IMAP() { 130 setDefaultPort(DEFAULT_PORT); 131 state = IMAPState.DISCONNECTED_STATE; 132 _reader = null; 133 __writer = null; 134 replyLines = new ArrayList<>(); 135 createCommandSupport(); 136 } 137 138 /** 139 * Performs connection initialization and sets state to {@link IMAPState#NOT_AUTH_STATE}. 140 */ 141 @Override 142 protected void _connectAction_() throws IOException { 143 super._connectAction_(); 144 _reader = new CRLFLineReader(new InputStreamReader(_input_, __DEFAULT_ENCODING)); 145 __writer = new BufferedWriter(new OutputStreamWriter(_output_, __DEFAULT_ENCODING)); 146 final int tmo = getSoTimeout(); 147 if (tmo <= 0) { // none set currently 148 setSoTimeout(connectTimeout); // use connect timeout to ensure we don't block forever 149 } 150 getReply(false); // untagged response 151 if (tmo <= 0) { 152 setSoTimeout(tmo); // restore the original value 153 } 154 setState(IMAPState.NOT_AUTH_STATE); 155 } 156 157 /** 158 * Disconnects the client from the server, and sets the state to <code> DISCONNECTED_STATE </code>. The reply text information from the last issued command 159 * is voided to allow garbage collection of the memory used to store that information. 160 * 161 * @throws IOException If there is an error in disconnecting. 162 */ 163 @Override 164 public void disconnect() throws IOException { 165 super.disconnect(); 166 _reader = null; 167 __writer = null; 168 replyLines.clear(); 169 setState(IMAPState.DISCONNECTED_STATE); 170 } 171 172 /** 173 * Sends a command to the server and return whether successful. 174 * 175 * @param command The IMAP command to send (one of the IMAPCommand constants). 176 * @return {@code true} if the command was successful 177 * @throws IOException on error 178 */ 179 public boolean doCommand(final IMAPCommand command) throws IOException { 180 return IMAPReply.isSuccess(sendCommand(command)); 181 } 182 183 /** 184 * Sends a command and arguments to the server and return whether successful. 185 * 186 * @param command The IMAP command to send (one of the IMAPCommand constants). 187 * @param args The command arguments. 188 * @return {@code true} if the command was successful 189 * @throws IOException on error 190 */ 191 public boolean doCommand(final IMAPCommand command, final String args) throws IOException { 192 return IMAPReply.isSuccess(sendCommand(command, args)); 193 } 194 195 /** 196 * Overrides {@link SocketClient#fireReplyReceived(int, String)} so as to 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 * Generates a new command ID (tag) for a command. 212 * 213 * @return a new command ID (tag) for an IMAP command. 214 */ 215 protected String generateCommandID() { 216 final String res = new String(initialID); 217 // "increase" the ID for the next call 218 boolean carry = true; // want to increment initially 219 for (int i = initialID.length - 1; carry && i >= 0; i--) { 220 if (initialID[i] == 'Z') { 221 initialID[i] = 'A'; 222 } else { 223 initialID[i]++; 224 carry = false; // did not wrap round 225 } 226 } 227 return res; 228 } 229 230 /** 231 * Get the reply for a command that expects a tagged response. 232 * 233 * @throws IOException 234 */ 235 private void getReply() throws IOException { 236 getReply(true); // tagged response 237 } 238 239 /** 240 * Get the reply for a command, reading the response until the reply is found. 241 * 242 * @param wantTag {@code true} if the command expects a tagged response. 243 * @throws IOException 244 */ 245 private void getReply(final boolean wantTag) throws IOException { 246 replyLines.clear(); 247 String line = _reader.readLine(); 248 249 if (line == null) { 250 throw new EOFException("Connection closed without indication."); 251 } 252 253 replyLines.add(line); 254 255 if (wantTag) { 256 while (IMAPReply.isUntagged(line)) { 257 int literalCount = IMAPReply.literalCount(line); 258 final boolean isMultiLine = literalCount >= 0; 259 while (literalCount >= 0) { 260 line = _reader.readLine(); 261 if (line == null) { 262 throw new EOFException("Connection closed without indication."); 263 } 264 replyLines.add(line); 265 literalCount -= line.length() + 2; // Allow for CRLF 266 } 267 if (isMultiLine) { 268 final IMAPChunkListener il = chunkListener; 269 if (il != null) { 270 final boolean clear = il.chunkReceived(this); 271 if (clear) { 272 fireReplyReceived(IMAPReply.PARTIAL, getReplyString()); 273 replyLines.clear(); 274 } 275 } 276 } 277 line = _reader.readLine(); // get next chunk or final tag 278 if (line == null) { 279 throw new EOFException("Connection closed without indication."); 280 } 281 replyLines.add(line); 282 } 283 // check the response code on the last line 284 replyCode = IMAPReply.getReplyCode(line); 285 } else { 286 replyCode = IMAPReply.getUntaggedReplyCode(line); 287 } 288 289 fireReplyReceived(replyCode, getReplyString()); 290 } 291 292 /** 293 * Returns the reply to the last command sent to the server. The value is a single string containing all the reply lines including newlines. 294 * 295 * @return The last server response. 296 */ 297 public String getReplyString() { 298 final StringBuilder buffer = new StringBuilder(256); 299 for (final String s : replyLines) { 300 buffer.append(s); 301 buffer.append(SocketClient.NETASCII_EOL); 302 } 303 304 return buffer.toString(); 305 } 306 307 /** 308 * Returns an array of lines received as a reply to the last command sent to the server. The lines have end of lines truncated. 309 * 310 * @return The last server response. 311 */ 312 public String[] getReplyStrings() { 313 return replyLines.toArray(NetConstants.EMPTY_STRING_ARRAY); 314 } 315 316 /** 317 * Returns the current IMAP client state. 318 * 319 * @return The current IMAP client state. 320 */ 321 public IMAP.IMAPState getState() { 322 return state; 323 } 324 325 /** 326 * Sends a command with no arguments to the server and returns the reply code. 327 * 328 * @param command The IMAP command to send (one of the IMAPCommand constants). 329 * @return The server reply code (see IMAPReply). 330 * @throws IOException on error 331 **/ 332 public int sendCommand(final IMAPCommand command) throws IOException { 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 (one of the IMAPCommand constants). 340 * @param args The command arguments. 341 * @return The server reply code (see IMAPReply). 342 * @throws IOException on error 343 */ 344 public int sendCommand(final IMAPCommand command, final String args) throws IOException { 345 return sendCommand(command.getIMAPCommand(), args); 346 } 347 348 /** 349 * Sends a command with no arguments to the server and returns the reply code. 350 * 351 * @param command The IMAP command to send. 352 * @return The server reply code (see IMAPReply). 353 * @throws IOException on error 354 */ 355 public int sendCommand(final String command) throws IOException { 356 return sendCommand(command, null); 357 } 358 359 /** 360 * Sends a command an arguments to the server and returns the reply code. 361 * 362 * @param command The IMAP command to send. 363 * @param args The command arguments. 364 * @return The server reply code (see IMAPReply). 365 * @throws IOException on error 366 */ 367 public int sendCommand(final String command, final String args) throws IOException { 368 return sendCommandWithID(generateCommandID(), command, args); 369 } 370 371 /** 372 * Sends a command an arguments to the server and returns the reply code. 373 * 374 * @param commandID The ID (tag) of the command. 375 * @param command The IMAP command to send. 376 * @param args The command arguments. 377 * @return The server reply code (either IMAPReply.OK, IMAPReply.NO or IMAPReply.BAD). 378 */ 379 private int sendCommandWithID(final String commandID, final String command, final String args) throws IOException { 380 final StringBuilder __commandBuffer = new StringBuilder(); 381 if (commandID != null) { 382 __commandBuffer.append(commandID); 383 __commandBuffer.append(' '); 384 } 385 __commandBuffer.append(command); 386 387 if (args != null) { 388 __commandBuffer.append(' '); 389 __commandBuffer.append(args); 390 } 391 __commandBuffer.append(SocketClient.NETASCII_EOL); 392 393 final String message = __commandBuffer.toString(); 394 __writer.write(message); 395 __writer.flush(); 396 397 fireCommandSent(command, message); 398 399 getReply(); 400 return replyCode; 401 } 402 403 /** 404 * Sends data to the server and returns the reply code. 405 * 406 * @param command The IMAP command to send. 407 * @return The server reply code (see IMAPReply). 408 * @throws IOException on error 409 */ 410 public int sendData(final String command) throws IOException { 411 return sendCommandWithID(null, command, null); 412 } 413 414 /** 415 * Sets the current chunk listener. If a listener is registered and the implementation returns true, then any registered 416 * {@link org.apache.commons.net.PrintCommandListener PrintCommandListener} instances will be invoked with the partial response and a status of 417 * {@link IMAPReply#PARTIAL} to indicate that the final reply code is not yet known. 418 * 419 * @param listener the class to use, or {@code null} to disable 420 * @see #TRUE_CHUNK_LISTENER 421 * @since 3.4 422 */ 423 public void setChunkListener(final IMAPChunkListener listener) { 424 chunkListener = listener; 425 } 426 427 /** 428 * Sets IMAP client state. This must be one of the <code>_STATE</code> constants. 429 * 430 * @param state The new state. 431 */ 432 protected void setState(final IMAP.IMAPState state) { 433 this.state = state; 434 } 435 } 436 /* kate: indent-width 4; replace-tabs on; */