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 package org.apache.geronimo.javamail.store.imap.connection; 19 20 import java.util.ArrayList; 21 import java.util.Iterator; 22 import java.util.List; 23 import java.util.Map; 24 import java.util.StringTokenizer; 25 26 import javax.mail.MessagingException; 27 import javax.mail.Session; 28 import javax.mail.Store; 29 30 import javax.mail.StoreClosedException; 31 32 import org.apache.geronimo.javamail.store.imap.IMAPStore; 33 import org.apache.geronimo.javamail.util.ProtocolProperties; 34 35 public class IMAPConnectionPool { 36 37 protected static final String MAIL_PORT = "port"; 38 protected static final String MAIL_POOL_SIZE = "connectionpoolsize"; 39 protected static final String MAIL_POOL_TIMEOUT = "connectionpooltimeout"; 40 protected static final String MAIL_SEPARATE_STORE_CONNECTION = "separatestoreconnection"; 41 42 protected static final String MAIL_SASL_REALM = "sasl.realm"; 43 protected static final String MAIL_AUTHORIZATIONID = "sasl.authorizationid"; 44 45 // 45 seconds, by default. 46 protected static final int DEFAULT_POOL_TIMEOUT = 45000; 47 protected static final String DEFAULT_MAIL_HOST = "localhost"; 48 49 protected static final int MAX_CONNECTION_RETRIES = 3; 50 protected static final int MAX_POOL_WAIT = 500; 51 52 53 // Our hosting Store instance 54 protected IMAPStore store; 55 // our Protocol abstraction 56 protected ProtocolProperties props; 57 // our list of created connections 58 protected List poolConnections = new ArrayList(); 59 // our list of available connections 60 protected List availableConnections = new ArrayList(); 61 62 // the dedicated Store connection (if we're configured that way) 63 protected IMAPConnection storeConnection = null; 64 65 // our dedicated Store connection attribute 66 protected boolean dedicatedStoreConnection; 67 // the size of our connection pool (by default, we only keep a single connection in the pool) 68 protected int poolSize = 1; 69 // the connection timeout property 70 protected long poolTimeout; 71 // our debug flag 72 protected boolean debug; 73 74 // the target host 75 protected String host; 76 // the target server port. 77 protected int port; 78 // the username we connect with 79 protected String username; 80 // the authentication password. 81 protected String password; 82 // the SASL realm name 83 protected String realm; 84 // the authorization id. With IMAP, it's possible to 85 // log on with another's authorization. 86 protected String authid; 87 // Turned on when the store is closed for business. 88 protected boolean closed = false; 89 // the connection capabilities map 90 protected Map capabilities; 91 92 /** 93 * Create a connection pool associated with a give IMAPStore instance. The 94 * connection pool manages handing out connections for both the Store and 95 * Folder and Message usage. 96 * 97 * Depending on the session properties, the Store may be given a dedicated 98 * connection, or will share connections with the Folders. Connections may 99 * be requested from either the Store or Folders. Messages must request 100 * their connections from their hosting Folder, and only one connection is 101 * allowed per folder. 102 * 103 * @param store The Store we're creating the pool for. 104 * @param props The property bundle that defines protocol properties 105 * that alter the connection behavior. 106 */ 107 public IMAPConnectionPool(IMAPStore store, ProtocolProperties props) { 108 this.store = store; 109 this.props = props; 110 111 // get the pool size. By default, we just use a single connection that's 112 // shared among Store and all of the Folders. Since most apps that use 113 // javamail tend to be single-threaded, this generally poses no great hardship. 114 poolSize = props.getIntProperty(MAIL_POOL_SIZE, 1); 115 // get the timeout property. Default is 45 seconds. 116 poolTimeout = props.getIntProperty(MAIL_POOL_TIMEOUT, DEFAULT_POOL_TIMEOUT); 117 // we can create a dedicated connection over and above the pool set that's 118 // reserved for the Store instance to use. 119 dedicatedStoreConnection = props.getBooleanProperty(MAIL_SEPARATE_STORE_CONNECTION, false); 120 // if we have a dedicated pool connection, we allocated that from the pool. Add this to 121 // the total pool size so we don't find ourselves stuck if the pool size is 1. 122 if (dedicatedStoreConnection) { 123 poolSize++; 124 } 125 } 126 127 128 /** 129 * Manage the initial connection to the IMAP server. This is the first 130 * point where we obtain the information needed to make an actual server 131 * connection. Like the Store protocolConnect method, we return false 132 * if there's any sort of authentication difficulties. 133 * 134 * @param host The host of the IMAP server. 135 * @param port The IMAP server connection port. 136 * @param user The connection user name. 137 * @param password The connection password. 138 * 139 * @return True if we were able to connect and authenticate correctly. 140 * @exception MessagingException 141 */ 142 public synchronized boolean protocolConnect(String host, int port, String username, String password) throws MessagingException { 143 // NOTE: We don't check for the username/password being null at this point. It's possible that 144 // the server will send back a PREAUTH response, which means we don't need to go through login 145 // processing. We'll need to check the capabilities response after we make the connection to decide 146 // if logging in is necesssary. 147 148 // save this for subsequent connections. All pool connections will use this info. 149 // if the port is defaulted, then see if we have something configured in the session. 150 // if not configured, we just use the default default. 151 if (port == -1) { 152 // check for a property and fall back on the default if it's not set. 153 port = props.getIntProperty(MAIL_PORT, props.getDefaultPort()); 154 // it's possible that -1 might have been explicitly set, so one last check. 155 if (port == -1) { 156 port = props.getDefaultPort(); 157 } 158 } 159 160 // Before we do anything, let's make sure that we succesfully received a host 161 if ( host == null ) { 162 host = DEFAULT_MAIL_HOST; 163 } 164 165 this.host = host; 166 this.port = port; 167 this.username = username; 168 this.password = password; 169 170 // make sure we have the realm information 171 realm = props.getProperty(MAIL_SASL_REALM); 172 // get an authzid value, if we have one. The default is to use the username. 173 authid = props.getProperty(MAIL_AUTHORIZATIONID, username); 174 175 // go create a connection and just add it to the pool. If there is an authenticaton error, 176 // return the connect failure, and we may end up trying again. 177 IMAPConnection connection = createPoolConnection(); 178 if (connection == null) { 179 return false; 180 } 181 // save the capabilities map from the first connection. 182 capabilities = connection.getCapabilities(); 183 // if we're using a dedicated store connection, remove this from the pool and 184 // reserve it for the store. 185 if (dedicatedStoreConnection) 186 { 187 storeConnection = connection; 188 // make sure this is hooked up to the store. 189 connection.addResponseHandler(store); 190 } 191 else { 192 // just put this back in the pool. It's ready for anybody to use now. 193 synchronized(this) { 194 availableConnections.add(connection); 195 } 196 } 197 // we're connection, authenticated, and ready to go. 198 return true; 199 } 200 201 /** 202 * Creates an authenticated pool connection and adds it to 203 * the connection pool. If there is an existing connection 204 * already in the pool, this returns without creating a new 205 * connection. 206 * 207 * @exception MessagingException 208 */ 209 protected IMAPConnection createPoolConnection() throws MessagingException { 210 IMAPConnection connection = new IMAPConnection(props, this); 211 if (!connection.protocolConnect(host, port, authid, realm, username, password)) { 212 // we only add live connections to the pool. Sever the connections and 213 // allow it to go free. 214 connection.closeServerConnection(); 215 return null; 216 } 217 218 // add this to the master list. We do NOT add this to the 219 // available queue because we're handing this out. 220 synchronized(this) { 221 // uh oh, we closed up shop while we were doing this...clean it up a 222 // get out of here 223 if (closed) { 224 connection.close(); 225 throw new StoreClosedException(store, "No Store connections available"); 226 } 227 228 poolConnections.add(connection); 229 } 230 // return that connection 231 return connection; 232 } 233 234 235 /** 236 * Get a connection from the pool. We try to retrieve a live 237 * connection, but we test the connection's liveness before 238 * returning one. If we don't have a viable connection in 239 * the pool, we'll create a new one. The returned connection 240 * will be in the authenticated state already. 241 * 242 * @return An IMAPConnection object that is connected to the server. 243 */ 244 protected IMAPConnection getConnection() throws MessagingException { 245 int retryCount = 0; 246 247 // To keep us from falling into a futile failure loop, we'll only allow 248 // a set number of connection failures. 249 while (retryCount < MAX_CONNECTION_RETRIES) { 250 // first try for an already created one. If this returns 251 // null, then we'll probably have to make a new one. 252 IMAPConnection connection = getPoolConnection(); 253 // cool, we got one, the hard part is done. 254 if (connection != null) { 255 return connection; 256 } 257 // ok, create a new one. This *should* work, but the server might 258 // have gone down, or other problem may occur. If we have a problem, 259 // retry the entire process...but only for a bit. No sense 260 // being stubborn about it. 261 connection = createPoolConnection(); 262 if (connection != null) { 263 return connection; 264 } 265 // step the retry count 266 retryCount++; 267 } 268 269 throw new MessagingException("Unable to get connection to IMAP server"); 270 } 271 272 /** 273 * Obtain a connection from the existing connection pool. If none are 274 * available, and we've reached the connection pool limit, we'll wait for 275 * some other thread to return one. It generally doesn't take too long, as 276 * they're usually only held for the time required to execute a single 277 * command. If we're not at the pool limit, return null, which will signal 278 * the caller to go ahead and create a new connection outside of the 279 * lock. 280 * 281 * @return Either an active connection instance, or null if the caller should go 282 * ahead and try to create a new connection. 283 * @exception MessagingException 284 */ 285 protected synchronized IMAPConnection getPoolConnection() throws MessagingException { 286 // if the pool is closed, we can't process this 287 if (closed) { 288 throw new StoreClosedException(store, "No Store connections available"); 289 } 290 291 // we'll retry this a few times if the connection pool is full, but 292 // after that, we'll just create a new connection. 293 for (int i = 0; i < MAX_CONNECTION_RETRIES; i++) { 294 Iterator it = availableConnections.iterator(); 295 while (it.hasNext()) { 296 IMAPConnection connection = (IMAPConnection)it.next(); 297 // live or dead, we're going to remove this from the 298 // available list. 299 it.remove(); 300 if (connection.isAlive(poolTimeout)) { 301 // return the connection to the requestor 302 return connection; 303 } 304 else { 305 // remove this from the pool...it's toast. 306 poolConnections.remove(connection); 307 // make sure this cleans up after itself. 308 connection.closeServerConnection(); 309 } 310 } 311 312 // we've not found something usable in the pool. Now see if 313 // we're allowed to add another connection, or must just wait for 314 // someone else to return one. 315 316 if (poolConnections.size() >= poolSize) { 317 // check to see if we've been told to shutdown before waiting 318 if (closed) { 319 throw new StoreClosedException(store, "No Store connections available"); 320 } 321 // we need to wait for somebody to return a connection 322 // once woken up, we'll spin around and try to snag one from 323 // the pool again. 324 try { 325 wait(MAX_POOL_WAIT); 326 } catch (InterruptedException e) { 327 } 328 329 // check to see if we've been told to shutdown while we waited 330 if (closed) { 331 throw new StoreClosedException(store, "No Store connections available"); 332 } 333 } 334 else { 335 // exit out and create a new connection. Since 336 // we're going to be outside the synchronized block, it's possible 337 // we'll go over our pool limit. We'll take care of that when connections start 338 // getting returned. 339 return null; 340 } 341 } 342 // we've hit the maximum number of retries...just create a new connection. 343 return null; 344 } 345 346 /** 347 * Return a connection to the connection pool. 348 * 349 * @param connection The connection getting returned. 350 * 351 * @exception MessagingException 352 */ 353 protected void returnPoolConnection(IMAPConnection connection) throws MessagingException 354 { 355 synchronized(this) { 356 // If we're still within the bounds of our connection pool, 357 // just add this to the active list and send out a notification 358 // in case somebody else is waiting for the connection. 359 if (availableConnections.size() < poolSize) { 360 availableConnections.add(connection); 361 notify(); 362 return; 363 } 364 // remove this from the connection pool...we have too many. 365 poolConnections.remove(connection); 366 } 367 // the additional cleanup occurs outside the synchronized block 368 connection.close(); 369 } 370 371 /** 372 * Release a closed connection. 373 * 374 * @param connection The connection getting released. 375 * 376 * @exception MessagingException 377 */ 378 protected void releasePoolConnection(IMAPConnection connection) throws MessagingException 379 { 380 synchronized(this) { 381 // remove this from the connection pool...it's no longer usable. 382 poolConnections.remove(connection); 383 } 384 // the additional cleanup occurs outside the synchronized block 385 connection.close(); 386 } 387 388 389 /** 390 * Get a connection for the Store. This will be either a 391 * dedicated connection object, or one from the pool, depending 392 * on the mail.imap.separatestoreconnection property. 393 * 394 * @return An authenticated connection object. 395 */ 396 public synchronized IMAPConnection getStoreConnection() throws MessagingException { 397 if (closed) { 398 throw new StoreClosedException(store, "No Store connections available"); 399 } 400 // if we have a dedicated connection created, return it. 401 if (storeConnection != null) { 402 return storeConnection; 403 } 404 else { 405 IMAPConnection connection = getConnection(); 406 // add the store as a response handler while it has it. 407 connection.addResponseHandler(store); 408 return connection; 409 } 410 } 411 412 413 /** 414 * Return the Store connection to the connection pool. If we have a dedicated 415 * store connection, this is simple. Otherwise, the connection goes back 416 * into the general connection pool. 417 * 418 * @param connection The connection getting returned. 419 */ 420 public synchronized void releaseStoreConnection(IMAPConnection connection) throws MessagingException { 421 // have a server disconnect situation? 422 if (connection.isClosed()) { 423 // we no longer have a dedicated store connection. 424 // we need to return to the pool from now on. 425 storeConnection = null; 426 // throw this away. 427 releasePoolConnection(connection); 428 } 429 else { 430 // if we have a dedicated connection, nothing to do really. Otherwise, 431 // return this connection to the pool. 432 if (storeConnection == null) { 433 // unhook the store from the connection. 434 connection.removeResponseHandler(store); 435 returnPoolConnection(connection); 436 } 437 } 438 } 439 440 441 /** 442 * Get a connection for Folder. 443 * 444 * @return An authenticated connection object. 445 */ 446 public IMAPConnection getFolderConnection() throws MessagingException { 447 // just get a connection from the pool 448 return getConnection(); 449 } 450 451 452 /** 453 * Return a Folder connection to the connection pool. 454 * 455 * @param connection The connection getting returned. 456 */ 457 public void releaseFolderConnection(IMAPConnection connection) throws MessagingException { 458 // potentially, the server may have decided to shut us down. 459 // In that case, the connection is no longer usable, so we need 460 // to remove it from the list of available ones. 461 if (!connection.isClosed()) { 462 // back into the pool with yee, matey....arrggghhh 463 returnPoolConnection(connection); 464 } 465 else { 466 // can't return this one to the pool. It's been stomped on 467 releasePoolConnection(connection); 468 } 469 } 470 471 472 /** 473 * Close the entire connection pool. 474 * 475 * @exception MessagingException 476 */ 477 public synchronized void close() throws MessagingException { 478 // first close each of the connections. This also closes the 479 // store connection. 480 for (int i = 0; i < poolConnections.size(); i++) { 481 IMAPConnection connection = (IMAPConnection)poolConnections.get(i); 482 connection.close(); 483 } 484 // clear the pool 485 poolConnections.clear(); 486 availableConnections.clear(); 487 storeConnection = null; 488 // turn out the lights, hang the closed sign on the wall. 489 closed = true; 490 } 491 492 493 /** 494 * Flush any connections from the pool that have not been used 495 * for at least the connection pool timeout interval. 496 */ 497 protected synchronized void closeStaleConnections() { 498 Iterator i = poolConnections.iterator(); 499 500 while (i.hasNext()) { 501 IMAPConnection connection = (IMAPConnection)i.next(); 502 // if this connection is a stale one, remove it from the pool 503 // and close it out. 504 if (connection.isStale(poolTimeout)) { 505 i.remove(); 506 try { 507 connection.close(); 508 } catch (MessagingException e) { 509 // ignored. we're just closing connections that are probably timed out anyway, so errors 510 // on those shouldn't have an effect on the real operation we're dealing with. 511 } 512 } 513 } 514 } 515 516 517 /** 518 * Return a connection back to the connection pool. If we're not 519 * over our limit, the connection is kept around. Otherwise, it's 520 * given a nice burial. 521 * 522 * @param connection The returned connection. 523 */ 524 protected synchronized void releaseConnection(IMAPConnection connection) { 525 // before adding this to the pool, close any stale connections we may 526 // have. The connection we're adding is quite likely to be a fresh one, 527 // so we should cache that one if we can. 528 closeStaleConnections(); 529 // still over the limit? 530 if (poolConnections.size() + 1 > poolSize) { 531 try { 532 // close this out and forget we ever saw it. 533 connection.close(); 534 } catch (MessagingException e) { 535 // ignore....this is a non-critical problem if this fails now. 536 } 537 } 538 else { 539 // listen to alerts on this connection, and put it back in the pool. 540 poolConnections.add(connection); 541 } 542 } 543 544 /** 545 * Cleanup time. Sever and cleanup all of the pool connection 546 * objects, including the special Store connection, if we have one. 547 */ 548 protected synchronized void freeAllConnections() { 549 for (int i = 0; i < poolConnections.size(); i++) { 550 IMAPConnection connection = (IMAPConnection)poolConnections.get(i); 551 try { 552 // close this out and forget we ever saw it. 553 connection.close(); 554 } catch (MessagingException e) { 555 // ignore....this is a non-critical problem if this fails now. 556 } 557 } 558 // everybody, out of the pool! 559 poolConnections.clear(); 560 561 // don't forget the special store connection, if we have one. 562 if (storeConnection != null) { 563 try { 564 // close this out and forget we ever saw it. 565 storeConnection.close(); 566 } catch (MessagingException e) { 567 // ignore....this is a non-critical problem if this fails now. 568 } 569 storeConnection = null; 570 } 571 } 572 573 574 /** 575 * Test if this connection has a given capability. 576 * 577 * @param capability The capability name. 578 * 579 * @return true if this capability is in the list, false for a mismatch. 580 */ 581 public boolean hasCapability(String capability) { 582 if (capabilities == null) { 583 return false; 584 } 585 return capabilities.containsKey(capability); 586 } 587 } 588 589