001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.net.imap;
019
020import java.io.BufferedReader;
021import java.io.BufferedWriter;
022import java.io.EOFException;
023import java.io.InputStreamReader;
024import java.io.IOException;
025import java.io.OutputStreamWriter;
026import java.util.ArrayList;
027import java.util.List;
028
029import org.apache.commons.net.SocketClient;
030import org.apache.commons.net.io.CRLFLineReader;
031
032
033/**
034 * The IMAP class provides the basic the functionality necessary to implement your
035 * own IMAP client.
036 */
037public class IMAP extends SocketClient
038{
039    /** The default IMAP port (RFC 3501). */
040    public static final int DEFAULT_PORT = 143;
041
042    public enum IMAPState
043    {
044        /** A constant representing the state where the client is not yet connected to a server. */
045        DISCONNECTED_STATE,
046        /**  A constant representing the "not authenticated" state. */
047        NOT_AUTH_STATE,
048        /**  A constant representing the "authenticated" state. */
049        AUTH_STATE,
050        /**  A constant representing the "logout" state. */
051        LOGOUT_STATE;
052    }
053
054    // RFC 3501, section 5.1.3. It should be "modified UTF-7".
055    /**
056     * The default control socket ecoding.
057     */
058    protected static final String __DEFAULT_ENCODING = "ISO-8859-1";
059
060    private IMAPState __state;
061    protected BufferedWriter __writer;
062
063    protected BufferedReader _reader;
064    private int _replyCode;
065    private final List<String> _replyLines;
066
067    /**
068     * Implement this interface and register it via {@link #setChunkListener(IMAPChunkListener)}
069     * in order to get access to multi-line partial command responses.
070     * Useful when processing large FETCH responses.
071     */
072    public interface IMAPChunkListener {
073        /**
074         * Called when a multi-line partial response has been received.
075         * @param imap the instance, get the response
076         * by calling {@link #getReplyString()} or {@link #getReplyStrings()}
077         * @return {@code true} if the reply buffer is to be cleared on return
078         */
079        boolean chunkReceived(IMAP imap);
080    }
081
082    /**
083     * <p>
084     * Implementation of IMAPChunkListener that returns {@code true}
085     * but otherwise does nothing.
086     * </p>
087     * <p>
088     * This is intended for use with a suitable ProtocolCommandListener.
089     * If the IMAP response contains multiple-line data, the protocol listener
090     * will be called for each multi-line chunk.
091     * The accumulated reply data will be cleared after calling the listener.
092     * If the response is very long, this can significantly reduce memory requirements.
093     * The listener will also start receiving response data earlier, as it does not have
094     * to wait for the entire response to be read.
095     * </p>
096     * <p>
097     * The ProtocolCommandListener must be prepared to accept partial responses.
098     * This should not be a problem for listeners that just log the input.
099     * </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     * @throws 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; */