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 || 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 }