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}