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.IOException;
024import java.io.InputStreamReader;
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;
031import org.apache.commons.net.util.NetConstants;
032
033
034/**
035 * The IMAP class provides the basic the functionality necessary to implement your
036 * own IMAP client.
037 */
038public class IMAP extends SocketClient
039{
040    /** The default IMAP port (RFC 3501). */
041    public static final int DEFAULT_PORT = 143;
042
043    public enum IMAPState
044    {
045        /** A constant representing the state where the client is not yet connected to a server. */
046        DISCONNECTED_STATE,
047        /**  A constant representing the "not authenticated" state. */
048        NOT_AUTH_STATE,
049        /**  A constant representing the "authenticated" state. */
050        AUTH_STATE,
051        /**  A constant representing the "logout" state. */
052        LOGOUT_STATE
053    }
054
055    // RFC 3501, section 5.1.3. It should be "modified UTF-7".
056    /**
057     * The default control socket encoding.
058     */
059    protected static final String __DEFAULT_ENCODING = "ISO-8859-1";
060
061    private IMAPState state;
062    protected BufferedWriter __writer;
063
064    protected BufferedReader _reader;
065    private int replyCode;
066    private final List<String> replyLines;
067
068    /**
069     * Implement this interface and register it via {@link #setChunkListener(IMAPChunkListener)}
070     * in order to get access to multi-line partial command responses.
071     * Useful when processing large FETCH responses.
072     */
073    public interface IMAPChunkListener {
074        /**
075         * Called when a multi-line partial response has been received.
076         * @param imap the instance, get the response
077         * by calling {@link #getReplyString()} or {@link #getReplyStrings()}
078         * @return {@code true} if the reply buffer is to be cleared on return
079         */
080        boolean chunkReceived(IMAP imap);
081    }
082
083    /**
084     * <p>
085     * Implementation of IMAPChunkListener that returns {@code true}
086     * but otherwise does nothing.
087     * </p>
088     * <p>
089     * This is intended for use with a suitable ProtocolCommandListener.
090     * If the IMAP response contains multiple-line data, the protocol listener
091     * will be called for each multi-line chunk.
092     * The accumulated reply data will be cleared after calling the listener.
093     * If the response is very long, this can significantly reduce memory requirements.
094     * The listener will also start receiving response data earlier, as it does not have
095     * to wait for the entire response to be read.
096     * </p>
097     * <p>
098     * The ProtocolCommandListener must be prepared to accept partial responses.
099     * This should not be a problem for listeners that just log the input.
100     * </p>
101     * @see #setChunkListener(IMAPChunkListener)
102     * @since 3.4
103     */
104    public static final IMAPChunkListener TRUE_CHUNK_LISTENER = new IMAPChunkListener(){
105        @Override
106        public boolean chunkReceived(final IMAP imap) {
107            return true;
108        }
109
110    };
111    private volatile IMAPChunkListener chunkListener;
112
113    private final char[] initialID = { 'A', 'A', 'A', 'A' };
114
115    /**
116     * The default IMAPClient constructor.  Initializes the state
117     * to <code>DISCONNECTED_STATE</code>.
118     */
119    public IMAP()
120    {
121        setDefaultPort(DEFAULT_PORT);
122        state = IMAPState.DISCONNECTED_STATE;
123        _reader = null;
124        __writer = null;
125        replyLines = new ArrayList<>();
126        createCommandSupport();
127    }
128
129    /**
130     * Get the reply for a command that expects a tagged response.
131     *
132     * @throws IOException
133     */
134    private void getReply() throws IOException
135    {
136        getReply(true); // tagged response
137    }
138
139    /**
140     * Get the reply for a command, reading the response until the
141     * reply is found.
142     *
143     * @param wantTag {@code true} if the command expects a tagged response.
144     * @throws IOException
145     */
146    private void getReply(final boolean wantTag) throws IOException
147    {
148        replyLines.clear();
149        String line = _reader.readLine();
150
151        if (line == null) {
152            throw new EOFException("Connection closed without indication.");
153        }
154
155        replyLines.add(line);
156
157        if (wantTag) {
158            while(IMAPReply.isUntagged(line)) {
159                int literalCount = IMAPReply.literalCount(line);
160                final boolean isMultiLine = literalCount >= 0;
161                while (literalCount >= 0) {
162                    line=_reader.readLine();
163                    if (line == null) {
164                        throw new EOFException("Connection closed without indication.");
165                    }
166                    replyLines.add(line);
167                    literalCount -= line.length() + 2; // Allow for CRLF
168                }
169                if (isMultiLine) {
170                    final IMAPChunkListener il = chunkListener;
171                    if (il != null) {
172                        final boolean clear = il.chunkReceived(this);
173                        if (clear) {
174                            fireReplyReceived(IMAPReply.PARTIAL, getReplyString());
175                            replyLines.clear();
176                        }
177                    }
178                }
179                line = _reader.readLine(); // get next chunk or final tag
180                if (line == null) {
181                    throw new EOFException("Connection closed without indication.");
182                }
183                replyLines.add(line);
184            }
185            // check the response code on the last line
186            replyCode = IMAPReply.getReplyCode(line);
187        } else {
188            replyCode = IMAPReply.getUntaggedReplyCode(line);
189        }
190
191        fireReplyReceived(replyCode, getReplyString());
192    }
193
194    /**
195     * Overrides {@link SocketClient#fireReplyReceived(int, String)} so as to
196     * avoid creating the reply string if there are no listeners to invoke.
197     *
198     * @param replyCode passed to the listeners
199     * @param ignored the string is only created if there are listeners defined.
200     * @see #getReplyString()
201     * @since 3.4
202     */
203    @Override
204    protected void fireReplyReceived(final int replyCode, final String ignored) {
205        if (getCommandSupport().getListenerCount() > 0) {
206            getCommandSupport().fireReplyReceived(replyCode, getReplyString());
207        }
208    }
209
210    /**
211     * Performs connection initialization and sets state to
212     * {@link IMAPState#NOT_AUTH_STATE}.
213     */
214    @Override
215    protected void _connectAction_() throws IOException
216    {
217        super._connectAction_();
218        _reader =
219          new CRLFLineReader(new InputStreamReader(_input_,
220                                                   __DEFAULT_ENCODING));
221        __writer =
222          new BufferedWriter(new OutputStreamWriter(_output_,
223                                                    __DEFAULT_ENCODING));
224        final int tmo = getSoTimeout();
225        if (tmo <= 0) { // none set currently
226            setSoTimeout(connectTimeout); // use connect timeout to ensure we don't block forever
227        }
228        getReply(false); // untagged response
229        if (tmo <= 0) {
230            setSoTimeout(tmo); // restore the original value
231        }
232        setState(IMAPState.NOT_AUTH_STATE);
233    }
234
235    /**
236     * Sets IMAP client state.  This must be one of the
237     * <code>_STATE</code> constants.
238     *
239     * @param state  The new state.
240     */
241    protected void setState(final IMAP.IMAPState state)
242    {
243        this.state = state;
244    }
245
246
247    /**
248     * Returns the current IMAP client state.
249     *
250     * @return The current IMAP client state.
251     */
252    public IMAP.IMAPState getState()
253    {
254        return state;
255    }
256
257    /**
258     * Disconnects the client from the server, and sets the state to
259     * <code> DISCONNECTED_STATE </code>.  The reply text information
260     * from the last issued command is voided to allow garbage collection
261     * of the memory used to store that information.
262     *
263     * @throws IOException  If there is an error in disconnecting.
264     */
265    @Override
266    public void disconnect() throws IOException
267    {
268        super.disconnect();
269        _reader = null;
270        __writer = null;
271        replyLines.clear();
272        setState(IMAPState.DISCONNECTED_STATE);
273    }
274
275
276    /**
277     * Sends a command an arguments to the server and returns the reply code.
278     *
279     * @param commandID The ID (tag) of the command.
280     * @param command  The IMAP command to send.
281     * @param args     The command arguments.
282     * @return  The server reply code (either IMAPReply.OK, IMAPReply.NO or IMAPReply.BAD).
283     */
284    private int sendCommandWithID(final String commandID, final String command, final String args) throws IOException
285    {
286        final StringBuilder __commandBuffer = new StringBuilder();
287        if (commandID != null)
288        {
289            __commandBuffer.append(commandID);
290            __commandBuffer.append(' ');
291        }
292        __commandBuffer.append(command);
293
294        if (args != null)
295        {
296            __commandBuffer.append(' ');
297            __commandBuffer.append(args);
298        }
299        __commandBuffer.append(SocketClient.NETASCII_EOL);
300
301        final String message = __commandBuffer.toString();
302        __writer.write(message);
303        __writer.flush();
304
305        fireCommandSent(command, message);
306
307        getReply();
308        return replyCode;
309    }
310
311    /**
312     * Sends a command an arguments to the server and returns the reply code.
313     *
314     * @param command  The IMAP command to send.
315     * @param args     The command arguments.
316     * @return  The server reply code (see IMAPReply).
317     * @throws IOException on error
318     */
319    public int sendCommand(final String command, final String args) throws IOException
320    {
321        return sendCommandWithID(generateCommandID(), command, args);
322    }
323
324    /**
325     * Sends a command with no arguments to the server and returns the
326     * reply code.
327     *
328     * @param command  The IMAP command to send.
329     * @return  The server reply code (see IMAPReply).
330     * @throws IOException on error
331     */
332    public int sendCommand(final String command) throws IOException
333    {
334        return sendCommand(command, null);
335    }
336
337    /**
338     * Sends a command and arguments to the server and returns the reply code.
339     *
340     * @param command  The IMAP command to send
341     *                  (one of the IMAPCommand constants).
342     * @param args     The command arguments.
343     * @return  The server reply code (see IMAPReply).
344     * @throws IOException on error
345     */
346    public int sendCommand(final IMAPCommand command, final String args) throws IOException
347    {
348        return sendCommand(command.getIMAPCommand(), args);
349    }
350
351    /**
352     * Sends a command and arguments to the server and return whether successful.
353     *
354     * @param command  The IMAP command to send
355     *                  (one of the IMAPCommand constants).
356     * @param args     The command arguments.
357     * @return  {@code true} if the command was successful
358     * @throws IOException on error
359     */
360    public boolean doCommand(final IMAPCommand command, final String args) throws IOException
361    {
362        return IMAPReply.isSuccess(sendCommand(command, args));
363    }
364
365    /**
366     * Sends a command with no arguments to the server and returns the
367     * reply code.
368     *
369     * @param command  The IMAP command to send
370     *                  (one of the IMAPCommand constants).
371     * @return  The server reply code (see IMAPReply).
372     * @throws IOException on error
373    **/
374    public int sendCommand(final IMAPCommand command) throws IOException
375    {
376        return sendCommand(command, null);
377    }
378
379    /**
380     * Sends a command to the server and return whether successful.
381     *
382     * @param command  The IMAP command to send
383     *                  (one of the IMAPCommand constants).
384     * @return  {@code true} if the command was successful
385     * @throws IOException on error
386     */
387    public boolean doCommand(final IMAPCommand command) throws IOException
388    {
389        return IMAPReply.isSuccess(sendCommand(command));
390    }
391
392    /**
393     * Sends data to the server and returns the reply code.
394     *
395     * @param command  The IMAP command to send.
396     * @return  The server reply code (see IMAPReply).
397     * @throws IOException on error
398     */
399    public int sendData(final String command) throws IOException
400    {
401        return sendCommandWithID(null, command, null);
402    }
403
404    /**
405     * Returns an array of lines received as a reply to the last command
406     * sent to the server.  The lines have end of lines truncated.
407     * @return The last server response.
408     */
409    public String[] getReplyStrings()
410    {
411        return replyLines.toArray(NetConstants.EMPTY_STRING_ARRAY);
412    }
413
414    /**
415     * Returns the reply to the last command sent to the server.
416     * The value is a single string containing all the reply lines including
417     * newlines.
418     *
419     * @return The last server response.
420     */
421    public String getReplyString()
422    {
423        final StringBuilder buffer = new StringBuilder(256);
424        for (final String s : replyLines)
425        {
426            buffer.append(s);
427            buffer.append(SocketClient.NETASCII_EOL);
428        }
429
430        return buffer.toString();
431    }
432
433    /**
434     * Sets the current chunk listener.
435     * If a listener is registered and the implementation returns true,
436     * then any registered
437     * {@link org.apache.commons.net.PrintCommandListener PrintCommandListener}
438     * instances will be invoked with the partial response and a status of
439     * {@link IMAPReply#PARTIAL} to indicate that the final reply code is not yet known.
440     * @param listener the class to use, or {@code null} to disable
441     * @see #TRUE_CHUNK_LISTENER
442     * @since 3.4
443     */
444    public void setChunkListener(final IMAPChunkListener listener) {
445        chunkListener = listener;
446    }
447
448    /**
449     * Generates a new command ID (tag) for a command.
450     * @return a new command ID (tag) for an IMAP command.
451     */
452    protected String generateCommandID()
453    {
454        final String res = new String (initialID);
455        // "increase" the ID for the next call
456        boolean carry = true; // want to increment initially
457        for (int i = initialID.length-1; carry && i>=0; i--)
458        {
459            if (initialID[i] == 'Z')
460            {
461                initialID[i] = 'A';
462            }
463            else
464            {
465                initialID[i]++;
466                carry = false; // did not wrap round
467            }
468        }
469        return res;
470    }
471
472    /**
473     * Quote an input string if necessary.
474     * If the string is enclosed in double-quotes it is assumed
475     * to be quoted already and is returned unchanged.
476     * If it is the empty string, "" is returned.
477     * If it contains a space
478     * then it is enclosed in double quotes, escaping the
479     * characters backslash and double-quote.
480     *
481     * @param input the value to be quoted, may be null
482     * @return the quoted value
483     */
484    static String quoteMailboxName(final String input) {
485        if (input == null) { // Don't throw NPE here
486            return null;
487        }
488        if (input.isEmpty()) {
489            return "\"\""; // return the string ""
490        }
491        // Length check is necessary to ensure a lone double-quote is quoted
492        if (input.length() > 1 && input.startsWith("\"") && input.endsWith("\"")) {
493            return input; // Assume already quoted
494        }
495        if (input.contains(" ")) {
496            // quoted strings must escape \ and "
497            return "\"" + input.replaceAll("([\\\\\"])", "\\\\$1") + "\"";
498        }
499        return input;
500
501    }
502}
503/* kate: indent-width 4; replace-tabs on; */