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