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.imap.connection; 019 020 import java.io.IOException; 021 import java.io.InputStream; 022 import java.io.OutputStream; 023 import java.io.PrintStream; 024 import java.io.UnsupportedEncodingException; 025 import java.io.Writer; 026 import java.net.InetAddress; 027 import java.net.Socket; 028 import java.net.SocketException; 029 import java.net.UnknownHostException; 030 import java.util.ArrayList; 031 import java.util.Date; 032 import java.util.HashMap; 033 import java.util.Iterator; 034 import java.util.LinkedList; 035 import java.util.List; 036 import java.util.StringTokenizer; 037 038 import javax.mail.Address; 039 import javax.mail.AuthenticationFailedException; 040 import javax.mail.FetchProfile; 041 import javax.mail.Flags; 042 import javax.mail.Folder; 043 import javax.mail.Message; 044 import javax.mail.MessagingException; 045 import javax.mail.MethodNotSupportedException; 046 import javax.mail.Quota; 047 import javax.mail.Session; 048 import javax.mail.UIDFolder; 049 import javax.mail.URLName; 050 051 import javax.mail.internet.InternetHeaders; 052 053 import javax.mail.search.SearchTerm; 054 055 import org.apache.geronimo.javamail.authentication.AuthenticatorFactory; 056 import org.apache.geronimo.javamail.authentication.ClientAuthenticator; 057 import org.apache.geronimo.javamail.authentication.LoginAuthenticator; 058 import org.apache.geronimo.javamail.authentication.PlainAuthenticator; 059 import org.apache.geronimo.javamail.store.imap.ACL; 060 import org.apache.geronimo.javamail.store.imap.Rights; 061 062 import org.apache.geronimo.javamail.util.CommandFailedException; 063 import org.apache.geronimo.javamail.util.InvalidCommandException; 064 import org.apache.geronimo.javamail.util.MailConnection; 065 import org.apache.geronimo.javamail.util.ProtocolProperties; 066 import org.apache.geronimo.javamail.util.TraceInputStream; 067 import org.apache.geronimo.javamail.util.TraceOutputStream; 068 import org.apache.geronimo.mail.util.Base64; 069 070 /** 071 * Simple implementation of IMAP transport. Just does plain RFC977-ish 072 * delivery. 073 * <p/> 074 * There is no way to indicate failure for a given recipient (it's possible to have a 075 * recipient address rejected). The sun impl throws exceptions even if others successful), 076 * but maybe we do a different way... 077 * <p/> 078 * 079 * @version $Rev: 739377 $ $Date: 2009-01-30 13:57:39 -0500 (Fri, 30 Jan 2009) $ 080 */ 081 public class IMAPConnection extends MailConnection { 082 083 protected static final String CAPABILITY_LOGIN_DISABLED = "LOGINDISABLED"; 084 085 // The connection pool we're a member of. This keeps holds most of the 086 // connnection parameter information for us. 087 protected IMAPConnectionPool pool; 088 089 // special input stream for reading individual response lines. 090 protected IMAPResponseStream reader; 091 092 // connection pool connections. 093 protected long lastAccess = 0; 094 // our handlers for any untagged responses 095 protected LinkedList responseHandlers = new LinkedList(); 096 // the list of queued untagged responses. 097 protected List queuedResponses = new LinkedList(); 098 // this is set on if we had a forced disconnect situation from 099 // the server. 100 protected boolean closed = false; 101 102 /** 103 * Normal constructor for an IMAPConnection() object. 104 * 105 * @param props The protocol properties abstraction containing our 106 * property modifiers. 107 * @param pool 108 */ 109 public IMAPConnection(ProtocolProperties props, IMAPConnectionPool pool) { 110 super(props); 111 this.pool = pool; 112 } 113 114 115 /** 116 * Connect to the server and do the initial handshaking. 117 * 118 * @exception MessagingException 119 */ 120 public boolean protocolConnect(String host, int port, String authid, String realm, String username, String password) throws MessagingException { 121 this.serverHost = host; 122 this.serverPort = port; 123 this.realm = realm; 124 this.authid = authid; 125 this.username = username; 126 this.password = password; 127 128 boolean preAuthorized = false; 129 130 try { 131 // create socket and connect to server. 132 getConnection(); 133 134 // we need to ask the server what its capabilities are. This can be done 135 // before we login. 136 getCapability(); 137 // do a preauthoriziation check. 138 if (extractResponse("PREAUTH") != null) { 139 preAuthorized = true; 140 } 141 142 // make sure we process these now 143 processPendingResponses(); 144 145 // if we're not already using an SSL connection, and we have permission to issue STARTTLS, AND 146 // the server supports this, then switch to TLS mode before continuing. 147 if (!sslConnection && props.getBooleanProperty(MAIL_STARTTLS_ENABLE, false) && hasCapability(CAPABILITY_STARTTLS)) { 148 // if the server supports TLS, then use it for the connection. 149 // on our connection. 150 151 // tell the server of our intention to start a TLS session 152 sendSimpleCommand("STARTTLS"); 153 154 // The connection is then handled by the superclass level. 155 getConnectedTLSSocket(); 156 157 // create the special reader for pulling the responses. 158 reader = new IMAPResponseStream(inputStream); 159 160 // the IMAP spec states that the capability response is independent of login state or 161 // user, but I'm not sure I believe that to be the case. It doesn't hurt to refresh 162 // the information again after establishing a secure connection. 163 getCapability(); 164 // and we need to repeat this check. 165 if (extractResponse("PREAUTH") != null) { 166 preAuthorized = true; 167 } 168 } 169 170 // damn, no login required. 171 if (preAuthorized) { 172 return true; 173 } 174 175 // go login with the server 176 return login(); 177 } catch (IOException e) { 178 if (debug) { 179 debugOut("I/O exception establishing connection", e); 180 } 181 throw new MessagingException("Connection error", e); 182 } 183 finally { 184 // make sure the queue is cleared 185 processPendingResponses(); 186 } 187 } 188 189 /** 190 * Update the last access time for the connection. 191 */ 192 protected void updateLastAccess() { 193 lastAccess = System.currentTimeMillis(); 194 } 195 196 /** 197 * Test if the connection has been sitting idle for longer than 198 * the set timeout period. 199 * 200 * @param timeout The allowed "freshness" interval. 201 * 202 * @return True if the connection has been active within the required 203 * interval, false if it has been sitting idle for too long. 204 */ 205 public boolean isStale(long timeout) { 206 return (System.currentTimeMillis() - lastAccess) > timeout; 207 } 208 209 210 /** 211 * Close the connection. On completion, we'll be disconnected from 212 * the server and unable to send more data. 213 * 214 * @exception MessagingException 215 */ 216 public void close() throws MessagingException { 217 // if we're already closed, get outta here. 218 if (socket == null) { 219 return; 220 } 221 try { 222 // say goodbye 223 logout(); 224 } finally { 225 // and close up the connection. We do this in a finally block to make sure the connection 226 // is shut down even if quit gets an error. 227 closeServerConnection(); 228 // get rid of our response processor too. 229 reader = null; 230 } 231 } 232 233 234 /** 235 * Create a transport connection object and connect it to the 236 * target server. 237 * 238 * @exception MessagingException 239 */ 240 protected void getConnection() throws IOException, MessagingException 241 { 242 // do all of the non-protocol specific set up. This will get our socket established 243 // and ready use. 244 super.getConnection(); 245 // create the special reader for pulling the responses. 246 reader = new IMAPResponseStream(inputStream); 247 248 // set the initial access time stamp 249 updateLastAccess(); 250 } 251 252 253 /** 254 * Process a simple command/response sequence between the 255 * client and the server. These are commands where the 256 * client is expecting them to "just work", and also will not 257 * directly process the reply information. Unsolicited untagged 258 * responses are dispatched to handlers, and a MessagingException 259 * will be thrown for any non-OK responses from the server. 260 * 261 * @param data The command data we're writing out. 262 * 263 * @exception MessagingException 264 */ 265 public void sendSimpleCommand(String data) throws MessagingException { 266 // create a command object and issue the command with that. 267 IMAPCommand command = new IMAPCommand(data); 268 sendSimpleCommand(command); 269 } 270 271 272 /** 273 * Process a simple command/response sequence between the 274 * client and the server. These are commands where the 275 * client is expecting them to "just work", and also will not 276 * directly process the reply information. Unsolicited untagged 277 * responses are dispatched to handlers, and a MessagingException 278 * will be thrown for any non-OK responses from the server. 279 * 280 * @param data The command data we're writing out. 281 * 282 * @exception MessagingException 283 */ 284 public void sendSimpleCommand(IMAPCommand data) throws MessagingException { 285 // the command sending process will raise exceptions for bad responses.... 286 // we just need to send the command and forget about it. 287 sendCommand(data); 288 } 289 290 291 /** 292 * Sends a command down the socket, returning the server response. 293 * 294 * @param data The String form of the command. 295 * 296 * @return The tagged response information that terminates the command interaction. 297 * @exception MessagingException 298 */ 299 public IMAPTaggedResponse sendCommand(String data) throws MessagingException { 300 IMAPCommand command = new IMAPCommand(data); 301 return sendCommand(command); 302 } 303 304 305 /** 306 * Sends a command down the socket, returning the server response. 307 * 308 * @param data An IMAPCommand object with the prepared command information. 309 * 310 * @return The tagged (or continuation) response information that terminates the 311 * command response sequence. 312 * @exception MessagingException 313 */ 314 public synchronized IMAPTaggedResponse sendCommand(IMAPCommand data) throws MessagingException { 315 // check first 316 checkConnected(); 317 try { 318 // have the command write the command data. This also prepends a tag. 319 data.writeTo(outputStream, this); 320 outputStream.flush(); 321 // update the activity timestamp 322 updateLastAccess(); 323 // get the received response 324 return receiveResponse(); 325 } catch (IOException e) { 326 throw new MessagingException(e.toString(), e); 327 } 328 } 329 330 331 /** 332 * Sends a message down the socket and terminates with the 333 * appropriate CRLF 334 * 335 * @param data The string data to send. 336 * 337 * @return An IMAPTaggedResponse item returned from the server. 338 * @exception MessagingException 339 */ 340 public IMAPTaggedResponse sendLine(String data) throws MessagingException { 341 return sendLine(data.getBytes()); 342 } 343 344 345 /** 346 * Sends a message down the socket and terminates with the 347 * appropriate CRLF 348 * 349 * @param data The array of data to send to the server. 350 * 351 * @return The response item returned from the IMAP server. 352 * @exception MessagingException 353 */ 354 public IMAPTaggedResponse sendLine(byte[] data) throws MessagingException { 355 return sendLine(data, 0, data.length); 356 } 357 358 359 /** 360 * Sends a message down the socket and terminates with the 361 * appropriate CRLF 362 * 363 * @param data The source data array. 364 * @param offset The offset within the data array. 365 * @param length The length of data to send. 366 * 367 * @return The response line returned from the IMAP server. 368 * @exception MessagingException 369 */ 370 public synchronized IMAPTaggedResponse sendLine(byte[] data, int offset, int length) throws MessagingException { 371 // check first 372 checkConnected(); 373 374 try { 375 outputStream.write(data, offset, length); 376 outputStream.write(CR); 377 outputStream.write(LF); 378 outputStream.flush(); 379 // update the activity timestamp 380 updateLastAccess(); 381 return receiveResponse(); 382 } catch (IOException e) { 383 throw new MessagingException(e.toString(), e); 384 } 385 } 386 387 388 /** 389 * Get a reply line for an IMAP command. 390 * 391 * @return An IMAP reply object from the stream. 392 */ 393 public IMAPTaggedResponse receiveResponse() throws MessagingException { 394 while (true) { 395 // read and parse a response from the server. 396 IMAPResponse response = reader.readResponse(); 397 // The response set is terminated by either a continuation response or a 398 // tagged response (we only have a single command active at one time). 399 if (response instanceof IMAPTaggedResponse) { 400 // update the access time stamp for later timeout processing. 401 updateLastAccess(); 402 IMAPTaggedResponse tagged = (IMAPTaggedResponse)response; 403 // we turn these into exceptions here, which means the issuer doesn't have to 404 // worry about checking status. 405 if (tagged.isBAD()) { 406 throw new InvalidCommandException("Unexpected command IMAP command error"); 407 } 408 else if (tagged.isNO()) { 409 throw new CommandFailedException("Unexpected error executing IMAP command"); 410 } 411 return tagged; 412 } 413 else { 414 // all other unsolicited responses are either async status updates or 415 // additional elements of a command we just sent. These will be processed 416 // either during processing of the command response, or at the end of the 417 // current command processing. 418 queuePendingResponse((IMAPUntaggedResponse)response); 419 } 420 } 421 } 422 423 424 /** 425 * Get the servers capabilities from the wire.... 426 */ 427 public void getCapability() throws MessagingException { 428 sendCommand("CAPABILITY"); 429 // get the capabilities from the response. 430 IMAPCapabilityResponse response = (IMAPCapabilityResponse)extractResponse("CAPABILITY"); 431 capabilities = response.getCapabilities(); 432 authentications = response.getAuthentications(); 433 } 434 435 /** 436 * Logs out from the server. 437 */ 438 public void logout() throws MessagingException { 439 // We can just send the command and generally ignore the 440 // status response. 441 sendCommand("LOGOUT"); 442 } 443 444 /** 445 * Deselect a mailbox when a folder returns a connection. 446 * 447 * @exception MessagingException 448 */ 449 public void closeMailbox() throws MessagingException { 450 // We can just send the command and generally ignore the 451 // status response. 452 sendCommand("CLOSE"); 453 } 454 455 456 /** 457 * Authenticate with the server, if necessary (or possible). 458 * 459 * @return true if we were able to authenticate correctly, false for authentication failures. 460 * @exception MessagingException 461 */ 462 protected boolean login() throws MessagingException 463 { 464 // if no username or password, fail this immediately. 465 // the base connect property should resolve a username/password combo for us and 466 // try again. 467 if (username == null || password == null) { 468 return false; 469 } 470 471 // are we permitted to use SASL mechanisms? 472 if (props.getBooleanProperty(MAIL_SASL_ENABLE, false)) { 473 // we might be enable for SASL, but the client and the server might 474 // not have any supported mechanisms in common. Try again with another 475 // mechanism. 476 if (processSaslAuthentication()) { 477 return true; 478 } 479 } 480 481 // see if we're allowed to try plain. 482 if (!props.getBooleanProperty(MAIL_PLAIN_DISABLE, false) && supportsMechanism(AUTHENTICATION_PLAIN)) { 483 return processPlainAuthentication(); 484 } 485 486 // see if we're allowed to try login. 487 if (!props.getBooleanProperty(MAIL_LOGIN_DISABLE, false) && supportsMechanism(AUTHENTICATION_LOGIN)) { 488 // no authzid capability with this authentication method. 489 return processLoginAuthentication(); 490 } 491 492 // the server can choose to disable the LOGIN command. If not disabled, try 493 // using LOGIN rather than AUTHENTICATE. 494 if (!hasCapability(CAPABILITY_LOGIN_DISABLED)) { 495 return processLogin(); 496 } 497 498 throw new MessagingException("No supported LOGIN methods enabled"); 499 } 500 501 502 /** 503 * Process SASL-type authentication. 504 * 505 * @return Returns true if the server support a SASL authentication mechanism and 506 * accepted reponse challenges. 507 * @exception MessagingException 508 */ 509 protected boolean processSaslAuthentication() throws MessagingException { 510 // if unable to get an appropriate authenticator, just fail it. 511 ClientAuthenticator authenticator = getSaslAuthenticator(); 512 if (authenticator == null) { 513 return false; 514 } 515 516 // go process the login. 517 return processLogin(authenticator); 518 } 519 520 protected ClientAuthenticator getSaslAuthenticator() { 521 return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm); 522 } 523 524 /** 525 * Process SASL-type PLAIN authentication. 526 * 527 * @return Returns true if the login is accepted. 528 * @exception MessagingException 529 */ 530 protected boolean processPlainAuthentication() throws MessagingException { 531 // go process the login. 532 return processLogin(new PlainAuthenticator(username, password)); 533 } 534 535 536 /** 537 * Process SASL-type LOGIN authentication. 538 * 539 * @return Returns true if the login is accepted. 540 * @exception MessagingException 541 */ 542 protected boolean processLoginAuthentication() throws MessagingException { 543 // go process the login. 544 return processLogin(new LoginAuthenticator(username, password)); 545 } 546 547 548 /** 549 * Process a LOGIN using the LOGIN command instead of AUTHENTICATE. 550 * 551 * @return true if the command succeeded, false for any authentication failures. 552 * @exception MessagingException 553 */ 554 protected boolean processLogin() throws MessagingException { 555 // arguments are "LOGIN userid password" 556 IMAPCommand command = new IMAPCommand("LOGIN"); 557 command.appendAtom(username); 558 command.appendAtom(password); 559 560 // go issue the command 561 try { 562 sendCommand(command); 563 } catch (CommandFailedException e) { 564 // we'll get a NO response for a rejected login 565 return false; 566 } 567 // seemed to work ok.... 568 return true; 569 } 570 571 572 /** 573 * Process a login using the provided authenticator object. 574 * 575 * NB: This method is synchronized because we have a multi-step process going on 576 * here. No other commands should be sent to the server until we complete. 577 * 578 * @return Returns true if the server support a SASL authentication mechanism and 579 * accepted reponse challenges. 580 * @exception MessagingException 581 */ 582 protected synchronized boolean processLogin(ClientAuthenticator authenticator) throws MessagingException { 583 if (debug) { 584 debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName()); 585 } 586 587 IMAPCommand command = new IMAPCommand("AUTHENTICATE"); 588 // and tell the server which mechanism we're using. 589 command.appendAtom(authenticator.getMechanismName()); 590 // send the command now 591 592 try { 593 IMAPTaggedResponse response = sendCommand(command); 594 595 // now process the challenge sequence. We get a 235 response back when the server accepts the 596 // authentication, and a 334 indicates we have an additional challenge. 597 while (true) { 598 // this should be a continuation reply, if things are still good. 599 if (response.isContinuation()) { 600 // we're passed back a challenge value, Base64 encoded. 601 byte[] challenge = response.decodeChallengeResponse(); 602 603 // have the authenticator evaluate and send back the encoded response. 604 response = sendLine(Base64.encode(authenticator.evaluateChallenge(challenge))); 605 } 606 else { 607 // there are only two choices here, OK or a continuation. OK means 608 // we've passed muster and are in. 609 return true; 610 } 611 } 612 } catch (CommandFailedException e ) { 613 // a failure at any point in this process will result in a "NO" response. 614 // That causes an exception to get thrown, so just fail the login 615 // if we get one. 616 return false; 617 } 618 } 619 620 621 /** 622 * Return the server host for this connection. 623 * 624 * @return The String name of the server host. 625 */ 626 public String getHost() { 627 return serverHost; 628 } 629 630 631 /** 632 * Attach a handler for untagged responses to this connection. 633 * 634 * @param h The new untagged response handler. 635 */ 636 public synchronized void addResponseHandler(IMAPUntaggedResponseHandler h) { 637 responseHandlers.add(h); 638 } 639 640 641 /** 642 * Remove a response handler from the connection. 643 * 644 * @param h The handler to remove. 645 */ 646 public synchronized void removeResponseHandler(IMAPUntaggedResponseHandler h) { 647 responseHandlers.remove(h); 648 } 649 650 651 /** 652 * Add a response to the pending untagged response queue. 653 * 654 * @param response The response to add. 655 */ 656 public synchronized void queuePendingResponse(IMAPUntaggedResponse response) { 657 queuedResponses.add(response); 658 } 659 660 /** 661 * Process any untagged responses in the queue. This will clear out 662 * the queue, and send each response to the registered 663 * untagged response handlers. 664 */ 665 public void processPendingResponses() throws MessagingException { 666 List pendingResponses = null; 667 List handlerList = null; 668 669 synchronized(this) { 670 if (queuedResponses.isEmpty()) { 671 return; 672 } 673 pendingResponses = queuedResponses; 674 queuedResponses = new LinkedList(); 675 // get a copy of the response handlers so we can 676 // release the connection lock before broadcasting 677 handlerList = (List)responseHandlers.clone(); 678 } 679 680 for (int i = 0; i < pendingResponses.size(); i++) { 681 IMAPUntaggedResponse response = (IMAPUntaggedResponse)pendingResponses.get(i); 682 for (int j = 0; j < handlerList.size(); j++) { 683 // broadcast to each handler. If a handler returns true, then it 684 // handled whatever this message required and we should skip sending 685 // it to other handlers. 686 IMAPUntaggedResponseHandler h = (IMAPUntaggedResponseHandler)handlerList.get(j); 687 if (h.handleResponse(response)) { 688 break; 689 } 690 } 691 } 692 } 693 694 /** 695 * Extract a single response from the pending queue that 696 * match a give keyword type. All matching responses 697 * are removed from the pending queue. 698 * 699 * @param type The string name of the keyword. 700 * 701 * @return A List of all matching queued responses. 702 */ 703 public IMAPUntaggedResponse extractResponse(String type) { 704 Iterator i = queuedResponses.iterator(); 705 while (i.hasNext()) { 706 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next(); 707 // if this is of the target type, move it to the response set. 708 if (response.isKeyword(type)) { 709 i.remove(); 710 return response; 711 } 712 } 713 return null; 714 } 715 716 /** 717 * Extract all responses from the pending queue that 718 * match a give keyword type. All matching responses 719 * are removed from the pending queue. 720 * 721 * @param type The string name of the keyword. 722 * 723 * @return A List of all matching queued responses. 724 */ 725 public List extractResponses(String type) { 726 List responses = new ArrayList(); 727 728 Iterator i = queuedResponses.iterator(); 729 while (i.hasNext()) { 730 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next(); 731 // if this is of the target type, move it to the response set. 732 if (response.isKeyword(type)) { 733 i.remove(); 734 responses.add(response); 735 } 736 } 737 return responses; 738 } 739 740 741 /** 742 * Extract all responses from the pending queue that 743 * are "FETCH" responses for a given message number. All matching responses 744 * are removed from the pending queue. 745 * 746 * @param type The string name of the keyword. 747 * 748 * @return A List of all matching queued responses. 749 */ 750 public List extractFetchResponses(int sequenceNumber) { 751 List responses = new ArrayList(); 752 753 Iterator i = queuedResponses.iterator(); 754 while (i.hasNext()) { 755 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next(); 756 // if this is of the target type, move it to the response set. 757 if (response.isKeyword("FETCH")) { 758 IMAPFetchResponse fetch = (IMAPFetchResponse)response; 759 // a response for the correct message number? 760 if (fetch.sequenceNumber == sequenceNumber) { 761 // pluck these from the list and add to the response set. 762 i.remove(); 763 responses.add(response); 764 } 765 } 766 } 767 return responses; 768 } 769 770 /** 771 * Extract a fetch response data item from the queued elements. 772 * 773 * @param sequenceNumber 774 * The message number we're interested in. Fetch responses for other messages 775 * will be skipped. 776 * @param type The type of body element we need. It is assumed that only one item for 777 * the given message number will exist in the queue. The located item will 778 * be returned, and that fetch response will be removed from the pending queue. 779 * 780 * @return The target data item, or null if a match is not found. 781 */ 782 protected IMAPFetchDataItem extractFetchDataItem(long sequenceNumber, int type) 783 { 784 Iterator i = queuedResponses.iterator(); 785 while (i.hasNext()) { 786 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next(); 787 // if this is of the target type, move it to the response set. 788 if (response.isKeyword("FETCH")) { 789 IMAPFetchResponse fetch = (IMAPFetchResponse)response; 790 // a response for the correct message number? 791 if (fetch.sequenceNumber == sequenceNumber) { 792 // does this response have the item we're looking for? 793 IMAPFetchDataItem item = fetch.getDataItem(type); 794 if (item != null) { 795 // remove this from the pending queue and return the 796 // located item 797 i.remove(); 798 return item; 799 } 800 } 801 } 802 } 803 // not located, sorry 804 return null; 805 } 806 807 /** 808 * Extract a all fetch responses that contain a given data item. 809 * 810 * @param type The type of body element we need. It is assumed that only one item for 811 * the given message number will exist in the queue. The located item will 812 * be returned, and that fetch response will be removed from the pending queue. 813 * 814 * @return A List of all matching Fetch responses. 815 */ 816 protected List extractFetchDataItems(int type) 817 { 818 Iterator i = queuedResponses.iterator(); 819 List items = new ArrayList(); 820 821 while (i.hasNext()) { 822 IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next(); 823 // if this is of the target type, move it to the response set. 824 if (response.isKeyword("FETCH")) { 825 IMAPFetchResponse fetch = (IMAPFetchResponse)response; 826 // does this response have the item we're looking for? 827 IMAPFetchDataItem item = fetch.getDataItem(type); 828 if (item != null) { 829 // remove this from the pending queue and return the 830 // located item 831 i.remove(); 832 // we want the fetch response, not the data item, because 833 // we're going to require the message sequence number information 834 // too. 835 items.add(fetch); 836 } 837 } 838 } 839 // return whatever we have. 840 return items; 841 } 842 843 /** 844 * Make sure we have the latest status information available. We 845 * retreive this by sending a NOOP command to the server, and 846 * processing any untagged responses we get back. 847 */ 848 public void updateMailboxStatus() throws MessagingException { 849 sendSimpleCommand("NOOP"); 850 } 851 852 853 /** 854 * check to see if this connection is truely alive. 855 * 856 * @param timeout The timeout value to control how often we ping 857 * the server to see if we're still good. 858 * 859 * @return true if the server is responding to requests, false for any 860 * connection errors. This will also update the folder status 861 * by processing returned unsolicited messages. 862 */ 863 public synchronized boolean isAlive(long timeout) { 864 long lastUsed = System.currentTimeMillis() - lastAccess; 865 if (lastUsed < timeout) { 866 return true; 867 } 868 869 try { 870 sendSimpleCommand("NOOP"); 871 return true; 872 } catch (MessagingException e) { 873 // the NOOP command will throw a MessagingException if we get anything 874 // other than an OK response back from the server. 875 } 876 return false; 877 } 878 879 880 /** 881 * Issue a fetch command to retrieve the message ENVELOPE structure. 882 * 883 * @param sequenceNumber The sequence number of the message. 884 * 885 * @return The IMAPResponse item containing the ENVELOPE information. 886 */ 887 public synchronized List fetchEnvelope(int sequenceNumber) throws MessagingException { 888 IMAPCommand command = new IMAPCommand("FETCH"); 889 command.appendInteger(sequenceNumber); 890 command.startList(); 891 command.appendAtom("ENVELOPE INTERNALDATE RFC822.SIZE"); 892 command.endList(); 893 894 // we want all of the envelope information about the message, which involves multiple FETCH chunks. 895 sendCommand(command); 896 // these are fairly involved sets, so the caller needs to handle these. 897 // we just return all of the FETCH results matching the target message number. 898 return extractFetchResponses(sequenceNumber); 899 } 900 901 /** 902 * Issue a FETCH command to retrieve the message BODYSTRUCTURE structure. 903 * 904 * @param sequenceNumber The sequence number of the message. 905 * 906 * @return The IMAPBodyStructure item for the message. 907 * All other untagged responses are queued for processing. 908 */ 909 public synchronized IMAPBodyStructure fetchBodyStructure(int sequenceNumber) throws MessagingException { 910 IMAPCommand command = new IMAPCommand("FETCH"); 911 command.appendInteger(sequenceNumber); 912 command.startList(); 913 command.appendAtom("BODYSTRUCTURE"); 914 command.endList(); 915 916 // we want all of the envelope information about the message, which involves multiple FETCH chunks. 917 sendCommand(command); 918 // locate the response from this 919 IMAPBodyStructure bodyStructure = (IMAPBodyStructure)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.BODYSTRUCTURE); 920 921 if (bodyStructure == null) { 922 throw new MessagingException("No BODYSTRUCTURE information received from IMAP server"); 923 } 924 // and return the body structure directly. 925 return bodyStructure; 926 } 927 928 929 /** 930 * Issue a FETCH command to retrieve the message RFC822.HEADERS structure containing the message headers (using PEEK). 931 * 932 * @param sequenceNumber The sequence number of the message. 933 * 934 * @return The IMAPRFC822Headers item for the message. 935 * All other untagged responses are queued for processing. 936 */ 937 public synchronized InternetHeaders fetchHeaders(int sequenceNumber, String part) throws MessagingException { 938 IMAPCommand command = new IMAPCommand("FETCH"); 939 command.appendInteger(sequenceNumber); 940 command.startList(); 941 command.appendAtom("BODY.PEEK"); 942 command.appendBodySection(part, "HEADER"); 943 command.endList(); 944 945 // we want all of the envelope information about the message, which involves multiple FETCH chunks. 946 sendCommand(command); 947 IMAPInternetHeader header = (IMAPInternetHeader)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.HEADER); 948 949 if (header == null) { 950 throw new MessagingException("No HEADER information received from IMAP server"); 951 } 952 // and return the body structure directly. 953 return header.headers; 954 } 955 956 957 /** 958 * Issue a FETCH command to retrieve the message text 959 * 960 * @param sequenceNumber The sequence number of the message. 961 * 962 * @return The IMAPMessageText item for the message. 963 * All other untagged responses are queued for processing. 964 */ 965 public synchronized IMAPMessageText fetchText(int sequenceNumber) throws MessagingException { 966 IMAPCommand command = new IMAPCommand("FETCH"); 967 command.appendInteger(sequenceNumber); 968 command.startList(); 969 command.appendAtom("BODY.PEEK"); 970 command.appendBodySection("TEXT"); 971 command.endList(); 972 973 // we want all of the envelope information about the message, which involves multiple FETCH chunks. 974 sendCommand(command); 975 IMAPMessageText text = (IMAPMessageText)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.TEXT); 976 977 if (text == null) { 978 throw new MessagingException("No TEXT information received from IMAP server"); 979 } 980 // and return the body structure directly. 981 return text; 982 } 983 984 985 /** 986 * Issue a FETCH command to retrieve the message text 987 * 988 * @param sequenceNumber The sequence number of the message. 989 * 990 * @return The IMAPMessageText item for the message. 991 * All other untagged responses are queued for processing. 992 */ 993 public synchronized IMAPMessageText fetchBodyPartText(int sequenceNumber, String section) throws MessagingException { 994 IMAPCommand command = new IMAPCommand("FETCH"); 995 command.appendInteger(sequenceNumber); 996 command.startList(); 997 command.appendAtom("BODY.PEEK"); 998 command.appendBodySection(section, "TEXT"); 999 command.endList(); 1000 1001 // we want all of the envelope information about the message, which involves multiple FETCH chunks. 1002 sendCommand(command); 1003 IMAPMessageText text = (IMAPMessageText)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.TEXT); 1004 1005 if (text == null) { 1006 throw new MessagingException("No TEXT information received from IMAP server"); 1007 } 1008 // and return the body structure directly. 1009 return text; 1010 } 1011 1012 1013 /** 1014 * Issue a FETCH command to retrieve the entire message body in one shot. 1015 * This may also be used to fetch an embedded message part as a unit. 1016 * 1017 * @param sequenceNumber 1018 * The sequence number of the message. 1019 * @param section The section number to fetch. If null, the entire body of the message 1020 * is retrieved. 1021 * 1022 * @return The IMAPBody item for the message. 1023 * All other untagged responses are queued for processing. 1024 * @exception MessagingException 1025 */ 1026 public synchronized IMAPBody fetchBody(int sequenceNumber, String section) throws MessagingException { 1027 IMAPCommand command = new IMAPCommand("FETCH"); 1028 command.appendInteger(sequenceNumber); 1029 command.startList(); 1030 command.appendAtom("BODY.PEEK"); 1031 // no part name here, only the section identifier. This will fetch 1032 // the entire body, with all of the bits in place. 1033 command.appendBodySection(section, null); 1034 command.endList(); 1035 1036 // we want all of the envelope information about the message, which involves multiple FETCH chunks. 1037 sendCommand(command); 1038 IMAPBody body = (IMAPBody)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.BODY); 1039 1040 if (body == null) { 1041 throw new MessagingException("No BODY information received from IMAP server"); 1042 } 1043 // and return the body structure directly. 1044 return body; 1045 } 1046 1047 1048 /** 1049 * Fetch the message content. This sorts out which method should be used 1050 * based on the server capability. 1051 * 1052 * @param sequenceNumber 1053 * The sequence number of the target message. 1054 * 1055 * @return The byte[] content information. 1056 * @exception MessagingException 1057 */ 1058 public byte[] fetchContent(int sequenceNumber) throws MessagingException { 1059 // fetch the text item and return the data 1060 IMAPMessageText text = fetchText(sequenceNumber); 1061 return text.getContent(); 1062 } 1063 1064 1065 /** 1066 * Fetch the message content. This sorts out which method should be used 1067 * based on the server capability. 1068 * 1069 * @param sequenceNumber 1070 * The sequence number of the target message. 1071 * 1072 * @return The byte[] content information. 1073 * @exception MessagingException 1074 */ 1075 public byte[] fetchContent(int sequenceNumber, String section) throws MessagingException { 1076 if (section == null) { 1077 IMAPMessageText text = fetchText(sequenceNumber); 1078 return text.getContent(); 1079 } else { 1080 IMAPBody body = fetchBody(sequenceNumber, section); 1081 return body.getContent(); 1082 } 1083 } 1084 1085 1086 /** 1087 * Send an LIST command to the IMAP server, returning all LIST 1088 * response information. 1089 * 1090 * @param mailbox The reference mailbox name sent on the command. 1091 * @param pattern The match pattern used on the name. 1092 * 1093 * @return A List of all LIST response information sent back from the server. 1094 */ 1095 public synchronized List list(String mailbox, String pattern) throws MessagingException { 1096 IMAPCommand command = new IMAPCommand("LIST"); 1097 1098 // construct the command, encoding the tokens as required by the content. 1099 command.appendEncodedString(mailbox); 1100 command.appendEncodedString(pattern); 1101 1102 sendCommand(command); 1103 1104 // pull out the ones we're interested in 1105 return extractResponses("LIST"); 1106 } 1107 1108 1109 /** 1110 * Send an LSUB command to the IMAP server, returning all LSUB 1111 * response information. 1112 * 1113 * @param mailbox The reference mailbox name sent on the command. 1114 * @param pattern The match pattern used on the name. 1115 * 1116 * @return A List of all LSUB response information sent back from the server. 1117 */ 1118 public List listSubscribed(String mailbox, String pattern) throws MessagingException { 1119 IMAPCommand command = new IMAPCommand("LSUB"); 1120 1121 // construct the command, encoding the tokens as required by the content. 1122 command.appendEncodedString(mailbox); 1123 command.appendEncodedString(pattern); 1124 1125 sendCommand(command); 1126 // pull out the ones we're interested in 1127 return extractResponses("LSUB"); 1128 } 1129 1130 1131 /** 1132 * Subscribe to a give mailbox. 1133 * 1134 * @param mailbox The desired mailbox name. 1135 * 1136 * @exception MessagingException 1137 */ 1138 public void subscribe(String mailbox) throws MessagingException { 1139 IMAPCommand command = new IMAPCommand("SUBSCRIBE"); 1140 // add on the encoded mailbox name, as the appropriate token type. 1141 command.appendEncodedString(mailbox); 1142 1143 // send this, and ignore the response. 1144 sendSimpleCommand(command); 1145 } 1146 1147 1148 /** 1149 * Unsubscribe from a mailbox. 1150 * 1151 * @param mailbox The mailbox to remove. 1152 * 1153 * @exception MessagingException 1154 */ 1155 public void unsubscribe(String mailbox) throws MessagingException { 1156 IMAPCommand command = new IMAPCommand("UNSUBSCRIBE"); 1157 // add on the encoded mailbox name, as the appropriate token type. 1158 command.appendEncodedString(mailbox); 1159 1160 // send this, and ignore the response. 1161 sendSimpleCommand(command); 1162 } 1163 1164 1165 /** 1166 * Create a mailbox. 1167 * 1168 * @param mailbox The desired new mailbox name (fully qualified); 1169 * 1170 * @exception MessagingException 1171 */ 1172 public void createMailbox(String mailbox) throws MessagingException { 1173 IMAPCommand command = new IMAPCommand("CREATE"); 1174 // add on the encoded mailbox name, as the appropriate token type. 1175 command.appendEncodedString(mailbox); 1176 1177 // send this, and ignore the response. 1178 sendSimpleCommand(command); 1179 } 1180 1181 1182 /** 1183 * Delete a mailbox. 1184 * 1185 * @param mailbox The target mailbox name (fully qualified); 1186 * 1187 * @exception MessagingException 1188 */ 1189 public void deleteMailbox(String mailbox) throws MessagingException { 1190 IMAPCommand command = new IMAPCommand("DELETE"); 1191 // add on the encoded mailbox name, as the appropriate token type. 1192 command.appendEncodedString(mailbox); 1193 1194 // send this, and ignore the response. 1195 sendSimpleCommand(command); 1196 } 1197 1198 1199 /** 1200 * Rename a mailbox. 1201 * 1202 * @param mailbox The target mailbox name (fully qualified); 1203 * 1204 * @exception MessagingException 1205 */ 1206 public void renameMailbox(String oldName, String newName) throws MessagingException { 1207 IMAPCommand command = new IMAPCommand("RENAME"); 1208 // add on the encoded mailbox name, as the appropriate token type. 1209 command.appendEncodedString(oldName); 1210 command.appendEncodedString(newName); 1211 1212 // send this, and ignore the response. 1213 sendSimpleCommand(command); 1214 } 1215 1216 1217 /** 1218 * Retrieve a complete set of status items for a mailbox. 1219 * 1220 * @param mailbox The mailbox name. 1221 * 1222 * @return An IMAPMailboxStatus item filled in with the STATUS responses. 1223 * @exception MessagingException 1224 */ 1225 public synchronized IMAPMailboxStatus getMailboxStatus(String mailbox) throws MessagingException { 1226 IMAPCommand command = new IMAPCommand("STATUS"); 1227 1228 // construct the command, encoding the tokens as required by the content. 1229 command.appendEncodedString(mailbox); 1230 // request all of the status items 1231 command.append(" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)"); 1232 1233 sendCommand(command); 1234 1235 // now harvest each of the respon 1236 IMAPMailboxStatus status = new IMAPMailboxStatus(); 1237 status.mergeSizeResponses(extractResponses("EXISTS")); 1238 status.mergeSizeResponses(extractResponses("RECENT")); 1239 status.mergeOkResponses(extractResponses("UIDNEXT")); 1240 status.mergeOkResponses(extractResponses("UIDVALIDITY")); 1241 status.mergeOkResponses(extractResponses("UNSEEN")); 1242 status.mergeStatus((IMAPStatusResponse)extractResponse("STATUS")); 1243 status.mergeStatus((IMAPPermanentFlagsResponse)extractResponse("PERMANENTFLAGS")); 1244 1245 return status; 1246 } 1247 1248 1249 /** 1250 * Select a mailbox, returning the accumulated status information 1251 * about the mailbox returned with the response. 1252 * 1253 * @param mailbox The desired mailbox name. 1254 * @param readOnly The open mode. If readOnly is true, the mailbox is opened 1255 * using EXAMINE rather than SELECT. 1256 * 1257 * @return A status object containing the mailbox particulars. 1258 * @exception MessagingException 1259 */ 1260 public synchronized IMAPMailboxStatus openMailbox(String mailbox, boolean readOnly) throws MessagingException { 1261 IMAPCommand command = new IMAPCommand(); 1262 1263 // if readOnly is required, we use EXAMINE to switch to the mailbox rather than SELECT. 1264 // This returns the same response information, but the mailbox will not accept update operations. 1265 if (readOnly) { 1266 command.appendAtom("EXAMINE"); 1267 } 1268 else { 1269 command.appendAtom("SELECT"); 1270 } 1271 1272 // construct the command, encoding the tokens as required by the content. 1273 command.appendEncodedString(mailbox); 1274 1275 // issue the select 1276 IMAPTaggedResponse response = sendCommand(command); 1277 1278 IMAPMailboxStatus status = new IMAPMailboxStatus(); 1279 // set the mode to the requested open mode. 1280 status.mode = readOnly ? Folder.READ_ONLY : Folder.READ_WRITE; 1281 1282 // the server might disagree on the mode, so check to see if 1283 // it's telling us READ-ONLY. 1284 if (response.hasStatus("READ-ONLY")) { 1285 status.mode = Folder.READ_ONLY; 1286 } 1287 1288 // some of these are required, some are optional. 1289 status.mergeFlags((IMAPFlagsResponse)extractResponse("FLAGS")); 1290 status.mergeStatus((IMAPSizeResponse)extractResponse("EXISTS")); 1291 status.mergeStatus((IMAPSizeResponse)extractResponse("RECENT")); 1292 status.mergeStatus((IMAPOkResponse)extractResponse("UIDVALIDITY")); 1293 status.mergeStatus((IMAPOkResponse)extractResponse("UNSEEN")); 1294 status.mergeStatus((IMAPPermanentFlagsResponse)extractResponse("PERMANENTFLAGS")); 1295 // mine the response for status information about the selected mailbox. 1296 return status; 1297 } 1298 1299 1300 /** 1301 * Tells the IMAP server to expunge messages marked for deletion. 1302 * The server will send us an untagged EXPUNGE message back for 1303 * each deleted message. For explicit expunges we request, we'll 1304 * grabbed the untagged responses here, rather than force them to 1305 * be handled as pending responses. The caller will handle the 1306 * updates directly. 1307 * 1308 * @exception MessagingException 1309 */ 1310 public synchronized List expungeMailbox() throws MessagingException { 1311 // send the message, and make sure we got an OK response 1312 sendCommand("EXPUNGE"); 1313 // extract all of the expunged responses and return. 1314 return extractResponses("EXPUNGED"); 1315 } 1316 1317 public int[] searchMailbox(SearchTerm term) throws MessagingException { 1318 return searchMailbox("ALL", term); 1319 } 1320 1321 /** 1322 * Send a search to the IMAP server using the specified 1323 * messages selector and search term. This figures out what 1324 * to do with CHARSET on the SEARCH command. 1325 * 1326 * @param messages The list of messages (comma-separated numbers or "ALL"). 1327 * @param term The desired search criteria 1328 * 1329 * @return Returns an int[] array of message numbers for all matched messages. 1330 * @exception MessagingException 1331 */ 1332 public int[] searchMailbox(String messages, SearchTerm term) throws MessagingException { 1333 // don't use a charset by default, but we need to look at the data to see if we have a problem. 1334 String charset = null; 1335 1336 if (IMAPCommand.checkSearchEncoding(term)) { 1337 // not sure exactly how to decide what to use here. Two immediate possibilities come to mind, 1338 // UTF-8 or the MimeUtility.getDefaultJavaCharset() value. Running a small test against the 1339 // Sun impl shows them sending a CHARSET value of UTF-8, so that sounds like the winner. I don't 1340 // believe there's anything in the CAPABILITY response that would tell us what to use. 1341 charset = "UTF-8"; 1342 } 1343 1344 return searchMailbox(messages, term, charset); 1345 } 1346 1347 /** 1348 * Send a search to the IMAP server using the specified 1349 * messages selector and search term. 1350 * 1351 * @param messages The list of messages (comma-separated numbers or "ALL"). 1352 * @param charset The charset specifier to send to the server. If null, then 1353 * the CHARSET keyword is omitted. 1354 * @param term The desired search criteria 1355 * 1356 * @return Returns an int[] array of message numbers for all matched messages. 1357 * @exception MessagingException 1358 */ 1359 public synchronized int[] searchMailbox(String messages, SearchTerm term, String charset) throws MessagingException { 1360 IMAPCommand command = new IMAPCommand("SEARCH"); 1361 1362 // if we have an explicit charset to use, append that. 1363 if (charset != null) { 1364 command.appendAtom("CHARSET"); 1365 command.appendAtom(charset); 1366 } 1367 1368 // now go through the process of translating the javamail SearchTerm objects into 1369 // the IMAP command sequence. The SearchTerm sequence may be a complex tree of comparison terms, 1370 // so this is not a simple process. 1371 command.appendSearchTerm(term, charset); 1372 // need to append the message set 1373 command.appendAtom(messages); 1374 1375 // now issue the composed command. 1376 sendCommand(command); 1377 1378 // get the list of search responses 1379 IMAPSearchResponse hits = (IMAPSearchResponse)extractResponse("SEARCH"); 1380 // and return the message hits 1381 return hits.messageNumbers; 1382 } 1383 1384 1385 /** 1386 * Append a message to a mailbox, given the direct message data. 1387 * 1388 * @param mailbox The target mailbox name. 1389 * @param messageFlags 1390 * The initial flag set for the appended message. 1391 * @param messageDate 1392 * The received date the message is created with, 1393 * @param messageData 1394 * The RFC822 Message data stored on the server. 1395 * 1396 * @exception MessagingException 1397 */ 1398 public void appendMessage(String mailbox, Date messageDate, Flags messageFlags, byte[] messageData) throws MessagingException { 1399 IMAPCommand command = new IMAPCommand("APPEND"); 1400 1401 // the mailbox is encoded. 1402 command.appendEncodedString(mailbox); 1403 1404 if (messageFlags != null) { 1405 // the flags are pulled from an existing object. We can set most flag values, but the servers 1406 // reserve RECENT for themselves. We need to force that one off. 1407 messageFlags.remove(Flags.Flag.RECENT); 1408 // and add the flag list to the commmand. 1409 command.appendFlags(messageFlags); 1410 } 1411 1412 if (messageDate != null) { 1413 command.appendDate(messageDate); 1414 } 1415 1416 // this gets appended as a literal. 1417 command.appendLiteral(messageData); 1418 // just send this as a simple command...we don't deal with the response other than to verifiy 1419 // it was ok. 1420 sendSimpleCommand(command); 1421 } 1422 1423 /** 1424 * Fetch the flag set for a given message sequence number. 1425 * 1426 * @param sequenceNumber 1427 * The message sequence number. 1428 * 1429 * @return The Flags defined for this message. 1430 * @exception MessagingException 1431 */ 1432 public synchronized Flags fetchFlags(int sequenceNumber) throws MessagingException { 1433 // we want just the flag item here. 1434 sendCommand("FETCH " + String.valueOf(sequenceNumber) + " (FLAGS)"); 1435 // get the return data item, and get the flags from within it 1436 IMAPFlags flags = (IMAPFlags)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.FLAGS); 1437 return flags.flags; 1438 } 1439 1440 1441 /** 1442 * Set the flags for a range of messages. 1443 * 1444 * @param messageSet The set of message numbers. 1445 * @param flags The new flag settings. 1446 * @param set true if the flags should be set, false for a clear operation. 1447 * 1448 * @return A list containing all of the responses with the new flag values. 1449 * @exception MessagingException 1450 */ 1451 public synchronized List setFlags(String messageSet, Flags flags, boolean set) throws MessagingException { 1452 IMAPCommand command = new IMAPCommand("STORE"); 1453 command.appendAtom(messageSet); 1454 // the command varies depending on whether this is a set or clear operation 1455 if (set) { 1456 command.appendAtom("+FLAGS"); 1457 } 1458 else { 1459 command.appendAtom("-FLAGS"); 1460 } 1461 1462 // append the flag set 1463 command.appendFlags(flags); 1464 1465 // we want just the flag item here. 1466 sendCommand(command); 1467 // we should have a FETCH response for each of the updated messages. Return this 1468 // response, and update the message numbers. 1469 return extractFetchDataItems(IMAPFetchDataItem.FLAGS); 1470 } 1471 1472 1473 /** 1474 * Set the flags for a single message. 1475 * 1476 * @param sequenceNumber 1477 * The sequence number of target message. 1478 * @param flags The new flag settings. 1479 * @param set true if the flags should be set, false for a clear operation. 1480 * 1481 * @exception MessagingException 1482 */ 1483 public synchronized Flags setFlags(int sequenceNumber, Flags flags, boolean set) throws MessagingException { 1484 IMAPCommand command = new IMAPCommand("STORE"); 1485 command.appendInteger(sequenceNumber); 1486 // the command varies depending on whether this is a set or clear operation 1487 if (set) { 1488 command.appendAtom("+FLAGS"); 1489 } 1490 else { 1491 command.appendAtom("-FLAGS"); 1492 } 1493 1494 // append the flag set 1495 command.appendFlags(flags); 1496 1497 // we want just the flag item here. 1498 sendCommand(command); 1499 // get the return data item, and get the flags from within it 1500 IMAPFlags flagResponse = (IMAPFlags)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.FLAGS); 1501 return flagResponse.flags; 1502 } 1503 1504 1505 /** 1506 * Copy a range of messages to a target mailbox. 1507 * 1508 * @param messageSet The set of message numbers. 1509 * @param target The target mailbox name. 1510 * 1511 * @exception MessagingException 1512 */ 1513 public void copyMessages(String messageSet, String target) throws MessagingException { 1514 IMAPCommand command = new IMAPCommand("COPY"); 1515 // the auth command initiates the handshaking. 1516 command.appendAtom(messageSet); 1517 // the mailbox is encoded. 1518 command.appendEncodedString(target); 1519 // just send this as a simple command...we don't deal with the response other than to verifiy 1520 // it was ok. 1521 sendSimpleCommand(command); 1522 } 1523 1524 1525 /** 1526 * Fetch the message number for a give UID. 1527 * 1528 * @param uid The target UID 1529 * 1530 * @return An IMAPUid object containing the mapping information. 1531 */ 1532 public synchronized IMAPUid getSequenceNumberForUid(long uid) throws MessagingException { 1533 IMAPCommand command = new IMAPCommand("UID FETCH"); 1534 command.appendLong(uid); 1535 command.appendAtom("(UID)"); 1536 1537 // this situation is a little strange, so it deserves a little explanation. 1538 // We need the message sequence number for this message from a UID value. 1539 // we're going to send a UID FETCH command, requesting the UID value back. 1540 // That seems strange, but the * nnnn FETCH response for the request will 1541 // be tagged with the message sequence number. THAT'S the information we 1542 // really want, and it will be included in the IMAPUid object. 1543 1544 sendCommand(command); 1545 // ok, now we need to search through these looking for a FETCH response with a UID element. 1546 List responses = extractResponses("FETCH"); 1547 1548 // we're looking for a fetch response with a UID data item with the UID information 1549 // inside of it. 1550 for (int i = 0; i < responses.size(); i++) { 1551 IMAPFetchResponse response = (IMAPFetchResponse)responses.get(i); 1552 IMAPUid item = (IMAPUid)response.getDataItem(IMAPFetchDataItem.UID); 1553 // is this the response we're looking for? The information we 1554 // need is the message number returned with the response, which is 1555 // also contained in the UID item. 1556 if (item != null && item.uid == uid) { 1557 return item; 1558 } 1559 // not one meant for us, add it back to the pending queue. 1560 queuePendingResponse(response); 1561 } 1562 // didn't find this one 1563 return null; 1564 } 1565 1566 1567 /** 1568 * Fetch the message numbers for a consequetive range 1569 * of UIDs. 1570 * 1571 * @param start The start of the range. 1572 * @param end The end of the uid range. 1573 * 1574 * @return A list of UID objects containing the mappings. 1575 */ 1576 public synchronized List getSequenceNumbersForUids(long start, long end) throws MessagingException { 1577 IMAPCommand command = new IMAPCommand("UID FETCH"); 1578 // send the request for the range "start:end" so we can fetch all of the info 1579 // at once. 1580 command.appendLong(start); 1581 command.append(":"); 1582 // not the special range marker? Just append the 1583 // number. The LASTUID value needs to be "*" on the command. 1584 if (end != UIDFolder.LASTUID) { 1585 command.appendLong(end); 1586 } 1587 else { 1588 command.append("*"); 1589 } 1590 command.appendAtom("(UID)"); 1591 1592 // this situation is a little strange, so it deserves a little explanation. 1593 // We need the message sequence number for this message from a UID value. 1594 // we're going to send a UID FETCH command, requesting the UID value back. 1595 // That seems strange, but the * nnnn FETCH response for the request will 1596 // be tagged with the message sequence number. THAT'S the information we 1597 // really want, and it will be included in the IMAPUid object. 1598 1599 sendCommand(command); 1600 // ok, now we need to search through these looking for a FETCH response with a UID element. 1601 List responses = extractResponses("FETCH"); 1602 1603 List uids = new ArrayList((int)(end - start + 1)); 1604 1605 // we're looking for a fetch response with a UID data item with the UID information 1606 // inside of it. 1607 for (int i = 0; i < responses.size(); i++) { 1608 IMAPFetchResponse response = (IMAPFetchResponse)responses.get(i); 1609 IMAPUid item = (IMAPUid)response.getDataItem(IMAPFetchDataItem.UID); 1610 // is this the response we're looking for? The information we 1611 // need is the message number returned with the response, which is 1612 // also contained in the UID item. 1613 if (item != null) { 1614 uids.add(item); 1615 } 1616 else { 1617 // not one meant for us, add it back to the pending queue. 1618 queuePendingResponse(response); 1619 } 1620 } 1621 // return the list of uids we located. 1622 return uids; 1623 } 1624 1625 1626 /** 1627 * Fetch the UID value for a target message number 1628 * 1629 * @param sequenceNumber 1630 * The target message number. 1631 * 1632 * @return An IMAPUid object containing the mapping information. 1633 */ 1634 public synchronized IMAPUid getUidForSequenceNumber(int sequenceNumber) throws MessagingException { 1635 IMAPCommand command = new IMAPCommand("FETCH"); 1636 command.appendInteger(sequenceNumber); 1637 command.appendAtom("(UID)"); 1638 1639 // similar to the other fetches, but without the strange bit. We're starting 1640 // with the message number in this case. 1641 1642 sendCommand(command); 1643 1644 // ok, now we need to search through these looking for a FETCH response with a UID element. 1645 return (IMAPUid)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.UID); 1646 } 1647 1648 1649 /** 1650 * Retrieve the user name space info from the server. 1651 * 1652 * @return An IMAPNamespace response item with the information. If the server 1653 * doesn't support the namespace extension, an empty one is returned. 1654 */ 1655 public synchronized IMAPNamespaceResponse getNamespaces() throws MessagingException { 1656 // if no namespace capability, then return an empty 1657 // response, which will trigger the default behavior. 1658 if (!hasCapability("NAMESPACE")) { 1659 return new IMAPNamespaceResponse(); 1660 } 1661 // no arguments on this command, so just send an hope it works. 1662 sendCommand("NAMESPACE"); 1663 1664 // this should be here, since it's a required response when the 1665 // command worked. Just extract, and return. 1666 return (IMAPNamespaceResponse)extractResponse("NAMESPACE"); 1667 } 1668 1669 1670 /** 1671 * Prefetch message information based on the request profile. We'll return 1672 * all of the fetch information to the requesting Folder, which will sort 1673 * out what goes where. 1674 * 1675 * @param messageSet The set of message numbers we need to fetch. 1676 * @param profile The profile of the required information. 1677 * 1678 * @return All FETCH responses resulting from the command. 1679 * @exception MessagingException 1680 */ 1681 public synchronized List fetch(String messageSet, FetchProfile profile) throws MessagingException { 1682 IMAPCommand command = new IMAPCommand("FETCH"); 1683 command.appendAtom(messageSet); 1684 // this is the set of items to append 1685 command.appendFetchProfile(profile); 1686 1687 // now send the fetch command, which will likely send back a lot of "FETCH" responses. 1688 // Suck all of those reponses out of the queue and send them back for processing. 1689 sendCommand(command); 1690 // we can have a large number of messages here, so just grab all of the fetches 1691 // we get back, and let the Folder sort out who gets what. 1692 return extractResponses("FETCH"); 1693 } 1694 1695 1696 /** 1697 * Set the ACL rights for a mailbox. This replaces 1698 * any existing ACLs defined. 1699 * 1700 * @param mailbox The target mailbox. 1701 * @param acl The new ACL to be used for the mailbox. 1702 * 1703 * @exception MessagingException 1704 */ 1705 public synchronized void setACLRights(String mailbox, ACL acl) throws MessagingException { 1706 IMAPCommand command = new IMAPCommand("SETACL"); 1707 command.appendEncodedString(mailbox); 1708 1709 command.appendACL(acl); 1710 1711 sendSimpleCommand(command); 1712 } 1713 1714 1715 /** 1716 * Add a set of ACL rights to a mailbox. 1717 * 1718 * @param mailbox The mailbox to alter. 1719 * @param acl The ACL to add. 1720 * 1721 * @exception MessagingException 1722 */ 1723 public synchronized void addACLRights(String mailbox, ACL acl) throws MessagingException { 1724 if (!hasCapability("ACL")) { 1725 throw new MethodNotSupportedException("ACL not available from this IMAP server"); 1726 } 1727 IMAPCommand command = new IMAPCommand("SETACL"); 1728 command.appendEncodedString(mailbox); 1729 1730 command.appendACL(acl, "+"); 1731 1732 sendSimpleCommand(command); 1733 } 1734 1735 1736 /** 1737 * Remove an ACL from a given mailbox. 1738 * 1739 * @param mailbox The mailbox to alter. 1740 * @param acl The particular ACL to revoke. 1741 * 1742 * @exception MessagingException 1743 */ 1744 public synchronized void removeACLRights(String mailbox, ACL acl) throws MessagingException { 1745 if (!hasCapability("ACL")) { 1746 throw new MethodNotSupportedException("ACL not available from this IMAP server"); 1747 } 1748 IMAPCommand command = new IMAPCommand("SETACL"); 1749 command.appendEncodedString(mailbox); 1750 1751 command.appendACL(acl, "-"); 1752 1753 sendSimpleCommand(command); 1754 } 1755 1756 1757 /** 1758 * Get the ACL rights assigned to a given mailbox. 1759 * 1760 * @param mailbox The target mailbox. 1761 * 1762 * @return The an array of ACL items describing the access 1763 * rights to the mailbox. 1764 * @exception MessagingException 1765 */ 1766 public synchronized ACL[] getACLRights(String mailbox) throws MessagingException { 1767 if (!hasCapability("ACL")) { 1768 throw new MethodNotSupportedException("ACL not available from this IMAP server"); 1769 } 1770 IMAPCommand command = new IMAPCommand("GETACL"); 1771 command.appendEncodedString(mailbox); 1772 1773 // now send the GETACL command, which will return a single ACL untagged response. 1774 sendCommand(command); 1775 // there should be just a single ACL response back from this command. 1776 IMAPACLResponse response = (IMAPACLResponse)extractResponse("ACL"); 1777 return response.acls; 1778 } 1779 1780 1781 /** 1782 * Get the current user's ACL rights to a given mailbox. 1783 * 1784 * @param mailbox The target mailbox. 1785 * 1786 * @return The Rights associated with this mailbox. 1787 * @exception MessagingException 1788 */ 1789 public synchronized Rights getMyRights(String mailbox) throws MessagingException { 1790 if (!hasCapability("ACL")) { 1791 throw new MethodNotSupportedException("ACL not available from this IMAP server"); 1792 } 1793 IMAPCommand command = new IMAPCommand("MYRIGHTS"); 1794 command.appendEncodedString(mailbox); 1795 1796 // now send the MYRIGHTS command, which will return a single MYRIGHTS untagged response. 1797 sendCommand(command); 1798 // there should be just a single MYRIGHTS response back from this command. 1799 IMAPMyRightsResponse response = (IMAPMyRightsResponse)extractResponse("MYRIGHTS"); 1800 return response.rights; 1801 } 1802 1803 1804 /** 1805 * List the ACL rights that a particular user has 1806 * to a mailbox. 1807 * 1808 * @param mailbox The target mailbox. 1809 * @param name The user we're querying. 1810 * 1811 * @return An array of rights the use has to this mailbox. 1812 * @exception MessagingException 1813 */ 1814 public synchronized Rights[] listACLRights(String mailbox, String name) throws MessagingException { 1815 if (!hasCapability("ACL")) { 1816 throw new MethodNotSupportedException("ACL not available from this IMAP server"); 1817 } 1818 IMAPCommand command = new IMAPCommand("LISTRIGHTS"); 1819 command.appendEncodedString(mailbox); 1820 command.appendString(name); 1821 1822 // now send the GETACL command, which will return a single ACL untagged response. 1823 sendCommand(command); 1824 // there should be just a single ACL response back from this command. 1825 IMAPListRightsResponse response = (IMAPListRightsResponse)extractResponse("LISTRIGHTS"); 1826 return response.rights; 1827 } 1828 1829 1830 /** 1831 * Delete an ACL item for a given user name from 1832 * a target mailbox. 1833 * 1834 * @param mailbox The mailbox we're altering. 1835 * @param name The user name. 1836 * 1837 * @exception MessagingException 1838 */ 1839 public synchronized void deleteACL(String mailbox, String name) throws MessagingException { 1840 if (!hasCapability("ACL")) { 1841 throw new MethodNotSupportedException("ACL not available from this IMAP server"); 1842 } 1843 IMAPCommand command = new IMAPCommand("DELETEACL"); 1844 command.appendEncodedString(mailbox); 1845 command.appendString(name); 1846 1847 // just send the command. No response to handle. 1848 sendSimpleCommand(command); 1849 } 1850 1851 /** 1852 * Fetch the quota root information for a target mailbox. 1853 * 1854 * @param mailbox The mailbox of interest. 1855 * 1856 * @return An array of quotas describing all of the quota roots 1857 * that apply to the target mailbox. 1858 * @exception MessagingException 1859 */ 1860 public synchronized Quota[] fetchQuotaRoot(String mailbox) throws MessagingException { 1861 if (!hasCapability("QUOTA")) { 1862 throw new MethodNotSupportedException("QUOTA not available from this IMAP server"); 1863 } 1864 IMAPCommand command = new IMAPCommand("GETQUOTAROOT"); 1865 command.appendEncodedString(mailbox); 1866 1867 // This will return a single QUOTAROOT response, plust a series of QUOTA responses for 1868 // each root names in the first response. 1869 sendCommand(command); 1870 // we don't really need this, but pull it from the response queue anyway. 1871 extractResponse("QUOTAROOT"); 1872 1873 // now get the real meat of the matter 1874 List responses = extractResponses("QUOTA"); 1875 1876 // now copy all of the returned quota items into the response array. 1877 Quota[] quotas = new Quota[responses.size()]; 1878 for (int i = 0; i < quotas.length; i++) { 1879 IMAPQuotaResponse q = (IMAPQuotaResponse)responses.get(i); 1880 quotas[i] = q.quota; 1881 } 1882 1883 return quotas; 1884 } 1885 1886 /** 1887 * Fetch QUOTA information from a named QUOTE root. 1888 * 1889 * @param root The target root name. 1890 * 1891 * @return An array of Quota items associated with that root name. 1892 * @exception MessagingException 1893 */ 1894 public synchronized Quota[] fetchQuota(String root) throws MessagingException { 1895 if (!hasCapability("QUOTA")) { 1896 throw new MethodNotSupportedException("QUOTA not available from this IMAP server"); 1897 } 1898 IMAPCommand command = new IMAPCommand("GETQUOTA"); 1899 command.appendString(root); 1900 1901 // This will return a single QUOTAROOT response, plust a series of QUOTA responses for 1902 // each root names in the first response. 1903 sendCommand(command); 1904 1905 // now get the real meat of the matter 1906 List responses = extractResponses("QUOTA"); 1907 1908 // now copy all of the returned quota items into the response array. 1909 Quota[] quotas = new Quota[responses.size()]; 1910 for (int i = 0; i < quotas.length; i++) { 1911 IMAPQuotaResponse q = (IMAPQuotaResponse)responses.get(i); 1912 quotas[i] = q.quota; 1913 } 1914 1915 return quotas; 1916 } 1917 1918 /** 1919 * Set a Quota item for the currently accessed 1920 * userid/folder resource. 1921 * 1922 * @param quota The new QUOTA information. 1923 * 1924 * @exception MessagingException 1925 */ 1926 public synchronized void setQuota(Quota quota) throws MessagingException { 1927 if (!hasCapability("QUOTA")) { 1928 throw new MethodNotSupportedException("QUOTA not available from this IMAP server"); 1929 } 1930 IMAPCommand command = new IMAPCommand("GETQUOTA"); 1931 // this gets appended as a list of resource values 1932 command.appendQuota(quota); 1933 1934 // This will return a single QUOTAROOT response, plust a series of QUOTA responses for 1935 // each root names in the first response. 1936 sendCommand(command); 1937 // we don't really need this, but pull it from the response queue anyway. 1938 extractResponses("QUOTA"); 1939 } 1940 1941 1942 /** 1943 * Test if this connection has a given capability. 1944 * 1945 * @param capability The capability name. 1946 * 1947 * @return true if this capability is in the list, false for a mismatch. 1948 */ 1949 public boolean hasCapability(String capability) { 1950 if (capabilities == null) { 1951 return false; 1952 } 1953 return capabilities.containsKey(capability); 1954 } 1955 1956 /** 1957 * Tag this connection as having been closed by the 1958 * server. This will not be returned to the 1959 * connection pool. 1960 */ 1961 public void setClosed() { 1962 closed = true; 1963 } 1964 1965 /** 1966 * Test if the connnection has been forcibly closed. 1967 * 1968 * @return True if the server disconnected the connection. 1969 */ 1970 public boolean isClosed() { 1971 return closed; 1972 } 1973 } 1974