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