IMAP.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */

  17. package org.apache.commons.net.imap;

  18. import java.io.BufferedReader;
  19. import java.io.BufferedWriter;
  20. import java.io.EOFException;
  21. import java.io.IOException;
  22. import java.io.InputStreamReader;
  23. import java.io.OutputStreamWriter;
  24. import java.nio.charset.StandardCharsets;
  25. import java.util.ArrayList;
  26. import java.util.List;

  27. import org.apache.commons.net.SocketClient;
  28. import org.apache.commons.net.io.CRLFLineReader;
  29. import org.apache.commons.net.util.NetConstants;

  30. /**
  31.  * The IMAP class provides the basic the functionality necessary to implement your own IMAP client.
  32.  */
  33. public class IMAP extends SocketClient {
  34.     /**
  35.      * Implement this interface and register it via {@link #setChunkListener(IMAPChunkListener)} in order to get access to multi-line partial command responses.
  36.      * Useful when processing large FETCH responses.
  37.      */
  38.     public interface IMAPChunkListener {
  39.         /**
  40.          * Called when a multi-line partial response has been received.
  41.          *
  42.          * @param imap the instance, get the response by calling {@link #getReplyString()} or {@link #getReplyStrings()}
  43.          * @return {@code true} if the reply buffer is to be cleared on return
  44.          */
  45.         boolean chunkReceived(IMAP imap);
  46.     }

  47.     public enum IMAPState {
  48.         /** A constant representing the state where the client is not yet connected to a server. */
  49.         DISCONNECTED_STATE,
  50.         /** A constant representing the "not authenticated" state. */
  51.         NOT_AUTH_STATE,
  52.         /** A constant representing the "authenticated" state. */
  53.         AUTH_STATE,
  54.         /** A constant representing the "logout" state. */
  55.         LOGOUT_STATE
  56.     }

  57.     /** The default IMAP port (RFC 3501). */
  58.     public static final int DEFAULT_PORT = 143;

  59.     // RFC 3501, section 5.1.3. It should be "modified UTF-7".
  60.     /**
  61.      * The default control socket encoding.
  62.      */
  63.     protected static final String __DEFAULT_ENCODING = StandardCharsets.ISO_8859_1.name();

  64.     /**
  65.      * <p>
  66.      * Implements {@link IMAPChunkListener} to returns {@code true} but otherwise does nothing.
  67.      * </p>
  68.      * <p>
  69.      * This is intended for use with a suitable ProtocolCommandListener. If the IMAP response contains multiple-line data, the protocol listener will be called
  70.      * 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
  71.      * 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.
  72.      * </p>
  73.      * <p>
  74.      * The ProtocolCommandListener must be prepared to accept partial responses. This should not be a problem for listeners that just log the input.
  75.      * </p>
  76.      *
  77.      * @see #setChunkListener(IMAPChunkListener)
  78.      * @since 3.4
  79.      */
  80.     public static final IMAPChunkListener TRUE_CHUNK_LISTENER = imap -> true;

  81.     /**
  82.      * 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
  83.      * empty string, "" is returned. If it contains a space then it is enclosed in double quotes, escaping the characters backslash and double-quote.
  84.      *
  85.      * @param input the value to be quoted, may be null
  86.      * @return the quoted value
  87.      */
  88.     static String quoteMailboxName(final String input) {
  89.         if (input == null) { // Don't throw NPE here
  90.             return null;
  91.         }
  92.         if (input.isEmpty()) {
  93.             return "\"\""; // return the string ""
  94.         }
  95.         // Length check is necessary to ensure a lone double-quote is quoted
  96.         if (input.length() > 1 && input.startsWith("\"") && input.endsWith("\"")) {
  97.             return input; // Assume already quoted
  98.         }
  99.         if (input.contains(" ")) {
  100.             // quoted strings must escape \ and "
  101.             return "\"" + input.replaceAll("([\\\\\"])", "\\\\$1") + "\"";
  102.         }
  103.         return input;

  104.     }

  105.     private IMAPState state;
  106.     protected BufferedWriter __writer;

  107.     protected BufferedReader _reader;

  108.     private int replyCode;
  109.     private final List<String> replyLines;

  110.     private volatile IMAPChunkListener chunkListener;

  111.     private final char[] initialID = { 'A', 'A', 'A', 'A' };

  112.     /**
  113.      * The default IMAPClient constructor. Initializes the state to <code>DISCONNECTED_STATE</code>.
  114.      */
  115.     public IMAP() {
  116.         setDefaultPort(DEFAULT_PORT);
  117.         state = IMAPState.DISCONNECTED_STATE;
  118.         _reader = null;
  119.         __writer = null;
  120.         replyLines = new ArrayList<>();
  121.         createCommandSupport();
  122.     }

  123.     /**
  124.      * Performs connection initialization and sets state to {@link IMAPState#NOT_AUTH_STATE}.
  125.      */
  126.     @Override
  127.     protected void _connectAction_() throws IOException {
  128.         super._connectAction_();
  129.         _reader = new CRLFLineReader(new InputStreamReader(_input_, __DEFAULT_ENCODING));
  130.         __writer = new BufferedWriter(new OutputStreamWriter(_output_, __DEFAULT_ENCODING));
  131.         final int tmo = getSoTimeout();
  132.         if (tmo <= 0) { // none set currently
  133.             setSoTimeout(connectTimeout); // use connect timeout to ensure we don't block forever
  134.         }
  135.         getReply(false); // untagged response
  136.         if (tmo <= 0) {
  137.             setSoTimeout(tmo); // restore the original value
  138.         }
  139.         setState(IMAPState.NOT_AUTH_STATE);
  140.     }

  141.     /**
  142.      * Disconnects the client from the server, and sets the state to <code>DISCONNECTED_STATE</code>. The reply text information from the last issued command
  143.      * is voided to allow garbage collection of the memory used to store that information.
  144.      *
  145.      * @throws IOException If there is an error in disconnecting.
  146.      */
  147.     @Override
  148.     public void disconnect() throws IOException {
  149.         super.disconnect();
  150.         _reader = null;
  151.         __writer = null;
  152.         replyLines.clear();
  153.         setState(IMAPState.DISCONNECTED_STATE);
  154.     }

  155.     /**
  156.      * Sends a command to the server and return whether successful.
  157.      *
  158.      * @param command The IMAP command to send (one of the IMAPCommand constants).
  159.      * @return {@code true} if the command was successful
  160.      * @throws IOException on error
  161.      */
  162.     public boolean doCommand(final IMAPCommand command) throws IOException {
  163.         return IMAPReply.isSuccess(sendCommand(command));
  164.     }

  165.     /**
  166.      * Sends a command and arguments to the server and return whether successful.
  167.      *
  168.      * @param command The IMAP command to send (one of the IMAPCommand constants).
  169.      * @param args    The command arguments.
  170.      * @return {@code true} if the command was successful
  171.      * @throws IOException on error
  172.      */
  173.     public boolean doCommand(final IMAPCommand command, final String args) throws IOException {
  174.         return IMAPReply.isSuccess(sendCommand(command, args));
  175.     }

  176.     /**
  177.      * Overrides {@link SocketClient#fireReplyReceived(int, String)} to avoid creating the reply string if there are no listeners to invoke.
  178.      *
  179.      * @param replyCode passed to the listeners
  180.      * @param ignored   the string is only created if there are listeners defined.
  181.      * @see #getReplyString()
  182.      * @since 3.4
  183.      */
  184.     @Override
  185.     protected void fireReplyReceived(final int replyCode, final String ignored) {
  186.         if (getCommandSupport().getListenerCount() > 0) {
  187.             getCommandSupport().fireReplyReceived(replyCode, getReplyString());
  188.         }
  189.     }

  190.     /**
  191.      * Generates a new command ID (tag) for a command.
  192.      *
  193.      * @return a new command ID (tag) for an IMAP command.
  194.      */
  195.     protected String generateCommandID() {
  196.         final String res = new String(initialID);
  197.         // "increase" the ID for the next call
  198.         boolean carry = true; // want to increment initially
  199.         for (int i = initialID.length - 1; carry && i >= 0; i--) {
  200.             if (initialID[i] == 'Z') {
  201.                 initialID[i] = 'A';
  202.             } else {
  203.                 initialID[i]++;
  204.                 carry = false; // did not wrap round
  205.             }
  206.         }
  207.         return res;
  208.     }

  209.     /**
  210.      * Gets the reply for a command that expects a tagged response.
  211.      *
  212.      * @throws IOException
  213.      */
  214.     private void getReply() throws IOException {
  215.         getReply(true); // tagged response
  216.     }

  217.     /**
  218.      * Gets the reply for a command, reading the response until the reply is found.
  219.      *
  220.      * @param wantTag {@code true} if the command expects a tagged response.
  221.      * @throws IOException
  222.      */
  223.     private void getReply(final boolean wantTag) throws IOException {
  224.         replyLines.clear();
  225.         String line = _reader.readLine();

  226.         if (line == null) {
  227.             throw new EOFException("Connection closed without indication.");
  228.         }

  229.         replyLines.add(line);

  230.         if (wantTag) {
  231.             while (IMAPReply.isUntagged(line)) {
  232.                 int literalCount = IMAPReply.literalCount(line);
  233.                 final boolean isMultiLine = literalCount >= 0;
  234.                 while (literalCount >= 0) {
  235.                     line = _reader.readLine();
  236.                     if (line == null) {
  237.                         throw new EOFException("Connection closed without indication.");
  238.                     }
  239.                     replyLines.add(line);
  240.                     literalCount -= line.length() + 2; // Allow for CRLF
  241.                 }
  242.                 if (isMultiLine) {
  243.                     final IMAPChunkListener il = chunkListener;
  244.                     if (il != null) {
  245.                         final boolean clear = il.chunkReceived(this);
  246.                         if (clear) {
  247.                             fireReplyReceived(IMAPReply.PARTIAL, getReplyString());
  248.                             replyLines.clear();
  249.                         }
  250.                     }
  251.                 }
  252.                 line = _reader.readLine(); // get next chunk or final tag
  253.                 if (line == null) {
  254.                     throw new EOFException("Connection closed without indication.");
  255.                 }
  256.                 replyLines.add(line);
  257.             }
  258.             // check the response code on the last line
  259.             replyCode = IMAPReply.getReplyCode(line);
  260.         } else {
  261.             replyCode = IMAPReply.getUntaggedReplyCode(line);
  262.         }

  263.         fireReplyReceived(replyCode, getReplyString());
  264.     }

  265.     /**
  266.      * Returns the reply to the last command sent to the server. The value is a single string containing all the reply lines including newlines.
  267.      *
  268.      * @return The last server response.
  269.      */
  270.     public String getReplyString() {
  271.         final StringBuilder buffer = new StringBuilder(256);
  272.         for (final String s : replyLines) {
  273.             buffer.append(s);
  274.             buffer.append(SocketClient.NETASCII_EOL);
  275.         }

  276.         return buffer.toString();
  277.     }

  278.     /**
  279.      * Returns an array of lines received as a reply to the last command sent to the server. The lines have end of lines truncated.
  280.      *
  281.      * @return The last server response.
  282.      */
  283.     public String[] getReplyStrings() {
  284.         return replyLines.toArray(NetConstants.EMPTY_STRING_ARRAY);
  285.     }

  286.     /**
  287.      * Returns the current IMAP client state.
  288.      *
  289.      * @return The current IMAP client state.
  290.      */
  291.     public IMAP.IMAPState getState() {
  292.         return state;
  293.     }

  294.     /**
  295.      * Sends a command with no arguments to the server and returns the reply code.
  296.      *
  297.      * @param command The IMAP command to send (one of the IMAPCommand constants).
  298.      * @return The server reply code (see IMAPReply).
  299.      * @throws IOException on error
  300.      **/
  301.     public int sendCommand(final IMAPCommand command) throws IOException {
  302.         return sendCommand(command, null);
  303.     }

  304.     /**
  305.      * Sends a command and arguments to the server and returns the reply code.
  306.      *
  307.      * @param command The IMAP command to send (one of the IMAPCommand constants).
  308.      * @param args    The command arguments.
  309.      * @return The server reply code (see IMAPReply).
  310.      * @throws IOException on error
  311.      */
  312.     public int sendCommand(final IMAPCommand command, final String args) throws IOException {
  313.         return sendCommand(command.getIMAPCommand(), args);
  314.     }

  315.     /**
  316.      * Sends a command with no arguments to the server and returns the reply code.
  317.      *
  318.      * @param command The IMAP command to send.
  319.      * @return The server reply code (see IMAPReply).
  320.      * @throws IOException on error
  321.      */
  322.     public int sendCommand(final String command) throws IOException {
  323.         return sendCommand(command, null);
  324.     }

  325.     /**
  326.      * Sends a command an arguments to the server and returns the reply code.
  327.      *
  328.      * @param command The IMAP command to send.
  329.      * @param args    The command arguments.
  330.      * @return The server reply code (see IMAPReply).
  331.      * @throws IOException on error
  332.      */
  333.     public int sendCommand(final String command, final String args) throws IOException {
  334.         return sendCommandWithID(generateCommandID(), command, args);
  335.     }

  336.     /**
  337.      * Sends a command an arguments to the server and returns the reply code.
  338.      *
  339.      * @param commandID The ID (tag) of the command.
  340.      * @param command   The IMAP command to send.
  341.      * @param args      The command arguments.
  342.      * @return The server reply code (either {@link IMAPReply#OK}, {@link IMAPReply#NO} or {@link IMAPReply#BAD}).
  343.      */
  344.     private int sendCommandWithID(final String commandID, final String command, final String args) throws IOException {
  345.         final StringBuilder __commandBuffer = new StringBuilder();
  346.         if (commandID != null) {
  347.             __commandBuffer.append(commandID);
  348.             __commandBuffer.append(' ');
  349.         }
  350.         __commandBuffer.append(command);

  351.         if (args != null) {
  352.             __commandBuffer.append(' ');
  353.             __commandBuffer.append(args);
  354.         }
  355.         __commandBuffer.append(SocketClient.NETASCII_EOL);

  356.         final String message = __commandBuffer.toString();
  357.         __writer.write(message);
  358.         __writer.flush();

  359.         fireCommandSent(command, message);

  360.         getReply();
  361.         return replyCode;
  362.     }

  363.     /**
  364.      * Sends data 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 sendData(final String command) throws IOException {
  371.         return sendCommandWithID(null, command, null);
  372.     }

  373.     /**
  374.      * Sets the current chunk listener. If a listener is registered and the implementation returns true, then any registered
  375.      * {@link org.apache.commons.net.PrintCommandListener PrintCommandListener} instances will be invoked with the partial response and a status of
  376.      * {@link IMAPReply#PARTIAL} to indicate that the final reply code is not yet known.
  377.      *
  378.      * @param listener the class to use, or {@code null} to disable
  379.      * @see #TRUE_CHUNK_LISTENER
  380.      * @since 3.4
  381.      */
  382.     public void setChunkListener(final IMAPChunkListener listener) {
  383.         chunkListener = listener;
  384.     }

  385.     /**
  386.      * Sets IMAP client state. This must be one of the <code>_STATE</code> constants.
  387.      *
  388.      * @param state The new state.
  389.      */
  390.     protected void setState(final IMAP.IMAPState state) {
  391.         this.state = state;
  392.     }
  393. }