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    
019    package org.apache.geronimo.javamail.store.imap;
020    
021    import java.io.File;
022    import java.io.IOException;
023    import java.io.PrintStream;
024    import java.util.ArrayList;
025    import java.util.Iterator;
026    import java.util.LinkedList;
027    import java.util.List;
028    
029    import javax.mail.AuthenticationFailedException;
030    import javax.mail.Folder;
031    import javax.mail.MessagingException;
032    import javax.mail.Quota;
033    import javax.mail.QuotaAwareStore;
034    import javax.mail.Session;
035    import javax.mail.Store;
036    import javax.mail.URLName;
037    import javax.mail.event.StoreEvent; 
038    
039    import org.apache.geronimo.javamail.store.imap.connection.IMAPConnection; 
040    import org.apache.geronimo.javamail.store.imap.connection.IMAPConnectionPool; 
041    import org.apache.geronimo.javamail.store.imap.connection.IMAPOkResponse; 
042    import org.apache.geronimo.javamail.store.imap.connection.IMAPNamespaceResponse; 
043    import org.apache.geronimo.javamail.store.imap.connection.IMAPNamespace; 
044    import org.apache.geronimo.javamail.store.imap.connection.IMAPServerStatusResponse; 
045    import org.apache.geronimo.javamail.store.imap.connection.IMAPUntaggedResponse; 
046    import org.apache.geronimo.javamail.store.imap.connection.IMAPUntaggedResponseHandler; 
047    import org.apache.geronimo.javamail.util.ProtocolProperties;
048    
049    /**
050     * IMAP implementation of javax.mail.Store
051     * POP protocol spec is implemented in
052     * org.apache.geronimo.javamail.store.pop3.IMAPConnection
053     *
054     * @version $Rev: 707037 $ $Date: 2008-10-22 07:34:53 -0400 (Wed, 22 Oct 2008) $
055     */
056    
057    public class IMAPStore extends Store implements QuotaAwareStore, IMAPUntaggedResponseHandler {
058        // the default connection ports for secure and non-secure variations
059        protected static final int DEFAULT_IMAP_PORT = 143;
060        protected static final int DEFAULT_IMAP_SSL_PORT = 993;
061        
062        protected static final String MAIL_STATUS_TIMEOUT = "statuscacheimeout";
063        protected static final int DEFAULT_STATUS_TIMEOUT = 1000; 
064        
065        // our accessor for protocol properties and the holder of 
066        // protocol-specific information 
067        protected ProtocolProperties props; 
068    
069        // the connection pool we use for access 
070            protected IMAPConnectionPool connectionPool;
071    
072        // the root folder
073        protected IMAPRootFolder root;
074    
075        // the list of open folders (which also represents an open connection).
076        protected List openFolders = new LinkedList();
077    
078        // our session provided debug output stream.
079        protected PrintStream debugStream;
080        // the debug flag 
081        protected boolean debug; 
082        // until we're connected, we're closed 
083        boolean closedForBusiness = true; 
084        // The timeout value for our status cache 
085        long statusCacheTimeout = 0; 
086    
087        /**
088         * Construct an IMAPStore item.
089         *
090         * @param session The owning javamail Session.
091         * @param urlName The Store urlName, which can contain server target information.
092         */
093            public IMAPStore(Session session, URLName urlName) {
094            // we're the imap protocol, our default connection port is 119, and don't use 
095            // an SSL connection for the initial hookup 
096                    this(session, urlName, "imap", false, DEFAULT_IMAP_PORT);
097            }
098                                                              
099        /**
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    }