View Javadoc

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