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