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