View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.net.pop3;
19  
20  import java.io.BufferedReader;
21  import java.io.IOException;
22  import java.io.Reader;
23  import java.security.MessageDigest;
24  import java.security.NoSuchAlgorithmException;
25  import java.util.Arrays;
26  import java.util.ListIterator;
27  import java.util.StringTokenizer;
28  
29  import org.apache.commons.net.io.DotTerminatedMessageReader;
30  
31  /**
32   * The POP3Client class implements the client side of the Internet POP3 Protocol defined in RFC 1939. All commands are supported, including the APOP command
33   * which requires MD5 encryption. See RFC 1939 for more details on the POP3 protocol.
34   * <p>
35   * 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
36   * {@link org.apache.commons.net.MalformedServerReplyException} , which is a subclass of IOException. A MalformedServerReplyException will be thrown when the
37   * 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
38   * lenient as possible.
39   * </p>
40   *
41   * @see POP3MessageInfo
42   * @see org.apache.commons.net.io.DotTerminatedMessageReader
43   * @see org.apache.commons.net.MalformedServerReplyException
44   */
45  
46  public class POP3Client extends POP3 {
47  
48      private static POP3MessageInfo parseStatus(final String line) {
49          int num;
50          int size;
51          final StringTokenizer tokenizer;
52  
53          tokenizer = new StringTokenizer(line);
54  
55          if (!tokenizer.hasMoreElements()) {
56              return null;
57          }
58  
59          num = size = 0;
60  
61          try {
62              num = Integer.parseInt(tokenizer.nextToken());
63  
64              if (!tokenizer.hasMoreElements()) {
65                  return null;
66              }
67  
68              size = Integer.parseInt(tokenizer.nextToken());
69          } catch (final NumberFormatException e) {
70              return null;
71          }
72  
73          return new POP3MessageInfo(num, size);
74      }
75  
76      private static POP3MessageInfo parseUID(String line) {
77          int num;
78          final StringTokenizer tokenizer;
79  
80          tokenizer = new StringTokenizer(line);
81  
82          if (!tokenizer.hasMoreElements()) {
83              return null;
84          }
85  
86          num = 0;
87  
88          try {
89              num = Integer.parseInt(tokenizer.nextToken());
90  
91              if (!tokenizer.hasMoreElements()) {
92                  return null;
93              }
94  
95              line = tokenizer.nextToken();
96          } catch (final NumberFormatException e) {
97              return null;
98          }
99  
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 }