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.util.ArrayList;
021    import java.util.Iterator;
022    import java.util.List;
023    import java.util.Map;
024    import java.util.StringTokenizer;
025    
026    import javax.mail.MessagingException; 
027    import javax.mail.Session;
028    import javax.mail.Store;
029    
030    import javax.mail.StoreClosedException;
031    
032    import org.apache.geronimo.javamail.store.imap.IMAPStore; 
033    import org.apache.geronimo.javamail.util.ProtocolProperties; 
034    
035    public class IMAPConnectionPool {
036    
037        protected static final String MAIL_PORT = "port";
038        protected static final String MAIL_POOL_SIZE = "connectionpoolsize";
039        protected static final String MAIL_POOL_TIMEOUT = "connectionpooltimeout";
040        protected static final String MAIL_SEPARATE_STORE_CONNECTION = "separatestoreconnection";
041        
042        protected static final String MAIL_SASL_REALM = "sasl.realm"; 
043        protected static final String MAIL_AUTHORIZATIONID = "sasl.authorizationid"; 
044    
045        // 45 seconds, by default.
046        protected static final int DEFAULT_POOL_TIMEOUT = 45000;
047        protected static final String DEFAULT_MAIL_HOST = "localhost";
048        
049        protected static final int MAX_CONNECTION_RETRIES = 3; 
050        protected static final int MAX_POOL_WAIT = 500; 
051    
052    
053        // Our hosting Store instance
054        protected IMAPStore store;
055        // our Protocol abstraction 
056        protected ProtocolProperties props; 
057        // our list of created connections
058        protected List poolConnections = new ArrayList();
059        // our list of available connections 
060        protected List availableConnections = new ArrayList();
061        
062        // the dedicated Store connection (if we're configured that way)
063        protected IMAPConnection storeConnection = null;
064        
065        // our dedicated Store connection attribute
066        protected boolean dedicatedStoreConnection;
067        // the size of our connection pool (by default, we only keep a single connection in the pool)
068        protected int poolSize = 1;
069        // the connection timeout property
070        protected long poolTimeout;
071        // our debug flag
072        protected boolean debug;
073    
074        // the target host
075        protected String host;
076        // the target server port.
077        protected int port;
078        // the username we connect with
079        protected String username;
080        // the authentication password.
081        protected String password;
082        // the SASL realm name 
083        protected String realm; 
084        // the authorization id.  With IMAP, it's possible to 
085        // log on with another's authorization. 
086        protected String authid; 
087        // Turned on when the store is closed for business. 
088        protected boolean closed = false; 
089        // the connection capabilities map
090        protected Map capabilities; 
091    
092        /**
093         * Create a connection pool associated with a give IMAPStore instance.  The
094         * connection pool manages handing out connections for both the Store and
095         * Folder and Message usage.
096         * 
097         * Depending on the session properties, the Store may be given a dedicated
098         * connection, or will share connections with the Folders.  Connections may
099         * 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