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 }