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