    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 encoding.
    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(final 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<>();
    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(final 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                    final IMAPChunkListener il = chunkListener;
    170                    if (il != null) {
    171                        final 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(final int replyCode, final 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        final 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(final IMAP.IMAPState state)
    241    {
    242        this.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(final String commandID, final String command, final String args) throws IOException
    284    {
    285        final 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        final 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(final String command, final 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(final 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(final IMAPCommand command, final 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(final IMAPCommand command, final 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(final 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(final 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(final 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        final StringBuilder buffer = new StringBuilder(256);
    423        for (final 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(final 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        final 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    /**
    472     * Quote an input string if necessary.
    473     * If the string is enclosed in double-quotes it is assumed
    474     * to be quoted already and is returned unchanged.
    475     * If it is the empty string, "" is returned.
    476     * If it contains a space
    477     * then it is enclosed in double quotes, escaping the
    478     * characters backslash and double-quote.
    479     *
    480     * @param input the value to be quoted, may be null
    481     * @return the quoted value
    482     */
    483    static String quoteMailboxName(final String input) {
    484        if (input == null) { // Don't throw NPE here
    485            return null;
    486        }
    487        if (input.isEmpty()) {
    488            return "\"\""; // return the string ""
    489        }
    490        // Length check is necessary to ensure a lone double-quote is quoted
    491        if (input.length() > 1 && input.startsWith("\"") && input.endsWith("\"")) {
    492            return input; // Assume already quoted
    493        }
    494        if (input.contains(" ")) {
    495            // quoted strings must escape \ and "
    496            return "\"" + input.replaceAll("([\\\\\"])", "\\\\$1") + "\"";
    497        }
    498        return input;
    499
    500    }
    501}
    502/* kate: indent-width 4; replace-tabs on; */