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