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         public boolean chunkReceived(IMAP imap) {
105             return true;
106         }
107 
108     };
109     private volatile IMAPChunkListener __chunkListener;
110 
111     private final char[] _initialID = { 'A', 'A', 'A', 'A' };
112 
113     /**
114      * The default IMAPClient constructor.  Initializes the state
115      * to <code>DISCONNECTED_STATE</code>.
116      */
117     public IMAP()
118     {
119         setDefaultPort(DEFAULT_PORT);
120         __state = IMAPState.DISCONNECTED_STATE;
121         _reader = null;
122         __writer = null;
123         _replyLines = new ArrayList<String>();
124         createCommandSupport();
125     }
126 
127     /**
128      * Get the reply for a command that expects a tagged response.
129      *
130      * @throws IOException
131      */
132     private void __getReply() throws IOException
133     {
134         __getReply(true); // tagged response
135     }
136 
137     /**
138      * Get the reply for a command, reading the response until the
139      * reply is found.
140      *
141      * @param wantTag {@code true} if the command expects a tagged response.
142      * @throws IOException
143      */
144     private void __getReply(boolean wantTag) throws IOException
145     {
146         _replyLines.clear();
147         String line = _reader.readLine();
148 
149         if (line == null) {
150             throw new EOFException("Connection closed without indication.");
151         }
152 
153         _replyLines.add(line);
154 
155         if (wantTag) {
156             while(IMAPReply.isUntagged(line)) {
157                 int literalCount = IMAPReply.literalCount(line);
158                 final boolean isMultiLine = literalCount >= 0;
159                 while (literalCount >= 0) {
160                     line=_reader.readLine();
161                     if (line == null) {
162                         throw new EOFException("Connection closed without indication.");
163                     }
164                     _replyLines.add(line);
165                     literalCount -= (line.length() + 2); // Allow for CRLF
166                 }
167                 if (isMultiLine) {
168                     IMAPChunkListener il = __chunkListener;
169                     if (il != null) {
170                         boolean clear = il.chunkReceived(this);
171                         if (clear) {
172                             fireReplyReceived(IMAPReply.PARTIAL, getReplyString());
173                             _replyLines.clear();
174                         }
175                     }
176                 }
177                 line = _reader.readLine(); // get next chunk or final tag
178                 if (line == null) {
179                     throw new EOFException("Connection closed without indication.");
180                 }
181                 _replyLines.add(line);
182             }
183             // check the response code on the last line
184             _replyCode = IMAPReply.getReplyCode(line);
185         } else {
186             _replyCode = IMAPReply.getUntaggedReplyCode(line);
187         }
188 
189         fireReplyReceived(_replyCode, getReplyString());
190     }
191 
192     /**
193      * Overrides {@link SocketClient#fireReplyReceived(int, String)} so as to
194      * avoid creating the reply string if there are no listeners to invoke.
195      *
196      * @param replyCode passed to the listeners
197      * @param ignored the string is only created if there are listeners defined.
198      * @see #getReplyString()
199      * @since 3.4
200      */
201     @Override
202     protected void fireReplyReceived(int replyCode, String ignored) {
203         if (getCommandSupport().getListenerCount() > 0) {
204             getCommandSupport().fireReplyReceived(replyCode, getReplyString());
205         }
206     }
207 
208     /**
209      * Performs connection initialization and sets state to
210      * {@link IMAPState#NOT_AUTH_STATE}.
211      */
212     @Override
213     protected void _connectAction_() throws IOException
214     {
215         super._connectAction_();
216         _reader =
217           new CRLFLineReader(new InputStreamReader(_input_,
218                                                    __DEFAULT_ENCODING));
219         __writer =
220           new BufferedWriter(new OutputStreamWriter(_output_,
221                                                     __DEFAULT_ENCODING));
222         int tmo = getSoTimeout();
223         if (tmo <= 0) { // none set currently
224             setSoTimeout(connectTimeout); // use connect timeout to ensure we don't block forever
225         }
226         __getReply(false); // untagged response
227         if (tmo <= 0) {
228             setSoTimeout(tmo); // restore the original value
229         }
230         setState(IMAPState.NOT_AUTH_STATE);
231     }
232 
233     /**
234      * Sets IMAP client state.  This must be one of the
235      * <code>_STATE</code> constants.
236      *
237      * @param state  The new state.
238      */
239     protected void setState(IMAP.IMAPState state)
240     {
241         __state = state;
242     }
243 
244 
245     /**
246      * Returns the current IMAP client state.
247      *
248      * @return The current IMAP client state.
249      */
250     public IMAP.IMAPState getState()
251     {
252         return __state;
253     }
254 
255     /**
256      * Disconnects the client from the server, and sets the state to
257      * <code> DISCONNECTED_STATE </code>.  The reply text information
258      * from the last issued command is voided to allow garbage collection
259      * of the memory used to store that information.
260      *
261      * @exception IOException  If there is an error in disconnecting.
262      */
263     @Override
264     public void disconnect() throws IOException
265     {
266         super.disconnect();
267         _reader = null;
268         __writer = null;
269         _replyLines.clear();
270         setState(IMAPState.DISCONNECTED_STATE);
271     }
272 
273 
274     /**
275      * Sends a command an arguments to the server and returns the reply code.
276      *
277      * @param commandID The ID (tag) of the command.
278      * @param command  The IMAP command to send.
279      * @param args     The command arguments.
280      * @return  The server reply code (either IMAPReply.OK, IMAPReply.NO or IMAPReply.BAD).
281      */
282     private int sendCommandWithID(String commandID, String command, String args) throws IOException
283     {
284         StringBuilder __commandBuffer = new StringBuilder();
285         if (commandID != null)
286         {
287             __commandBuffer.append(commandID);
288             __commandBuffer.append(' ');
289         }
290         __commandBuffer.append(command);
291 
292         if (args != null)
293         {
294             __commandBuffer.append(' ');
295             __commandBuffer.append(args);
296         }
297         __commandBuffer.append(SocketClient.NETASCII_EOL);
298 
299         String message = __commandBuffer.toString();
300         __writer.write(message);
301         __writer.flush();
302 
303         fireCommandSent(command, message);
304 
305         __getReply();
306         return _replyCode;
307     }
308 
309     /**
310      * Sends a command an arguments to the server and returns the reply code.
311      *
312      * @param command  The IMAP command to send.
313      * @param args     The command arguments.
314      * @return  The server reply code (see IMAPReply).
315      * @throws IOException on error
316      */
317     public int sendCommand(String command, String args) throws IOException
318     {
319         return sendCommandWithID(generateCommandID(), command, args);
320     }
321 
322     /**
323      * Sends a command with no arguments to the server and returns the
324      * reply code.
325      *
326      * @param command  The IMAP command to send.
327      * @return  The server reply code (see IMAPReply).
328      * @throws IOException on error
329      */
330     public int sendCommand(String command) throws IOException
331     {
332         return sendCommand(command, null);
333     }
334 
335     /**
336      * Sends a command and arguments to the server and returns the reply code.
337      *
338      * @param command  The IMAP command to send
339      *                  (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(IMAPCommand command, String args) throws IOException
345     {
346         return sendCommand(command.getIMAPCommand(), args);
347     }
348 
349     /**
350      * Sends a command and arguments to the server and return whether successful.
351      *
352      * @param command  The IMAP command to send
353      *                  (one of the IMAPCommand constants).
354      * @param args     The command arguments.
355      * @return  {@code true} if the command was successful
356      * @throws IOException on error
357      */
358     public boolean doCommand(IMAPCommand command, String args) throws IOException
359     {
360         return IMAPReply.isSuccess(sendCommand(command, args));
361     }
362 
363     /**
364      * Sends a command with no arguments to the server and returns the
365      * reply code.
366      *
367      * @param command  The IMAP command to send
368      *                  (one of the IMAPCommand constants).
369      * @return  The server reply code (see IMAPReply).
370      * @throws IOException on error
371     **/
372     public int sendCommand(IMAPCommand command) throws IOException
373     {
374         return sendCommand(command, null);
375     }
376 
377     /**
378      * Sends a command to the server and return whether successful.
379      *
380      * @param command  The IMAP command to send
381      *                  (one of the IMAPCommand constants).
382      * @return  {@code true} if the command was successful
383      * @throws IOException on error
384      */
385     public boolean doCommand(IMAPCommand command) throws IOException
386     {
387         return IMAPReply.isSuccess(sendCommand(command));
388     }
389 
390     /**
391      * Sends data to the server and returns the reply code.
392      *
393      * @param command  The IMAP command to send.
394      * @return  The server reply code (see IMAPReply).
395      * @throws IOException on error
396      */
397     public int sendData(String command) throws IOException
398     {
399         return sendCommandWithID(null, command, null);
400     }
401 
402     /**
403      * Returns an array of lines received as a reply to the last command
404      * sent to the server.  The lines have end of lines truncated.
405      * @return The last server response.
406      */
407     public String[] getReplyStrings()
408     {
409         return _replyLines.toArray(new String[_replyLines.size()]);
410     }
411 
412     /**
413      * Returns the reply to the last command sent to the server.
414      * The value is a single string containing all the reply lines including
415      * newlines.
416      *
417      * @return The last server response.
418      */
419     public String getReplyString()
420     {
421         StringBuilder buffer = new StringBuilder(256);
422         for (String s : _replyLines)
423         {
424             buffer.append(s);
425             buffer.append(SocketClient.NETASCII_EOL);
426         }
427 
428         return buffer.toString();
429     }
430 
431     /**
432      * Sets the current chunk listener.
433      * If a listener is registered and the implementation returns true,
434      * then any registered
435      * {@link org.apache.commons.net.PrintCommandListener PrintCommandListener}
436      * instances will be invoked with the partial response and a status of
437      * {@link IMAPReply#PARTIAL} to indicate that the final reply code is not yet known.
438      * @param listener the class to use, or {@code null} to disable
439      * @see #TRUE_CHUNK_LISTENER
440      * @since 3.4
441      */
442     public void setChunkListener(IMAPChunkListener listener) {
443         __chunkListener = listener;
444     }
445 
446     /**
447      * Generates a new command ID (tag) for a command.
448      * @return a new command ID (tag) for an IMAP command.
449      */
450     protected String generateCommandID()
451     {
452         String res = new String (_initialID);
453         // "increase" the ID for the next call
454         boolean carry = true; // want to increment initially
455         for (int i = _initialID.length-1; carry && i>=0; i--)
456         {
457             if (_initialID[i] == 'Z')
458             {
459                 _initialID[i] = 'A';
460             }
461             else
462             {
463                 _initialID[i]++;
464                 carry = false; // did not wrap round
465             }
466         }
467         return res;
468     }
469 }
470 /* kate: indent-width 4; replace-tabs on; */