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