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