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