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    private final char[] _initialID = { 'A', 'A', 'A', 'A' };
068
069    /**
070     * The default IMAPClient constructor.  Initializes the state
071     * to <code>DISCONNECTED_STATE</code>.
072     */
073    public IMAP()
074    {
075        setDefaultPort(DEFAULT_PORT);
076        __state = IMAPState.DISCONNECTED_STATE;
077        _reader = null;
078        __writer = null;
079        _replyLines = new ArrayList<String>();
080        createCommandSupport();
081    }
082
083    /**
084     * Get the reply for a command that expects a tagged response.
085     *
086     * @throws IOException
087     */
088    private void __getReply() throws IOException
089    {
090        __getReply(true); // tagged response
091    }
092
093    /**
094     * Get the reply for a command, reading the response until the
095     * reply is found.
096     *
097     * @param wantTag {@code true} if the command expects a tagged response.
098     * @throws IOException
099     */
100    private void __getReply(boolean wantTag) throws IOException
101    {
102        _replyLines.clear();
103        String line = _reader.readLine();
104
105        if (line == null) {
106            throw new EOFException("Connection closed without indication.");
107        }
108
109        _replyLines.add(line);
110
111        if (wantTag) {
112            while(IMAPReply.isUntagged(line)) {
113                int literalCount = IMAPReply.literalCount(line);
114                while (literalCount >= 0) {
115                    line=_reader.readLine();
116                    if (line == null) {
117                        throw new EOFException("Connection closed without indication.");
118                    }
119                    _replyLines.add(line);
120                    literalCount -= (line.length() + 2); // Allow for CRLF
121                }
122                line = _reader.readLine();
123                if (line == null) {
124                    throw new EOFException("Connection closed without indication.");
125                }
126                _replyLines.add(line);
127            }
128            // check the response code on the last line
129            _replyCode = IMAPReply.getReplyCode(line);
130        } else {
131            _replyCode = IMAPReply.getUntaggedReplyCode(line);
132        }
133
134        fireReplyReceived(_replyCode, getReplyString());
135    }
136
137    /**
138     * Performs connection initialization and sets state to
139     * {@link IMAPState#NOT_AUTH_STATE}.
140     */
141    @Override
142    protected void _connectAction_() throws IOException
143    {
144        super._connectAction_();
145        _reader =
146          new CRLFLineReader(new InputStreamReader(_input_,
147                                                   __DEFAULT_ENCODING));
148        __writer =
149          new BufferedWriter(new OutputStreamWriter(_output_,
150                                                    __DEFAULT_ENCODING));
151        int tmo = getSoTimeout();
152        if (tmo <= 0) { // none set currently
153            setSoTimeout(connectTimeout); // use connect timeout to ensure we don't block forever
154        }
155        __getReply(false); // untagged response
156        if (tmo <= 0) {
157            setSoTimeout(tmo); // restore the original value
158        }
159        setState(IMAPState.NOT_AUTH_STATE);
160    }
161
162    /**
163     * Sets IMAP client state.  This must be one of the
164     * <code>_STATE</code> constants.
165     * <p>
166     * @param state  The new state.
167     */
168    protected void setState(IMAP.IMAPState state)
169    {
170        __state = state;
171    }
172
173
174    /**
175     * Returns the current IMAP client state.
176     * <p>
177     * @return The current IMAP client state.
178     */
179    public IMAP.IMAPState getState()
180    {
181        return __state;
182    }
183
184    /**
185     * Disconnects the client from the server, and sets the state to
186     * <code> DISCONNECTED_STATE </code>.  The reply text information
187     * from the last issued command is voided to allow garbage collection
188     * of the memory used to store that information.
189     * <p>
190     * @exception IOException  If there is an error in disconnecting.
191     */
192    @Override
193    public void disconnect() throws IOException
194    {
195        super.disconnect();
196        _reader = null;
197        __writer = null;
198        _replyLines.clear();
199        setState(IMAPState.DISCONNECTED_STATE);
200    }
201
202
203    /**
204     * Sends a command an arguments to the server and returns the reply code.
205     * <p>
206     * @param commandID The ID (tag) of the command.
207     * @param command  The IMAP command to send.
208     * @param args     The command arguments.
209     * @return  The server reply code (either IMAPReply.OK, IMAPReply.NO or IMAPReply.BAD).
210     */
211    private int sendCommandWithID(String commandID, String command, String args) throws IOException
212    {
213        StringBuilder __commandBuffer = new StringBuilder();
214        if (commandID != null)
215        {
216            __commandBuffer.append(commandID);
217            __commandBuffer.append(' ');
218        }
219        __commandBuffer.append(command);
220
221        if (args != null)
222        {
223            __commandBuffer.append(' ');
224            __commandBuffer.append(args);
225        }
226        __commandBuffer.append(SocketClient.NETASCII_EOL);
227
228        String message = __commandBuffer.toString();
229        __writer.write(message);
230        __writer.flush();
231
232        fireCommandSent(command, message);
233
234        __getReply();
235        return _replyCode;
236    }
237
238    /**
239     * Sends a command an arguments to the server and returns the reply code.
240     * <p>
241     * @param command  The IMAP command to send.
242     * @param args     The command arguments.
243     * @return  The server reply code (see IMAPReply).
244     */
245    public int sendCommand(String command, String args) throws IOException
246    {
247        return sendCommandWithID(generateCommandID(), command, args);
248    }
249
250    /**
251     * Sends a command with no arguments to the server and returns the
252     * reply code.
253     * <p>
254     * @param command  The IMAP command to send.
255     * @return  The server reply code (see IMAPReply).
256     */
257    public int sendCommand(String command) throws IOException
258    {
259        return sendCommand(command, null);
260    }
261
262    /**
263     * Sends a command and arguments to the server and returns the reply code.
264     * <p>
265     * @param command  The IMAP command to send
266     *                  (one of the IMAPCommand constants).
267     * @param args     The command arguments.
268     * @return  The server reply code (see IMAPReply).
269     */
270    public int sendCommand(IMAPCommand command, String args) throws IOException
271    {
272        return sendCommand(command.getIMAPCommand(), args);
273    }
274
275    /**
276     * Sends a command and arguments to the server and return whether successful.
277     * <p>
278     * @param command  The IMAP command to send
279     *                  (one of the IMAPCommand constants).
280     * @param args     The command arguments.
281     * @return  {@code true} if the command was successful
282     */
283    public boolean doCommand(IMAPCommand command, String args) throws IOException
284    {
285        return IMAPReply.isSuccess(sendCommand(command, args));
286    }
287
288    /**
289     * Sends a command with no arguments to the server and returns the
290     * reply code.
291     *
292     * @param command  The IMAP command to send
293     *                  (one of the IMAPCommand constants).
294     * @return  The server reply code (see IMAPReply).
295    **/
296    public int sendCommand(IMAPCommand command) throws IOException
297    {
298        return sendCommand(command, null);
299    }
300
301    /**
302     * Sends a command to the server and return whether successful.
303     *
304     * @param command  The IMAP command to send
305     *                  (one of the IMAPCommand constants).
306     * @return  {@code true} if the command was successful
307     */
308    public boolean doCommand(IMAPCommand command) throws IOException
309    {
310        return IMAPReply.isSuccess(sendCommand(command));
311    }
312
313    /**
314     * Sends data to the server and returns the reply code.
315     * <p>
316     * @param command  The IMAP command to send.
317     * @return  The server reply code (see IMAPReply).
318     */
319    public int sendData(String command) throws IOException
320    {
321        return sendCommandWithID(null, command, null);
322    }
323
324    /**
325     * Returns an array of lines received as a reply to the last command
326     * sent to the server.  The lines have end of lines truncated.
327     * @return The last server response.
328     */
329    public String[] getReplyStrings()
330    {
331        return _replyLines.toArray(new String[_replyLines.size()]);
332    }
333
334    /**
335     * Returns the reply to the last command sent to the server.
336     * The value is a single string containing all the reply lines including
337     * newlines.
338     * <p>
339     * @return The last server response.
340     */
341    public String getReplyString()
342    {
343        StringBuilder buffer = new StringBuilder(256);
344        for (String s : _replyLines)
345        {
346            buffer.append(s);
347            buffer.append(SocketClient.NETASCII_EOL);
348        }
349
350        return buffer.toString();
351    }
352
353    /**
354     * Generates a new command ID (tag) for a command.
355     * @return a new command ID (tag) for an IMAP command.
356     */
357    protected String generateCommandID()
358    {
359        String res = new String (_initialID);
360        // "increase" the ID for the next call
361        boolean carry = true; // want to increment initially
362        for (int i = _initialID.length-1; carry && i>=0; i--)
363        {
364            if (_initialID[i] == 'Z')
365            {
366                _initialID[i] = 'A';
367            }
368            else
369            {
370                _initialID[i]++;
371                carry = false; // did not wrap round
372            }
373        }
374        return res;
375    }
376}
377/* kate: indent-width 4; replace-tabs on; */