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