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