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 }