View Javadoc
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