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 * Implement this interface and register it via {@link #setChunkListener(IMAPChunkListener)} in order to get access to multi-line partial command responses.
40 * Useful when processing large FETCH responses.
41 */
42 public interface IMAPChunkListener {
43 /**
44 * Called when a multi-line partial response has been received.
45 *
46 * @param imap the instance, get the response by calling {@link #getReplyString()} or {@link #getReplyStrings()}
47 * @return {@code true} if the reply buffer is to be cleared on return
48 */
49 boolean chunkReceived(IMAP imap);
50 }
51
52 /**
53 * Enumerates IMAP states.
54 */
55 public enum IMAPState {
56 /** A constant representing the state where the client is not yet connected to a server. */
57 DISCONNECTED_STATE,
58 /** A constant representing the "not authenticated" state. */
59 NOT_AUTH_STATE,
60 /** A constant representing the "authenticated" state. */
61 AUTH_STATE,
62 /** A constant representing the "logout" state. */
63 LOGOUT_STATE
64 }
65
66 /** The default IMAP port (RFC 3501). */
67 public static final int DEFAULT_PORT = 143;
68
69 // RFC 3501, section 5.1.3. It should be "modified UTF-7".
70 /**
71 * The default control socket encoding.
72 */
73 protected static final String __DEFAULT_ENCODING = StandardCharsets.ISO_8859_1.name();
74
75 /**
76 * <p>
77 * Implements {@link IMAPChunkListener} to returns {@code true} but otherwise does nothing.
78 * </p>
79 * <p>
80 * This is intended for use with a suitable ProtocolCommandListener. If the IMAP response contains multiple-line data, the protocol listener will be called
81 * 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
82 * 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.
83 * </p>
84 * <p>
85 * The ProtocolCommandListener must be prepared to accept partial responses. This should not be a problem for listeners that just log the input.
86 * </p>
87 *
88 * @see #setChunkListener(IMAPChunkListener)
89 * @since 3.4
90 */
91 public static final IMAPChunkListener TRUE_CHUNK_LISTENER = imap -> true;
92
93 /**
94 * 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
95 * empty string, "" is returned. If it contains a space then it is enclosed in double quotes, escaping the characters backslash and double-quote.
96 *
97 * @param input the value to be quoted, may be null
98 * @return the quoted value
99 */
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