    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 || sendCommand(POP3Command.LIST, Integer.toString(messageId)) != POP3Reply.OK) {
    154            return null;
    155        }
    156        return parseStatus(lastReplyLine.substring(3));
    157    }
    158
    159    /**
    160     * 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}.
    161     * 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
    162     * returns a zero length array. If the list attempt fails, it returns null.
    163     *
    164     * @return An array of POP3MessageInfo instances representing all messages in the order they appear in the mailbox, each containing the number of a message
    165     *         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.
    166     * @throws IOException If a network I/O error occurs in the process of sending the list command.
    167     */
    168    public POP3MessageInfo[] listMessages() throws IOException {
    169        if (getState() != TRANSACTION_STATE || sendCommand(POP3Command.LIST) != POP3Reply.OK) {
    170            return null;
    171        }
    172        getAdditionalReply();
    173
    174        // This could be a zero length array if no messages present
    175        final POP3MessageInfo[] messages = new POP3MessageInfo[replyLines.size() - 2]; // skip first and last lines
    176
    177        final ListIterator<String> en = replyLines.listIterator(1); // Skip first line
    178
    179        // Fetch lines.
    180        Arrays.setAll(messages, i -> parseStatus(en.next()));
    181
    182        return messages;
    183    }
    184
    185    /**
    186     * List the unique identifier for a message. A list attempt can only succeed if the client is in the
    187     * {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE} . Returns a POP3MessageInfo instance containing the number of the listed
    188     * 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).
    189     *
    190     * @param messageId The number of the message list.
    191     * @return A POP3MessageInfo instance containing the number of the listed message and the unique identifier for that message. Returns null if the list
    192     *         attempt fails.
    193     * @throws IOException If a network I/O error occurs in the process of sending the list unique identifier command.
    194     */
    195    public POP3MessageInfo listUniqueIdentifier(final int messageId) throws IOException {
    196        if (getState() != TRANSACTION_STATE || sendCommand(POP3Command.UIDL, Integer.toString(messageId)) != POP3Reply.OK) {
    197            return null;
    198        }
    199        return parseUID(lastReplyLine.substring(3));
    200    }
    201
    202    /**
    203     * List the unique identifiers for all messages. A list attempt can only succeed if the client is in the
    204     * {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE} . Returns an array of POP3MessageInfo instances, each containing the number
    205     * 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.
    206     *
    207     * @return An array of POP3MessageInfo instances representing all messages in the order they appear in the mailbox, each containing the number of a message
    208     *         and its unique identifier If there are no messages, this method returns a zero length array. If the list attempt fails, it returns null.
    209     * @throws IOException If a network I/O error occurs in the process of sending the list unique identifier command.
    210     */
    211    public POP3MessageInfo[] listUniqueIdentifiers() throws IOException {
    212        if (getState() != TRANSACTION_STATE || sendCommand(POP3Command.UIDL) != POP3Reply.OK) {
    213            return null;
    214        }
    215        getAdditionalReply();
    216
    217        // This could be a zero length array if no messages present
    218        final POP3MessageInfo[] messages = new POP3MessageInfo[replyLines.size() - 2]; // skip first and last lines
    219
    220        final ListIterator<String> en = replyLines.listIterator(1); // skip first line
    221
    222        // Fetch lines.
    223        Arrays.setAll(messages, i -> parseUID(en.next()));
    224
    225        return messages;
    226    }
    227
    228    /**
    229     * 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
    230     * 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
    231     * AUTHORIZATION_STATE }. After logging in, the client enters the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}.
    232     *
    233     * @param user     The account name being logged in to.
    234     * @param password The plain text password of the account.
    235     * @return True if the login attempt was successful, false if not.
    236     * @throws IOException If a network I/O error occurs in the process of logging in.
    237     */
    238    public boolean login(final String user, final String password) throws IOException {
    239        if (getState() != AUTHORIZATION_STATE || sendCommand(POP3Command.USER, user) != POP3Reply.OK
    240                || sendCommand(POP3Command.PASS, password) != POP3Reply.OK) {
    241            return false;
    242        }
    243        setState(TRANSACTION_STATE);
    244        return true;
    245    }
    246
    247    /**
    248     * Login to the POP3 server with the given username and authentication information. Use this method when connecting to a server requiring authentication
    249     * using the APOP command. Because the timestamp produced in the greeting banner varies from server to server, it is not possible to consistently extract
    250     * the information. Therefore, after connecting to the server, you must call {@link org.apache.commons.net.pop3.POP3#getReplyString getReplyString} and
    251     * parse out the timestamp information yourself.
    252     * <p>
    253     * 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
    254     * 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
    255     * the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}. After connecting, you must parse out the server specific information
    256     * 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
    257     * regarding the APOP command.
    258     * </p>
    259     *
    260     * @param user      The account name being logged in to.
    261     * @param timestamp The timestamp string to combine with the secret.
    262     * @param secret    The shared secret which produces the MD5 digest when combined with the timestamp.
    263     * @return True if the login attempt was successful, false if not.
    264     * @throws IOException              If a network I/O error occurs in the process of logging in.
    265     * @throws NoSuchAlgorithmException If the MD5 encryption algorithm cannot be instantiated by the Java runtime system.
    266     */
    267    public boolean login(final String user, String timestamp, final String secret) throws IOException, NoSuchAlgorithmException {
    268        int i;
    269        final byte[] digest;
    270        final StringBuilder buffer;
    271        final StringBuilder digestBuffer;
    272        final MessageDigest md5;
    273        if (getState() != AUTHORIZATION_STATE) {
    274            return false;
    275        }
    276        md5 = MessageDigest.getInstance("MD5");
    277        timestamp += secret;
    278        digest = md5.digest(timestamp.getBytes(getCharset()));
    279        digestBuffer = new StringBuilder(128);
    280        for (i = 0; i < digest.length; i++) {
    281            final int digit = digest[i] & 0xff;
    282            if (digit <= 15) { // Add leading zero if necessary (NET-351)
    283                digestBuffer.append("0");
    284            }
    285            digestBuffer.append(Integer.toHexString(digit));
    286        }
    287        buffer = new StringBuilder(256);
    288        buffer.append(user);
    289        buffer.append(' ');
    290        buffer.append(digestBuffer.toString());
    291        if (sendCommand(POP3Command.APOP, buffer.toString()) != POP3Reply.OK) {
    292            return false;
    293        }
    294        setState(TRANSACTION_STATE);
    295        return true;
    296    }
    297
    298    /**
    299     * 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
    300     * 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
    301     * {@link org.apache.commons.net.pop3.POP3#UPDATE_STATE UPDATE_STATE} on a successful logout.
    302     *
    303     * @return True if the logout attempt was successful, false if not.
    304     * @throws IOException If a network I/O error occurs in the process of logging out.
    305     */
    306    public boolean logout() throws IOException {
    307        if (getState() == TRANSACTION_STATE) {
    308            setState(UPDATE_STATE);
    309        }
    310        sendCommand(POP3Command.QUIT);
    311        return replyCode == POP3Reply.OK;
    312    }
    313
    314    /**
    315     * 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
    316     * inactivity. A noop attempt will only succeed if the client is in the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}.
    317     *
    318     * @return True if the noop attempt was successful, false if not.
    319     * @throws IOException If a network I/O error occurs in the process of sending the NOOP command.
    320     */
    321    public boolean noop() throws IOException {
    322        if (getState() == TRANSACTION_STATE) {
    323            return sendCommand(POP3Command.NOOP) == POP3Reply.OK;
    324        }
    325        return false;
    326    }
    327
    328    /**
    329     * 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
    330     * in the {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}.
    331     *
    332     * @return True if the reset attempt was successful, false if not.
    333     * @throws IOException If a network I/O error occurs in the process of sending the reset command.
    334     */
    335    public boolean reset() throws IOException {
    336        if (getState() == TRANSACTION_STATE) {
    337            return sendCommand(POP3Command.RSET) == POP3Reply.OK;
    338        }
    339        return false;
    340    }
    341
    342    /**
    343     * Retrieve a message from the POP3 server. A retrieve message attempt can only succeed if the client is in the
    344     * {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}
    345     * <p>
    346     * 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
    347     * instance. The POP3 protocol uses the same stream for issuing commands as it does for returning results. Therefore, the returned BufferedReader actually
    348     * 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
    349     * follow these requirements, your program will not work properly.
    350     * </p>
    351     *
    352     * @param messageId The number of the message to fetch.
    353     * @return A DotTerminatedMessageReader instance from which the entire message can be read. This can safely be cast to a {@link BufferedReader} in order to
    354     *         use the {@link BufferedReader#readLine()} method. Returns null if the retrieval attempt fails (e.g., if the specified message number does not
    355     *         exist).
    356     * @throws IOException If a network I/O error occurs in the process of sending the retrieve message command.
    357     */
    358    public Reader retrieveMessage(final int messageId) throws IOException {
    359        if (getState() != TRANSACTION_STATE || sendCommand(POP3Command.RETR, Integer.toString(messageId)) != POP3Reply.OK) {
    360            return null;
    361        }
    362        return new DotTerminatedMessageReader(reader);
    363    }
    364
    365    /**
    366     * 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
    367     * {@link org.apache.commons.net.pop3.POP3#TRANSACTION_STATE TRANSACTION_STATE}
    368     * <p>
    369     * 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
    370     * instance. The POP3 protocol uses the same stream for issuing commands as it does for returning results. Therefore, the returned BufferedReader actually
    371     * 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
    372     * follow these requirements, your program will not work properly.
    373     * </p>
    374     *
    375     * @param messageId The number of the message to fetch.
    376     * @param numLines  The top number of lines to fetch. This must be &gt;= 0.
    377     * @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
    378     *         {@link BufferedReader} in order to use the {@link BufferedReader#readLine()} method. Returns null if the retrieval attempt fails (e.g., if the
    379     *         specified message number does not exist).
    380     * @throws IOException If a network I/O error occurs in the process of sending the top command.
    381     */
    382    public Reader retrieveMessageTop(final int messageId, final int numLines) throws IOException {
    383        if (numLines < 0 || getState() != TRANSACTION_STATE
    384                || sendCommand(POP3Command.TOP, Integer.toString(messageId) + " " + Integer.toString(numLines)) != POP3Reply.OK) {
    385            return null;
    386        }
    387        return new DotTerminatedMessageReader(reader);
    388    }
    389
    390    /**
    391     * 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
    392     * TRANSACTION_STATE } . Returns a POP3MessageInfo instance containing the number of messages in the mailbox and the total size of the messages in bytes.
    393     * Returns null if the status the attempt fails.
    394     *
    395     * @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
    396     *         status the attempt fails.
    397     * @throws IOException If a network I/O error occurs in the process of sending the status command.
    398     */
    399    public POP3MessageInfo status() throws IOException {
    400        if (getState() != TRANSACTION_STATE || sendCommand(POP3Command.STAT) != POP3Reply.OK) {
    401            return null;
    402        }
    403        return parseStatus(lastReplyLine.substring(3));
    404    }
    405
    406}