    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; */