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