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 * https://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.nio.charset.StandardCharsets;
27 import java.util.ArrayList;
28 import java.util.List;
29
30 import org.apache.commons.net.SocketClient;
31 import org.apache.commons.net.io.CRLFLineReader;
32 import org.apache.commons.net.util.NetConstants;
33
34 /**
35 * The IMAP class provides the basic the functionality necessary to implement your own IMAP client.
36 */
37 public class IMAP extends SocketClient {
38
39 /**
40 * Implement this interface and register it via {@link #setChunkListener(IMAPChunkListener)} in order to get access to multi-line partial command responses.
41 * Useful when processing large FETCH responses.
42 */
43 public interface IMAPChunkListener {
44
45 /**
46 * Called when a multi-line partial response has been received.
47 *
48 * @param imap the instance, get the response by calling {@link #getReplyString()} or {@link #getReplyStrings()}
49 * @return {@code true} if the reply buffer is to be cleared on return
50 */
51 boolean chunkReceived(IMAP imap);
52 }
53
54 /**
55 * Enumerates IMAP states.
56 */
57 public enum IMAPState {
58
59 /** A constant representing the state where the client is not yet connected to a server. */
60 DISCONNECTED_STATE,
61
62 /** A constant representing the "not authenticated" state. */
63 NOT_AUTH_STATE,
64
65 /** A constant representing the "authenticated" state. */
66 AUTH_STATE,
67
68 /** A constant representing the "logout" state. */
69 LOGOUT_STATE
70 }
71
72 /** The default IMAP port (RFC 3501). */
73 public static final int DEFAULT_PORT = 143;
74
75 /**
76 * The default control socket encoding.
77 */
78 protected static final String __DEFAULT_ENCODING = StandardCharsets.ISO_8859_1.name();
79
80 /**
81 * <p>
82 * Implements {@link IMAPChunkListener} to returns {@code true} but otherwise does nothing.
83 * </p>
84 * <p>
85 * This is intended for use with a suitable ProtocolCommandListener. If the IMAP response contains multiple-line data, the protocol listener will be called
86 * 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
87 * 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.
88 * </p>
89 * <p>
90 * The ProtocolCommandListener must be prepared to accept partial responses. This should not be a problem for listeners that just log the input.
91 * </p>
92 *
93 * @see #setChunkListener(IMAPChunkListener)
94 * @since 3.4
95 */
96 public static final IMAPChunkListener TRUE_CHUNK_LISTENER = imap -> true;
97
98 /**
99 * 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