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 issue a
119     * {@link #reset reset } command. Messages marked for deletion are only deleted by the server on {@link #logout logout }.
120     * A deletion attempt can only succeed 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 user and password. You must first connect to the server with
233     * {@link org.apache.commons.net.SocketClient#connect connect } before attempting to log in. 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 user 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 user, final String password) throws IOException {
243        if (getState() != AUTHORIZATION_STATE) {
244            return false;
245        }
246
247        if (sendCommand(POP3Command.USER, user) != 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 log in. 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 user  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 user, 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(user);
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 time out 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
393     * actually reads directly from the POP3 connection. After the end of message has been reached, new commands can be executed and their replies read.
394     * If you do not 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}