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    
018    package org.apache.commons.net.imap;
019    
020    import java.io.BufferedReader;
021    import java.io.BufferedWriter;
022    import java.io.EOFException;
023    import java.io.InputStreamReader;
024    import java.io.IOException;
025    import java.io.OutputStreamWriter;
026    import java.util.ArrayList;
027    import java.util.List;
028    
029    import org.apache.commons.net.SocketClient;
030    import 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     */
037    public 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                    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; */