001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 package org.apache.geronimo.javamail.store.pop3.connection; 019 020 import java.io.*; 021 import java.net.InetAddress; 022 import java.net.Socket; 023 import java.net.SocketException; 024 import java.net.UnknownHostException; 025 import java.security.MessageDigest; 026 import java.security.NoSuchAlgorithmException; 027 import java.util.ArrayList; 028 import java.util.Date; 029 import java.util.HashMap; 030 import java.util.Iterator; 031 import java.util.LinkedList; 032 import java.util.List; 033 import java.util.StringTokenizer; 034 035 import javax.mail.MessagingException; 036 import javax.mail.internet.InternetHeaders; 037 038 import org.apache.geronimo.javamail.authentication.AuthenticatorFactory; 039 import org.apache.geronimo.javamail.authentication.ClientAuthenticator; 040 import org.apache.geronimo.javamail.store.pop3.POP3Constants; 041 import org.apache.geronimo.javamail.util.CommandFailedException; 042 import org.apache.geronimo.javamail.util.InvalidCommandException; 043 import org.apache.geronimo.javamail.util.MIMEInputReader; 044 import org.apache.geronimo.javamail.util.MailConnection; 045 import org.apache.geronimo.javamail.util.ProtocolProperties; 046 import org.apache.geronimo.mail.util.Base64; 047 import org.apache.geronimo.mail.util.Hex; 048 049 /** 050 * Simple implementation of POP3 transport. 051 * 052 * @version $Rev: 597135 $ $Date: 2007-11-21 11:26:57 -0500 (Wed, 21 Nov 2007) $ 053 */ 054 public class POP3Connection extends MailConnection implements POP3Constants { 055 056 static final protected String MAIL_APOP_ENABLED = "apop.enable"; 057 static final protected String MAIL_AUTH_ENABLED = "auth.enable"; 058 static final protected String MAIL_RESET_QUIT = "rsetbeforequit"; 059 static final protected String MAIL_DISABLE_TOP = "disabletop"; 060 static final protected String MAIL_FORGET_TOP = "forgettopheaders"; 061 062 // the initial greeting string, which might be required for APOP authentication. 063 protected String greeting; 064 // is use of the AUTH command enabled 065 protected boolean authEnabled; 066 // is use of APOP command enabled 067 protected boolean apopEnabled; 068 // input reader wrapped around the socket input stream 069 protected BufferedReader reader; 070 // output writer wrapped around the socket output stream. 071 protected PrintWriter writer; 072 // this connection was closed unexpectedly 073 protected boolean closed; 074 // indicates whether this conneciton is currently logged in. Once 075 // we send a QUIT, we're finished. 076 protected boolean loggedIn; 077 // indicates whether we need to avoid using the TOP command 078 // when retrieving headers 079 protected boolean topDisabled = false; 080 081 /** 082 * Normal constructor for an POP3Connection() object. 083 * 084 * @param store The store we're associated with (source of parameter values). 085 * @param host The target host name of the IMAP server. 086 * @param port The target listening port of the server. Defaults to 119 if 087 * the port is specified as -1. 088 * @param username The login user name (can be null unless authentication is 089 * required). 090 * @param password Password associated with the userid account. Can be null if 091 * authentication is not required. 092 * @param sslConnection 093 * True if this is targetted as an SSLConnection. 094 * @param debug The session debug flag. 095 */ 096 public POP3Connection(ProtocolProperties props) { 097 super(props); 098 099 // get our login properties flags 100 authEnabled = props.getBooleanProperty(MAIL_AUTH_ENABLED, false); 101 apopEnabled = props.getBooleanProperty(MAIL_APOP_ENABLED, false); 102 topDisabled = props.getBooleanProperty(MAIL_DISABLE_TOP, false); 103 } 104 105 106 /** 107 * Connect to the server and do the initial handshaking. 108 * 109 * @exception MessagingException 110 */ 111 public boolean protocolConnect(String host, int port, String authid, String realm, String username, String password) throws MessagingException { 112 this.serverHost = host; 113 this.serverPort = port; 114 this.realm = realm; 115 this.authid = authid; 116 this.username = username; 117 this.password = password; 118 119 try { 120 // create socket and connect to server. 121 getConnection(); 122 // consume the welcome line 123 getWelcome(); 124 125 // go login with the server 126 if (login()) 127 { 128 loggedIn = true; 129 return true; 130 } 131 return false; 132 } catch (IOException e) { 133 if (debug) { 134 debugOut("I/O exception establishing connection", e); 135 } 136 throw new MessagingException("Connection error", e); 137 } 138 } 139 140 141 /** 142 * Create a transport connection object and connect it to the 143 * target server. 144 * 145 * @exception MessagingException 146 */ 147 protected void getConnection() throws MessagingException 148 { 149 try { 150 // do all of the non-protocol specific set up. This will get our socket established 151 // and ready use. 152 super.getConnection(); 153 } catch (IOException e) { 154 throw new MessagingException("Unable to obtain a connection to the POP3 server", e); 155 } 156 157 // The POp3 protocol is inherently a string-based protocol, so we get 158 // string readers/writers for the connection streams 159 reader = new BufferedReader(new InputStreamReader(inputStream)); 160 writer = new PrintWriter(new BufferedOutputStream(outputStream)); 161 } 162 163 protected void getWelcome() throws IOException { 164 // just read the line and consume it. If debug is 165 // enabled, there I/O stream will be traced 166 greeting = reader.readLine(); 167 } 168 169 public String toString() { 170 return "POP3Connection host: " + serverHost + " port: " + serverPort; 171 } 172 173 174 /** 175 * Close the connection. On completion, we'll be disconnected from 176 * the server and unable to send more data. 177 * 178 * @exception MessagingException 179 */ 180 public void close() throws MessagingException { 181 // if we're already closed, get outta here. 182 if (socket == null) { 183 return; 184 } 185 try { 186 // say goodbye 187 logout(); 188 } finally { 189 // and close up the connection. We do this in a finally block to make sure the connection 190 // is shut down even if quit gets an error. 191 closeServerConnection(); 192 // get rid of our response processor too. 193 reader = null; 194 writer = null; 195 } 196 } 197 198 199 /** 200 * Tag this connection as having been closed by the 201 * server. This will not be returned to the 202 * connection pool. 203 */ 204 public void setClosed() { 205 closed = true; 206 } 207 208 /** 209 * Test if the connnection has been forcibly closed. 210 * 211 * @return True if the server disconnected the connection. 212 */ 213 public boolean isClosed() { 214 return closed; 215 } 216 217 protected POP3Response sendCommand(String cmd) throws MessagingException { 218 return sendCommand(cmd, false); 219 } 220 221 protected POP3Response sendMultiLineCommand(String cmd) throws MessagingException { 222 return sendCommand(cmd, true); 223 } 224 225 protected synchronized POP3Response sendCommand(String cmd, boolean multiLine) throws MessagingException { 226 if (socket.isConnected()) { 227 { 228 // NOTE: We don't use println() because it uses the platform concept of a newline rather 229 // than using CRLF, which is required by the POP3 protocol. 230 writer.write(cmd); 231 writer.write("\r\n"); 232 writer.flush(); 233 234 POP3Response response = buildResponse(multiLine); 235 if (response.isError()) { 236 throw new CommandFailedException("Error issuing POP3 command: " + cmd); 237 } 238 return response; 239 } 240 } 241 throw new MessagingException("Connection to Mail Server is lost, connection " + this.toString()); 242 } 243 244 /** 245 * Build a POP3Response item from the response stream. 246 * 247 * @param isMultiLineResponse 248 * If true, this command is expecting multiple lines back from the server. 249 * 250 * @return A POP3Response item with all of the command response data. 251 * @exception MessagingException 252 */ 253 protected POP3Response buildResponse(boolean isMultiLineResponse) throws MessagingException { 254 int status = ERR; 255 byte[] data = null; 256 257 String line; 258 MIMEInputReader source = new MIMEInputReader(reader); 259 260 try { 261 line = reader.readLine(); 262 } catch (IOException e) { 263 throw new MessagingException("Error in receving response"); 264 } 265 266 if (line == null || line.trim().equals("")) { 267 throw new MessagingException("Empty Response"); 268 } 269 270 if (line.startsWith("+OK")) { 271 status = OK; 272 line = removeStatusField(line); 273 if (isMultiLineResponse) { 274 data = getMultiLineResponse(); 275 } 276 } else if (line.startsWith("-ERR")) { 277 status = ERR; 278 line = removeStatusField(line); 279 }else if (line.startsWith("+")) { 280 status = CHALLENGE; 281 line = removeStatusField(line); 282 if (isMultiLineResponse) { 283 data = getMultiLineResponse(); 284 } 285 } else { 286 throw new MessagingException("Unexpected response: " + line); 287 } 288 return new POP3Response(status, line, data); 289 } 290 291 private static String removeStatusField(String line) { 292 return line.substring(line.indexOf(SPACE) + 1); 293 } 294 295 /** 296 * This could be a multiline response 297 */ 298 private byte[] getMultiLineResponse() throws MessagingException { 299 300 MIMEInputReader source = new MIMEInputReader(reader); 301 302 ByteArrayOutputStream out = new ByteArrayOutputStream(); 303 304 // it's more efficient to do this a buffer at a time. 305 // the MIMEInputReader takes care of the byte-stuffing and 306 // ".\r\n" input terminator for us. 307 OutputStreamWriter outWriter = new OutputStreamWriter(out); 308 char buffer[] = new char[500]; 309 try { 310 int charsRead = -1; 311 while ((charsRead = source.read(buffer)) >= 0) { 312 outWriter.write(buffer, 0, charsRead); 313 } 314 outWriter.flush(); 315 } catch (IOException e) { 316 throw new MessagingException("Error processing a multi-line response", e); 317 } 318 319 return out.toByteArray(); 320 } 321 322 323 /** 324 * Retrieve the raw message content from the POP3 325 * server. This is all of the message data, including 326 * the header. 327 * 328 * @param sequenceNumber 329 * The message sequence number. 330 * 331 * @return A byte array containing all of the message data. 332 * @exception MessagingException 333 */ 334 public byte[] retrieveMessageData(int sequenceNumber) throws MessagingException { 335 POP3Response msgResponse = sendMultiLineCommand("RETR " + sequenceNumber); 336 // we want the data directly in this case. 337 return msgResponse.getData(); 338 } 339 340 /** 341 * Retrieve the message header information for a given 342 * message, returned as an input stream suitable 343 * for loading the message data. 344 * 345 * @param sequenceNumber 346 * The server sequence number for the message. 347 * 348 * @return An inputstream that can be used to read the message 349 * data. 350 * @exception MessagingException 351 */ 352 public ByteArrayInputStream retrieveMessageHeaders(int sequenceNumber) throws MessagingException { 353 POP3Response msgResponse; 354 355 // some POP3 servers don't correctly implement TOP, so this can be disabled. If 356 // we can't use TOP, then use RETR and retrieve everything. We can just hand back 357 // the stream, as the header loading routine will stop at the first 358 // null line. 359 if (topDisabled) { 360 msgResponse = sendMultiLineCommand("RETR " + sequenceNumber); 361 } 362 else { 363 msgResponse = sendMultiLineCommand("TOP " + sequenceNumber + " 0"); 364 } 365 366 // just load the returned message data as a set of headers 367 return msgResponse.getContentStream(); 368 } 369 370 /** 371 * Retrieve the total message size from the mail 372 * server. This is the size of the headers plus 373 * the size of the message content. 374 * 375 * @param sequenceNumber 376 * The message sequence number. 377 * 378 * @return The full size of the message. 379 * @exception MessagingException 380 */ 381 public int retrieveMessageSize(int sequenceNumber) throws MessagingException { 382 POP3Response msgResponse = sendCommand("LIST " + sequenceNumber); 383 // Convert this into the parsed response type we need. 384 POP3ListResponse list = new POP3ListResponse(msgResponse); 385 // this returns the total message size 386 return list.getSize(); 387 } 388 389 /** 390 * Retrieve the mail drop status information. 391 * 392 * @return An object representing the returned mail drop status. 393 * @exception MessagingException 394 */ 395 public POP3StatusResponse retrieveMailboxStatus() throws MessagingException { 396 // issue the STAT command and return this into a status response 397 return new POP3StatusResponse(sendCommand("STAT")); 398 } 399 400 401 /** 402 * Retrieve the UID for an individual message. 403 * 404 * @param sequenceNumber 405 * The target message sequence number. 406 * 407 * @return The string UID maintained by the server. 408 * @exception MessagingException 409 */ 410 public String retrieveMessageUid(int sequenceNumber) throws MessagingException { 411 POP3Response msgResponse = sendCommand("UIDL " + sequenceNumber); 412 413 String message = msgResponse.getFirstLine(); 414 // the UID is everything after the blank separating the message number and the UID. 415 // there's not supposed to be anything else on the message, but trim it of whitespace 416 // just to be on the safe side. 417 return message.substring(message.indexOf(' ') + 1).trim(); 418 } 419 420 421 /** 422 * Delete a single message from the mail server. 423 * 424 * @param sequenceNumber 425 * The sequence number of the message to delete. 426 * 427 * @exception MessagingException 428 */ 429 public void deleteMessage(int sequenceNumber) throws MessagingException { 430 // just issue the command...we ignore the command response 431 sendCommand("DELE " + sequenceNumber); 432 } 433 434 /** 435 * Logout from the mail server. This sends a QUIT 436 * command, which will likely sever the mail connection. 437 * 438 * @exception MessagingException 439 */ 440 public void logout() throws MessagingException { 441 // we may have already sent the QUIT command 442 if (!loggedIn) { 443 return; 444 } 445 // just issue the command...we ignore the command response 446 sendCommand("QUIT"); 447 loggedIn = false; 448 } 449 450 /** 451 * Perform a reset on the mail server. 452 * 453 * @exception MessagingException 454 */ 455 public void reset() throws MessagingException { 456 // some mail servers mark retrieved messages for deletion 457 // automatically. This will reset the read flags before 458 // we go through normal cleanup. 459 if (props.getBooleanProperty(MAIL_RESET_QUIT, false)) { 460 // just send an RSET command first 461 sendCommand("RSET"); 462 } 463 } 464 465 /** 466 * Ping the mail server to see if we still have an active connection. 467 * 468 * @exception MessagingException thrown if we do not have an active connection. 469 */ 470 public void pingServer() throws MessagingException { 471 // just issue the command...we ignore the command response 472 sendCommand("NOOP"); 473 } 474 475 /** 476 * Login to the mail server, using whichever method is 477 * configured. This will try multiple methods, if allowed, 478 * in decreasing levels of security. 479 * 480 * @return true if the login was successful. 481 * @exception MessagingException 482 */ 483 public synchronized boolean login() throws MessagingException { 484 // permitted to use the AUTH command? 485 if (authEnabled) { 486 try { 487 // go do the SASL thing 488 return processSaslAuthentication(); 489 } catch (MessagingException e) { 490 // Any error here means fall back to the next mechanism 491 } 492 } 493 494 if (apopEnabled) { 495 try { 496 // go do the SASL thing 497 return processAPOPAuthentication(); 498 } catch (MessagingException e) { 499 // Any error here means fall back to the next mechanism 500 } 501 } 502 503 try { 504 // do the tried and true login processing. 505 return processLogin(); 506 } catch (MessagingException e) { 507 } 508 // everything failed...can't get in 509 return false; 510 } 511 512 513 /** 514 * Process a basic LOGIN operation, using the 515 * plain test USER/PASS command combo. 516 * 517 * @return true if we logged successfully. 518 * @exception MessagingException 519 */ 520 public boolean processLogin() throws MessagingException { 521 // start by sending the USER command, followed by 522 // the PASS command 523 sendCommand("USER " + username); 524 sendCommand("PASS " + password); 525 return true; // we're in 526 } 527 528 /** 529 * Process logging in using the APOP command. Only 530 * works on servers that give a timestamp value 531 * in the welcome response. 532 * 533 * @return true if the login was accepted. 534 * @exception MessagingException 535 */ 536 public boolean processAPOPAuthentication() throws MessagingException { 537 int timeStart = greeting.indexOf('<'); 538 // if we didn't get an APOP challenge on the greeting, throw an exception 539 // the main login processor will swallow that and fall back to the next 540 // mechanism 541 if (timeStart == -1) { 542 throw new MessagingException("POP3 Server does not support APOP"); 543 } 544 int timeEnd = greeting.indexOf('>'); 545 String timeStamp = greeting.substring(timeStart, timeEnd + 1); 546 547 // we create the digest password using the timestamp value sent to use 548 // concatenated with the password. 549 String digestPassword = timeStamp + password; 550 551 byte[] digest; 552 553 try { 554 // create a digest value from the password. 555 MessageDigest md = MessageDigest.getInstance("MD5"); 556 digest = md.digest(digestPassword.getBytes("iso-8859-1")); 557 } catch (NoSuchAlgorithmException e) { 558 // this shouldn't happen, but if it does, we'll just try a plain 559 // login. 560 throw new MessagingException("Unable to create MD5 digest", e); 561 } catch (UnsupportedEncodingException e) { 562 // this shouldn't happen, but if it does, we'll just try a plain 563 // login. 564 throw new MessagingException("Unable to create MD5 digest", e); 565 } 566 // this will throw an exception if it gives an error failure 567 sendCommand("APOP " + username + " " + Hex.encode(digest)); 568 // no exception, we must have passed 569 return true; 570 } 571 572 573 /** 574 * Process SASL-type authentication. 575 * 576 * @return Returns true if the server support a SASL authentication mechanism and 577 * accepted reponse challenges. 578 * @exception MessagingException 579 */ 580 protected boolean processSaslAuthentication() throws MessagingException { 581 // if unable to get an appropriate authenticator, just fail it. 582 ClientAuthenticator authenticator = getSaslAuthenticator(); 583 if (authenticator == null) { 584 throw new MessagingException("Unable to obtain SASL authenticator"); 585 } 586 587 // go process the login. 588 return processLogin(authenticator); 589 } 590 591 /** 592 * Attempt to retrieve a SASL authenticator for this 593 * protocol. 594 * 595 * @return A SASL authenticator, or null if a suitable one 596 * was not located. 597 */ 598 protected ClientAuthenticator getSaslAuthenticator() { 599 return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm); 600 } 601 602 603 /** 604 * Process a login using the provided authenticator object. 605 * 606 * NB: This method is synchronized because we have a multi-step process going on 607 * here. No other commands should be sent to the server until we complete. 608 * 609 * @return Returns true if the server support a SASL authentication mechanism and 610 * accepted reponse challenges. 611 * @exception MessagingException 612 */ 613 protected synchronized boolean processLogin(ClientAuthenticator authenticator) throws MessagingException { 614 if (debug) { 615 debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName()); 616 } 617 618 POP3Response response = sendCommand("AUTH " + authenticator.getMechanismName()); 619 620 // now process the challenge sequence. We get a continuation response back for each stage of the 621 // authentication, and finally an OK when everything passes muster. 622 while (true) { 623 // this should be a continuation reply, if things are still good. 624 if (response.isChallenge()) { 625 // we're passed back a challenge value, Base64 encoded. 626 byte[] challenge = response.decodeChallengeResponse(); 627 628 String responseString = new String(Base64.encode(authenticator.evaluateChallenge(challenge))); 629 630 // have the authenticator evaluate and send back the encoded response. 631 response = sendCommand(responseString); 632 } 633 else { 634 // there are only two choices here, OK or a continuation. OK means 635 // we've passed muster and are in. 636 return true; 637 } 638 } 639 } 640 641 642 /** 643 * Merge the configured SASL mechanisms with the capabilities that the 644 * server has indicated it supports, returning a merged list that can 645 * be used for selecting a mechanism. 646 * 647 * @return A List representing the intersection of the configured list and the 648 * capabilities list. 649 */ 650 protected List selectSaslMechanisms() { 651 // just return the set that have been explicity permitted 652 return getSaslMechanisms(); 653 } 654 } 655