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 * http://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 19 package org.apache.geronimo.javamail.store.imap; 20 21 import java.io.File; 22 import java.io.IOException; 23 import java.io.PrintStream; 24 import java.util.ArrayList; 25 import java.util.Iterator; 26 import java.util.LinkedList; 27 import java.util.List; 28 29 import javax.mail.AuthenticationFailedException; 30 import javax.mail.Folder; 31 import javax.mail.MessagingException; 32 import javax.mail.Quota; 33 import javax.mail.QuotaAwareStore; 34 import javax.mail.Session; 35 import javax.mail.Store; 36 import javax.mail.URLName; 37 import javax.mail.event.StoreEvent; 38 39 import org.apache.geronimo.javamail.store.imap.connection.IMAPConnection; 40 import org.apache.geronimo.javamail.store.imap.connection.IMAPConnectionPool; 41 import org.apache.geronimo.javamail.store.imap.connection.IMAPOkResponse; 42 import org.apache.geronimo.javamail.store.imap.connection.IMAPNamespaceResponse; 43 import org.apache.geronimo.javamail.store.imap.connection.IMAPNamespace; 44 import org.apache.geronimo.javamail.store.imap.connection.IMAPServerStatusResponse; 45 import org.apache.geronimo.javamail.store.imap.connection.IMAPUntaggedResponse; 46 import org.apache.geronimo.javamail.store.imap.connection.IMAPUntaggedResponseHandler; 47 import org.apache.geronimo.javamail.util.ProtocolProperties; 48 49 /** 50 * IMAP implementation of javax.mail.Store 51 * POP protocol spec is implemented in 52 * org.apache.geronimo.javamail.store.pop3.IMAPConnection 53 * 54 * @version $Rev: 707037 $ $Date: 2008-10-22 07:34:53 -0400 (Wed, 22 Oct 2008) $ 55 */ 56 57 public class IMAPStore extends Store implements QuotaAwareStore, IMAPUntaggedResponseHandler { 58 // the default connection ports for secure and non-secure variations 59 protected static final int DEFAULT_IMAP_PORT = 143; 60 protected static final int DEFAULT_IMAP_SSL_PORT = 993; 61 62 protected static final String MAIL_STATUS_TIMEOUT = "statuscacheimeout"; 63 protected static final int DEFAULT_STATUS_TIMEOUT = 1000; 64 65 // our accessor for protocol properties and the holder of 66 // protocol-specific information 67 protected ProtocolProperties props; 68 69 // the connection pool we use for access 70 protected IMAPConnectionPool connectionPool; 71 72 // the root folder 73 protected IMAPRootFolder root; 74 75 // the list of open folders (which also represents an open connection). 76 protected List openFolders = new LinkedList(); 77 78 // our session provided debug output stream. 79 protected PrintStream debugStream; 80 // the debug flag 81 protected boolean debug; 82 // until we're connected, we're closed 83 boolean closedForBusiness = true; 84 // The timeout value for our status cache 85 long statusCacheTimeout = 0; 86 87 /** 88 * Construct an IMAPStore item. 89 * 90 * @param session The owning javamail Session. 91 * @param urlName The Store urlName, which can contain server target information. 92 */ 93 public IMAPStore(Session session, URLName urlName) { 94 // we're the imap protocol, our default connection port is 119, and don't use 95 // an SSL connection for the initial hookup 96 this(session, urlName, "imap", false, DEFAULT_IMAP_PORT); 97 } 98 99 /** 100 * Protected common constructor used by both the IMAPStore and the IMAPSSLStore 101 * to initialize the Store instance. 102 * 103 * @param session The Session we're attached to. 104 * @param urlName The urlName. 105 * @param protocol The protocol name. 106 * @param sslConnection 107 * The sslConnection flag. 108 * @param defaultPort 109 * The default connection port. 110 */ 111 protected IMAPStore(Session session, URLName urlName, String protocol, boolean sslConnection, int defaultPort) { 112 super(session, urlName); 113 // create the protocol property holder. This gives an abstraction over the different 114 // flavors of the protocol. 115 props = new ProtocolProperties(session, protocol, sslConnection, defaultPort); 116 117 // get the status timeout value for the folders. 118 statusCacheTimeout = props.getIntProperty(MAIL_STATUS_TIMEOUT, DEFAULT_STATUS_TIMEOUT); 119 120 // get our debug settings 121 debugStream = session.getDebugOut(); 122 debug = session.getDebug(); 123 124 // create a connection pool we can retrieve connections from 125 connectionPool = new IMAPConnectionPool(this, props); 126 } 127 128 129 /** 130 * Attempt the protocol-specific connection; subclasses should override this to establish 131 * a connection in the appropriate manner. 132 * 133 * This method should return true if the connection was established. 134 * It may return false to cause the {@link #connect(String, int, String, String)} method to 135 * reattempt the connection after trying to obtain user and password information from the user. 136 * Alternatively it may throw a AuthenticatedFailedException to abandon the conection attempt. 137 * 138 * @param host The target host name of the service. 139 * @param port The connection port for the service. 140 * @param user The user name used for the connection. 141 * @param password The password used for the connection. 142 * 143 * @return true if a connection was established, false if there was authentication 144 * error with the connection. 145 * @throws AuthenticationFailedException 146 * if authentication fails 147 * @throws MessagingException 148 * for other failures 149 */ 150 protected synchronized boolean protocolConnect(String host, int port, String username, String password) throws MessagingException { 151 if (debug) { 152 debugOut("Connecting to server " + host + ":" + port + " for user " + username); 153 } 154 155 // the connection pool handles all of the details here. 156 if (connectionPool.protocolConnect(host, port, username, password)) 157 { 158 // the store is now open 159 closedForBusiness = false; 160 return true; 161 } 162 return false; 163 } 164 165 166 /** 167 * Close this service and terminate its physical connection. 168 * The default implementation simply calls setConnected(false) and then 169 * sends a CLOSED event to all registered ConnectionListeners. 170 * Subclasses overriding this method should still ensure it is closed; they should 171 * also ensure that it is called if the connection is closed automatically, for 172 * for example in a finalizer. 173 * 174 *@throws MessagingException if there were errors closing; the connection is still closed 175 */ 176 public synchronized void close() throws MessagingException{ 177 // if already closed, nothing to do. 178 if (closedForBusiness) { 179 return; 180 } 181 182 // close the folders first, then shut down the Store. 183 closeOpenFolders(); 184 185 connectionPool.close(); 186 connectionPool = null; 187 188 // make sure we do the superclass close operation first so 189 // notification events get broadcast properly. 190 super.close(); 191 } 192 193 194 /** 195 * Return a Folder object that represents the root of the namespace for the current user. 196 * 197 * Note that in some store configurations (such as IMAP4) the root folder might 198 * not be the INBOX folder. 199 * 200 * @return the root Folder 201 * @throws MessagingException if there was a problem accessing the store 202 */ 203 public Folder getDefaultFolder() throws MessagingException { 204 checkConnectionStatus(); 205 // if no root yet, create a root folder instance. 206 if (root == null) { 207 return new IMAPRootFolder(this); 208 } 209 return root; 210 } 211 212 /** 213 * Return the Folder corresponding to the given name. 214 * The folder might not physically exist; the {@link Folder#exists()} method can be used 215 * to determine if it is real. 216 * 217 * @param name the name of the Folder to return 218 * 219 * @return the corresponding folder 220 * @throws MessagingException 221 * if there was a problem accessing the store 222 */ 223 public Folder getFolder(String name) throws MessagingException { 224 return getDefaultFolder().getFolder(name); 225 } 226 227 228 /** 229 * Return the folder identified by the URLName; the URLName must refer to this Store. 230 * Implementations may use the {@link URLName#getFile()} method to determined the folder name. 231 * 232 * @param url 233 * 234 * @return the corresponding folder 235 * @throws MessagingException 236 * if there was a problem accessing the store 237 */ 238 public Folder getFolder(URLName url) throws MessagingException { 239 return getDefaultFolder().getFolder(url.getFile()); 240 } 241 242 243 /** 244 * Return the root folders of the personal namespace belonging to the current user. 245 * 246 * The default implementation simply returns an array containing the folder returned by {@link #getDefaultFolder()}. 247 * @return the root folders of the user's peronal namespaces 248 * @throws MessagingException if there was a problem accessing the store 249 */ 250 public Folder[] getPersonalNamespaces() throws MessagingException { 251 IMAPNamespaceResponse namespaces = getNamespaces(); 252 253 // if nothing is returned, then use the API-defined default for this 254 if (namespaces.personalNamespaces.size() == 0) { 255 return super.getPersonalNamespaces(); 256 } 257 258 // convert the list into an array of Folders. 259 return getNamespaceFolders(namespaces.personalNamespaces); 260 } 261 262 263 /** 264 * Return the root folders of the personal namespaces belonging to the supplied user. 265 * 266 * The default implementation simply returns an empty array. 267 * 268 * @param user the user whose namespaces should be returned 269 * @return the root folders of the given user's peronal namespaces 270 * @throws MessagingException if there was a problem accessing the store 271 */ 272 public Folder[] getUserNamespaces(String user) throws MessagingException { 273 IMAPNamespaceResponse namespaces = getNamespaces(); 274 275 // if nothing is returned, then use the API-defined default for this 276 if (namespaces.otherUserNamespaces == null || namespaces.otherUserNamespaces.isEmpty()) { 277 return super.getUserNamespaces(user); 278 } 279 280 // convert the list into an array of Folders. 281 return getNamespaceFolders(namespaces.otherUserNamespaces); 282 } 283 284 285 /** 286 * Return the root folders of namespaces that are intended to be shared between users. 287 * 288 * The default implementation simply returns an empty array. 289 * @return the root folders of all shared namespaces 290 * @throws MessagingException if there was a problem accessing the store 291 */ 292 public Folder[] getSharedNamespaces() throws MessagingException { 293 IMAPNamespaceResponse namespaces = getNamespaces(); 294 295 // if nothing is returned, then use the API-defined default for this 296 if (namespaces.sharedNamespaces == null || namespaces.sharedNamespaces.isEmpty()) { 297 return super.getSharedNamespaces(); 298 } 299 300 // convert the list into an array of Folders. 301 return getNamespaceFolders(namespaces.sharedNamespaces); 302 } 303 304 305 /** 306 * Get the quotas for the specified root element. 307 * 308 * @param root The root name for the quota information. 309 * 310 * @return An array of Quota objects defined for the root. 311 * @throws MessagingException if the quotas cannot be retrieved 312 */ 313 public Quota[] getQuota(String root) throws javax.mail.MessagingException { 314 // get our private connection for access 315 IMAPConnection connection = getStoreConnection(); 316 try { 317 // request the namespace information from the server 318 return connection.fetchQuota(root); 319 } finally { 320 releaseStoreConnection(connection); 321 } 322 } 323 324 /** 325 * Set a quota item. The root contained in the Quota item identifies 326 * the quota target. 327 * 328 * @param quota The source quota item. 329 * @throws MessagingException if the quota cannot be set 330 */ 331 public void setQuota(Quota quota) throws javax.mail.MessagingException { 332 // get our private connection for access 333 IMAPConnection connection = getStoreConnection(); 334 try { 335 // request the namespace information from the server 336 connection.setQuota(quota); 337 } finally { 338 releaseStoreConnection(connection); 339 } 340 } 341 342 /** 343 * Verify that the server is in a connected state before 344 * performing operations that required that status. 345 * 346 * @exception MessagingException 347 */ 348 private void checkConnectionStatus() throws MessagingException { 349 // we just check the connection status with the superclass. This 350 // tells us we've gotten a connection. We don't want to do the 351 // complete connection checks that require pinging the server. 352 if (!super.isConnected()){ 353 throw new MessagingException("Not connected "); 354 } 355 } 356 357 358 /** 359 * Test to see if we're still connected. This will ping the server 360 * to see if we're still alive. 361 * 362 * @return true if we have a live, active culture, false otherwise. 363 */ 364 public synchronized boolean isConnected() { 365 // check if we're in a presumed connected state. If not, we don't really have a connection 366 // to check on. 367 if (!super.isConnected()) { 368 return false; 369 } 370 371 try { 372 IMAPConnection connection = getStoreConnection(); 373 try { 374 // check with the connecition to see if it's still alive. 375 // we use a zero timeout value to force it to check. 376 return connection.isAlive(0); 377 } finally { 378 releaseStoreConnection(connection); 379 } 380 } catch (MessagingException e) { 381 return false; 382 } 383 384 } 385 386 /** 387 * Internal debug output routine. 388 * 389 * @param value The string value to output. 390 */ 391 void debugOut(String message) { 392 debugStream.println("IMAPStore DEBUG: " + message); 393 } 394 395 /** 396 * Internal debugging routine for reporting exceptions. 397 * 398 * @param message A message associated with the exception context. 399 * @param e The received exception. 400 */ 401 void debugOut(String message, Throwable e) { 402 debugOut("Received exception -> " + message); 403 debugOut("Exception message -> " + e.getMessage()); 404 e.printStackTrace(debugStream); 405 } 406 407 408 /** 409 * Retrieve the server connection created by this store. 410 * 411 * @return The active connection object. 412 */ 413 protected IMAPConnection getStoreConnection() throws MessagingException { 414 return connectionPool.getStoreConnection(); 415 } 416 417 protected void releaseStoreConnection(IMAPConnection connection) throws MessagingException { 418 // This is a bit of a pain. We need to delay processing of the 419 // unsolicited responses until after each user of the connection has 420 // finished processing the expected responses. We need to do this because 421 // the unsolicited responses may include EXPUNGED messages. The EXPUNGED 422 // messages will alter the message sequence numbers for the messages in the 423 // cache. Processing the EXPUNGED messages too early will result in 424 // updates getting applied to the wrong message instances. So, as a result, 425 // we delay that stage of the processing until all expected responses have 426 // been handled. 427 428 // process any pending messages before returning. 429 connection.processPendingResponses(); 430 // return this to the connectin pool 431 connectionPool.releaseStoreConnection(connection); 432 } 433 434 synchronized IMAPConnection getFolderConnection(IMAPFolder folder) throws MessagingException { 435 IMAPConnection connection = connectionPool.getFolderConnection(); 436 openFolders.add(folder); 437 return connection; 438 } 439 440 441 synchronized void releaseFolderConnection(IMAPFolder folder, IMAPConnection connection) throws MessagingException { 442 openFolders.remove(folder); 443 // return this to the connectin pool 444 // NB: It is assumed that the Folder has already triggered handling of 445 // unsolicited responses on this connection before returning it. 446 connectionPool.releaseFolderConnection(connection); 447 } 448 449 450 /** 451 * Retrieve the Session object this Store is operating under. 452 * 453 * @return The attached Session instance. 454 */ 455 Session getSession() { 456 return session; 457 } 458 459 /** 460 * Close all open folders. We have a small problem here with a race condition. There's no safe, single 461 * synchronization point for us to block creation of new folders while we're closing. So we make a copy of 462 * the folders list, close all of those folders, and keep repeating until we're done. 463 */ 464 protected void closeOpenFolders() { 465 // we're no longer accepting additional opens. Any folders that open after this point will get an 466 // exception trying to get a connection. 467 closedForBusiness = true; 468 469 while (true) { 470 List folders = null; 471 472 // grab our lock, copy the open folders reference, and null this out. Once we see a null 473 // open folders ref, we're done closing. 474 synchronized(connectionPool) { 475 folders = openFolders; 476 openFolders = new LinkedList(); 477 } 478 479 // null folder, we're done 480 if (folders.isEmpty()) { 481 return; 482 } 483 // now close each of the open folders. 484 for (int i = 0; i < folders.size(); i++) { 485 IMAPFolder folder = (IMAPFolder)folders.get(i); 486 try { 487 folder.close(false); 488 } catch (MessagingException e) { 489 } 490 } 491 } 492 } 493 494 /** 495 * Get the namespace information from the IMAP server. 496 * 497 * @return An IMAPNamespaceResponse with the namespace information. 498 * @exception MessagingException 499 */ 500 protected IMAPNamespaceResponse getNamespaces() throws MessagingException { 501 // get our private connection for access 502 IMAPConnection connection = getStoreConnection(); 503 try { 504 // request the namespace information from the server 505 return connection.getNamespaces(); 506 } finally { 507 releaseStoreConnection(connection); 508 } 509 } 510 511 512 /** 513 * Convert a List of IMAPNamespace definitions into an array of Folder 514 * instances. 515 * 516 * @param namespaces The namespace List 517 * 518 * @return An array of the same size as the namespace list containing a Folder 519 * instance for each defined namespace. 520 * @exception MessagingException 521 */ 522 protected Folder[] getNamespaceFolders(List namespaces) throws MessagingException { 523 Folder[] folders = new Folder[namespaces.size()]; 524 525 // convert each of these to a Folder instance. 526 for (int i = 0; i < namespaces.size(); i++) { 527 IMAPNamespace namespace = (IMAPNamespace)namespaces.get(i); 528 folders[i] = new IMAPNamespaceFolder(this, namespace); 529 } 530 return folders; 531 } 532 533 534 /** 535 * Test if this connection has a given capability. 536 * 537 * @param capability The capability name. 538 * 539 * @return true if this capability is in the list, false for a mismatch. 540 */ 541 public boolean hasCapability(String capability) { 542 return connectionPool.hasCapability(capability); 543 } 544 545 546 /** 547 * Handle an unsolicited response from the server. Most unsolicited responses 548 * are replies to specific commands sent to the server. The remainder must 549 * be handled by the Store or the Folder using the connection. These are 550 * critical to handle, as events such as expunged messages will alter the 551 * sequence numbers of the live messages. We need to keep things in sync. 552 * 553 * @param response The UntaggedResponse to process. 554 * 555 * @return true if we handled this response and no further handling is required. false 556 * means this one wasn't one of ours. 557 */ 558 public boolean handleResponse(IMAPUntaggedResponse response) { 559 // Some sort of ALERT response from the server? 560 // we need to broadcast this to any of the listeners 561 if (response.isKeyword("ALERT")) { 562 notifyStoreListeners(StoreEvent.ALERT, ((IMAPOkResponse)response).getMessage()); 563 return true; 564 } 565 // potentially some sort of unsolicited OK notice. This is also an event. 566 else if (response.isKeyword("OK")) { 567 String message = ((IMAPOkResponse)response).getMessage(); 568 if (message.length() > 0) { 569 notifyStoreListeners(StoreEvent.NOTICE, message); 570 } 571 return true; 572 } 573 // potentially some sort of unsolicited notice. This is also an event. 574 else if (response.isKeyword("BAD") || response.isKeyword("NO")) { 575 String message = ((IMAPServerStatusResponse)response).getMessage(); 576 if (message.length() > 0) { 577 notifyStoreListeners(StoreEvent.NOTICE, message); 578 } 579 return true; 580 } 581 // this is a BYE response on our connection. Folders should be handling the 582 // BYE events on their connections, so we should only be seeing this if 583 // it's on the store connection. 584 else if (response.isKeyword("BYE")) { 585 // this is essentially a close event. We need to clean everything up 586 try { 587 close(); 588 } catch (MessagingException e) { 589 } 590 return true; 591 } 592 return false; 593 } 594 595 /** 596 * Finalizer to perform IMAPStore() cleanup when 597 * no longer in use. 598 * 599 * @exception Throwable 600 */ 601 protected void finalize() throws Throwable { 602 super.finalize(); 603 close(); 604 } 605 606 /** 607 * Retrieve the protocol properties for the Store. 608 * 609 * @return The protocol properties bundle. 610 */ 611 ProtocolProperties getProperties() { 612 return props; 613 } 614 }