    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.pop3;
    019
    020import java.io.BufferedReader;
    021import java.io.IOException;
    022import java.io.Reader;
    023import java.security.MessageDigest;
    024import java.security.NoSuchAlgorithmException;
    025import java.util.Arrays;
    026import java.util.ListIterator;
    027import java.util.StringTokenizer;
    028
    029import org.apache.commons.net.io.DotTerminatedMessageReader;
    030
    031/**
    032 * The POP3Client class implements the client side of the Internet POP3 Protocol defined in RFC 1939. All commands are supported, including the APOP command
    033 * which requires MD5 encryption. See RFC 1939 for more details on the POP3 protocol.
    034 * <p>
    035 * Rather than list it separately for each method, we mention here that every method communicating with the server and throwing an IOException can also throw a
    036 * {@link org.apache.commons.net.MalformedServerReplyException} , which is a subclass of IOException. A MalformedServerReplyException will be thrown when the
    037 * reply received from the server deviates enough from the protocol specification that it cannot be interpreted in a useful manner despite attempts to be as
    038 * lenient as possible.
    039 * </p>
    040 *
    041 * @see POP3MessageInfo
    042 * @see org.apache.commons.net.io.DotTerminatedMessageReader
    043 * @see org.apache.commons.net.MalformedServerReplyException
    044 */
    045
    046public class POP3Client extends POP3 {
    047
    048    private static POP3MessageInfo parseStatus(final String line) {
    049        int num;
    050        int size;
    051        final StringTokenizer tokenizer;
    052
    053        tokenizer = new StringTokenizer(line);
    054
    055        if (!tokenizer.hasMoreElements()) {
    056            return null;
    057        }
    058
    059        num = size = 0;
    060
    061        try {
    062            num = Integer.parseInt(tokenizer.nextToken());
    063
    064            if (!tokenizer.hasMoreElements()) {
    065                return null;
    066            }
    067
    068            size = Integer.parseInt(tokenizer.nextToken());
    069        } catch (final NumberFormatException e) {
    070            return null;
    071        }
    072
    073        return new POP3MessageInfo(num, size);
    074    }
    075
    076    private static POP3MessageInfo parseUID(String line) {
    077        int num;
    078        final StringTokenizer tokenizer;
    079
    080        tokenizer = new StringTokenizer(line);
    081
    082        if (!tokenizer.hasMoreElements()) {
    083            return null;
    084        }
    085
    086        num = 0;
    087
    088        try {
    089            num = Integer.parseInt(tokenizer.nextToken());
    090
    091            if (!tokenizer.hasMoreElements()) {
    092                return null;
    093            }
    094
    095            line = tokenizer.nextToken();
    096        } catch (final NumberFormatException e) {
    097            return null;
    098        }
    099
    100        return new POP3MessageInfo(num, line);
    101    }
    102
    103    /**
    104     * Constructs a new instance.
    105     */
    106    public POP3Client() {
    107        // empty
    108    }
    109
    110    /**
    111     * Send a CAPA command to the POP3 server.
    112     *
    113     * @return True if the command was successful, false if not.
    114     * @throws IOException If a network I/O error occurs in the process of sending the CAPA command.
    115     * @since 3.1 (was previously in ExtendedPOP3Client)
    116     */
    117    public boolean capa() throws IOException {
    118        if (sendCommand(POP3Command.CAPA) == POP3Reply.OK) {
    119            getAdditionalReply();
    120            return true;
    121        }
    122        return false;
    123
    124    }
    125
    126    /**
    127     * Delete a message from the POP3 server. The message is only marked for deletion by the server. If you decide to unmark the message, you must issue a
    128     * {@link #reset()} command. Messages marked for deletion are only deleted by the server on {@link #logout()}. A deletion attempt can only succeed if
    129     * the client is in the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}.
    130     *
    131     * @param messageId The message number to delete.
    132     * @return True if the deletion attempt was successful, false if not.
    133     * @throws IOException If a network I/O error occurs in the process of sending the delete command.
    134     */
    135    public boolean deleteMessage(final int messageId) throws IOException {
    136        if (getState() == TRANSACTION_STATE) {
    137            return sendCommand(POP3Command.DELE, Integer.toString(messageId)) == POP3Reply.OK;
    138        }
    139        return false;
    140    }
    141
    142    /**
    143     * List an individual message. A list attempt can only succeed if the client is in the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE
    144     * TRANSACTION_STATE}. Returns a POP3MessageInfo instance containing the number of the listed message and the size of the message in bytes. Returns null if
    145     * the list attempt fails (e.g., if the specified message number does not exist).
    146     *
    147     * @param messageId The number of the message list.
    148     * @return A POP3MessageInfo instance containing the number of the listed message and the size of the message in bytes. Returns null if the list attempt
    149     *         fails.
    150     * @throws IOException If a network I/O error occurs in the process of sending the list command.
    151     */
    152    public POP3MessageInfo listMessage(final int messageId) throws IOException {
    153        if (getState() != TRANSACTION_STATE) {
    154            return null;
    155        }
    156        if (sendCommand(POP3Command.LIST, Integer.toString(messageId)) != POP3Reply.OK) {
    157            return null;
    158        }
    159        return parseStatus(lastReplyLine.substring(3));
    160    }
    161
    162    /**
    163     * List all messages. A list attempt can only succeed if the client is in the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}.
    164     * Returns an array of POP3MessageInfo instances, each containing the number of a message and its size in bytes. If there are no messages, this method
    165     * returns a zero length array. If the list attempt fails, it returns null.
    166     *
    167     * @return An array of POP3MessageInfo instances representing all messages in the order they appear in the mailbox, each containing the number of a message
    168     *         and its size in bytes. If there are no messages, this method returns a zero length array. If the list attempt fails, it returns null.
    169     * @throws IOException If a network I/O error occurs in the process of sending the list command.
    170     */
    171    public POP3MessageInfo[] listMessages() throws IOException {
    172        if (getState() != TRANSACTION_STATE) {
    173            return null;
    174        }
    175        if (sendCommand(POP3Command.LIST) != POP3Reply.OK) {
    176            return null;
    177        }
    178        getAdditionalReply();
    179
    180        // This could be a zero length array if no messages present
    181        final POP3MessageInfo[] messages = new POP3MessageInfo[replyLines.size() - 2]; // skip first and last lines
    182
    183        final ListIterator<String> en = replyLines.listIterator(1); // Skip first line
    184
    185        // Fetch lines.
    186        Arrays.setAll(messages, i -> parseStatus(en.next()));
    187
    188        return messages;
    189    }
    190
    191    /**
    192     * List the unique identifier for a message. A list attempt can only succeed if the client is in the
    193     * {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE} . Returns a POP3MessageInfo instance containing the number of the listed
    194     * message and the unique identifier for that message. Returns null if the list attempt fails (e.g., if the specified message number does not exist).
    195     *
    196     * @param messageId The number of the message list.
    197     * @return A POP3MessageInfo instance containing the number of the listed message and the unique identifier for that message. Returns null if the list
    198     *         attempt fails.
    199     * @throws IOException If a network I/O error occurs in the process of sending the list unique identifier command.
    200     */
    201    public POP3MessageInfo listUniqueIdentifier(final int messageId) throws IOException {
    202        if (getState() != TRANSACTION_STATE) {
    203            return null;
    204        }
    205        if (sendCommand(POP3Command.UIDL, Integer.toString(messageId)) != POP3Reply.OK) {
    206            return null;
    207        }
    208        return parseUID(lastReplyLine.substring(3));
    209    }
    210
    211    /**
    212     * List the unique identifiers for all messages. A list attempt can only succeed if the client is in the
    213     * {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE} . Returns an array of POP3MessageInfo instances, each containing the number
    214     * of a message and its unique identifier. If there are no messages, this method returns a zero length array. If the list attempt fails, it returns null.
    215     *
    216     * @return An array of POP3MessageInfo instances representing all messages in the order they appear in the mailbox, each containing the number of a message
    217     *         and its unique identifier If there are no messages, this method returns a zero length array. If the list attempt fails, it returns null.
    218     * @throws IOException If a network I/O error occurs in the process of sending the list unique identifier command.
    219     */
    220    public POP3MessageInfo[] listUniqueIdentifiers() throws IOException {
    221        if (getState() != TRANSACTION_STATE) {
    222            return null;
    223        }
    224        if (sendCommand(POP3Command.UIDL) != POP3Reply.OK) {
    225            return null;
    226        }
    227        getAdditionalReply();
    228
    229        // This could be a zero length array if no messages present
    230        final POP3MessageInfo[] messages = new POP3MessageInfo[replyLines.size() - 2]; // skip first and last lines
    231
    232        final ListIterator<String> en = replyLines.listIterator(1); // skip first line
    233
    234        // Fetch lines.
    235        Arrays.setAll(messages, i -> parseUID(en.next()));
    236
    237        return messages;
    238    }
    239
    240    /**
    241     * Login to the POP3 server with the given user and password. You must first connect to the server with {@link org.apache.commons.net.SocketClient#connect
    242     * connect} before attempting to log in. A login attempt is only valid if the client is in the {@link org.apache.commons.net.pop3.POP3#AUTHORIZATION_STATE
    243     * AUTHORIZATION_STATE }. After logging in, the client enters the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}.
    244     *
    245     * @param user     The account name being logged in to.
    246     * @param password The plain text password of the account.
    247     * @return True if the login attempt was successful, false if not.
    248     * @throws IOException If a network I/O error occurs in the process of logging in.
    249     */
    250    public boolean login(final String user, final String password) throws IOException {
    251        if (getState() != AUTHORIZATION_STATE) {
    252            return false;
    253        }
    254
    255        if (sendCommand(POP3Command.USER, user) != POP3Reply.OK) {
    256            return false;
    257        }
    258
    259        if (sendCommand(POP3Command.PASS, password) != POP3Reply.OK) {
    260            return false;
    261        }
    262
    263        setState(TRANSACTION_STATE);
    264
    265        return true;
    266    }
    267
    268    /**
    269     * Login to the POP3 server with the given username and authentication information. Use this method when connecting to a server requiring authentication
    270     * using the APOP command. Because the timestamp produced in the greeting banner varies from server to server, it is not possible to consistently extract
    271     * the information. Therefore, after connecting to the server, you must call {@link org.apache.commons.net.pop3.POP3#getReplyString getReplyString} and
    272     * parse out the timestamp information yourself.
    273     * <p>
    274     * You must first connect to the server with {@link org.apache.commons.net.SocketClient#connect connect} before attempting to log in. A login attempt is
    275     * only valid if the client is in the {@link org.apache.commons.net.pop3.POP3#AUTHORIZATION_STATE AUTHORIZATION_STATE}. After logging in, the client enters
    276     * the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}. After connecting, you must parse out the server specific information
    277     * to use as a timestamp, and pass that information to this method. The secret is a shared secret known to you and the server. See RFC 1939 for more details
    278     * regarding the APOP command.
    279     * </p>
    280     *
    281     * @param user      The account name being logged in to.
    282     * @param timestamp The timestamp string to combine with the secret.
    283     * @param secret    The shared secret which produces the MD5 digest when combined with the timestamp.
    284     * @return True if the login attempt was successful, false if not.
    285     * @throws IOException              If a network I/O error occurs in the process of logging in.
    286     * @throws NoSuchAlgorithmException If the MD5 encryption algorithm cannot be instantiated by the Java runtime system.
    287     */
    288    public boolean login(final String user, String timestamp, final String secret) throws IOException, NoSuchAlgorithmException {
    289        int i;
    290        final byte[] digest;
    291        final StringBuilder buffer;
    292        final StringBuilder digestBuffer;
    293        final MessageDigest md5;
    294
    295        if (getState() != AUTHORIZATION_STATE) {
    296            return false;
    297        }
    298
    299        md5 = MessageDigest.getInstance("MD5");
    300        timestamp += secret;
    301        digest = md5.digest(timestamp.getBytes(getCharset()));
    302        digestBuffer = new StringBuilder(128);
    303
    304        for (i = 0; i < digest.length; i++) {
    305            final int digit = digest[i] & 0xff;
    306            if (digit <= 15) { // Add leading zero if necessary (NET-351)
    307                digestBuffer.append("0");
    308            }
    309            digestBuffer.append(Integer.toHexString(digit));
    310        }
    311
    312        buffer = new StringBuilder(256);
    313        buffer.append(user);
    314        buffer.append(' ');
    315        buffer.append(digestBuffer.toString());
    316
    317        if (sendCommand(POP3Command.APOP, buffer.toString()) != POP3Reply.OK) {
    318            return false;
    319        }
    320
    321        setState(TRANSACTION_STATE);
    322
    323        return true;
    324    }
    325
    326    /**
    327     * Logout of the POP3 server. To fully disconnect from the server you must call {@link org.apache.commons.net.pop3.POP3#disconnect disconnect}. A logout
    328     * attempt is valid in any state. If the client is in the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE} , it enters the
    329     * {@link org.apache.commons.net.pop3.POP3#UPDATE_STATE UPDATE_STATE} on a successful logout.
    330     *
    331     * @return True if the logout attempt was successful, false if not.
    332     * @throws IOException If a network I/O error occurs in the process of logging out.
    333     */
    334    public boolean logout() throws IOException {
    335        if (getState() == TRANSACTION_STATE) {
    336            setState(UPDATE_STATE);
    337        }
    338        sendCommand(POP3Command.QUIT);
    339        return replyCode == POP3Reply.OK;
    340    }
    341
    342    /**
    343     * Send a NOOP command to the POP3 server. This is useful for keeping a connection alive since most POP3 servers will time out after 10 minutes of
    344     * inactivity. A noop attempt will only succeed if the client is in the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}.
    345     *
    346     * @return True if the noop attempt was successful, false if not.
    347     * @throws IOException If a network I/O error occurs in the process of sending the NOOP command.
    348     */
    349    public boolean noop() throws IOException {
    350        if (getState() == TRANSACTION_STATE) {
    351            return sendCommand(POP3Command.NOOP) == POP3Reply.OK;
    352        }
    353        return false;
    354    }
    355
    356    /**
    357     * Reset the POP3 session. This is useful for undoing any message deletions that may have been performed. A reset attempt can only succeed if the client is
    358     * in the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}.
    359     *
    360     * @return True if the reset attempt was successful, false if not.
    361     * @throws IOException If a network I/O error occurs in the process of sending the reset command.
    362     */
    363    public boolean reset() throws IOException {
    364        if (getState() == TRANSACTION_STATE) {
    365            return sendCommand(POP3Command.RSET) == POP3Reply.OK;
    366        }
    367        return false;
    368    }
    369
    370    /**
    371     * Retrieve a message from the POP3 server. A retrieve message attempt can only succeed if the client is in the
    372     * {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}
    373     * <p>
    374     * You must not issue any commands to the POP3 server (i.e., call any other methods) until you finish reading the message from the returned BufferedReader
    375     * instance. The POP3 protocol uses the same stream for issuing commands as it does for returning results. Therefore, the returned BufferedReader actually
    376     * reads directly from the POP3 connection. After the end of message has been reached, new commands can be executed and their replies read. If you do not
    377     * follow these requirements, your program will not work properly.
    378     * </p>
    379     *
    380     * @param messageId The number of the message to fetch.
    381     * @return A DotTerminatedMessageReader instance from which the entire message can be read. This can safely be cast to a {@link BufferedReader} in order to
    382     *         use the {@link BufferedReader#readLine()} method. Returns null if the retrieval attempt fails (e.g., if the specified message number does not
    383     *         exist).
    384     * @throws IOException If a network I/O error occurs in the process of sending the retrieve message command.
    385     */
    386    public Reader retrieveMessage(final int messageId) throws IOException {
    387        if (getState() != TRANSACTION_STATE) {
    388            return null;
    389        }
    390        if (sendCommand(POP3Command.RETR, Integer.toString(messageId)) != POP3Reply.OK) {
    391            return null;
    392        }
    393
    394        return new DotTerminatedMessageReader(reader);
    395    }
    396
    397    /**
    398     * Retrieve only the specified top number of lines of a message from the POP3 server. A retrieve top lines attempt can only succeed if the client is in the
    399     * {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}
    400     * <p>
    401     * You must not issue any commands to the POP3 server (i.e., call any other methods) until you finish reading the message from the returned BufferedReader
    402     * instance. The POP3 protocol uses the same stream for issuing commands as it does for returning results. Therefore, the returned BufferedReader actually
    403     * reads directly from the POP3 connection. After the end of message has been reached, new commands can be executed and their replies read. If you do not
    404     * follow these requirements, your program will not work properly.
    405     * </p>
    406     *
    407     * @param messageId The number of the message to fetch.
    408     * @param numLines  The top number of lines to fetch. This must be &gt;= 0.
    409     * @return A DotTerminatedMessageReader instance from which the specified top number of lines of the message can be read. This can safely be cast to a
    410     *         {@link BufferedReader} in order to use the {@link BufferedReader#readLine()} method. Returns null if the retrieval attempt fails (e.g., if the
    411     *         specified message number does not exist).
    412     * @throws IOException If a network I/O error occurs in the process of sending the top command.
    413     */
    414    public Reader retrieveMessageTop(final int messageId, final int numLines) throws IOException {
    415        if (numLines < 0 || getState() != TRANSACTION_STATE) {
    416            return null;
    417        }
    418        if (sendCommand(POP3Command.TOP, Integer.toString(messageId) + " " + Integer.toString(numLines)) != POP3Reply.OK) {
    419            return null;
    420        }
    421
    422        return new DotTerminatedMessageReader(reader);
    423    }
    424
    425    /**
    426     * Gets the mailbox status. A status attempt can only succeed if the client is in the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE
    427     * TRANSACTION_STATE } . Returns a POP3MessageInfo instance containing the number of messages in the mailbox and the total size of the messages in bytes.
    428     * Returns null if the status the attempt fails.
    429     *
    430     * @return A POP3MessageInfo instance containing the number of messages in the mailbox and the total size of the messages in bytes. Returns null if the
    431     *         status the attempt fails.
    432     * @throws IOException If a network I/O error occurs in the process of sending the status command.
    433     */
    434    public POP3MessageInfo status() throws IOException {
    435        if (getState() != TRANSACTION_STATE) {
    436            return null;
    437        }
    438        if (sendCommand(POP3Command.STAT) != POP3Reply.OK) {
    439            return null;
    440        }
    441        return parseStatus(lastReplyLine.substring(3));
    442    }
    443
    444}