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