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