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