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