You are viewing a plain text version of this content. The canonical link for it is here.
Posted to scm@geronimo.apache.org by ri...@apache.org on 2007/11/13 13:57:53 UTC
svn commit: r594520 [2/10] - in
/geronimo/javamail/trunk/geronimo-javamail_1.4: ./
geronimo-javamail_1.4_provider/src/main/java/org/apache/geronimo/javamail/authentication/
geronimo-javamail_1.4_provider/src/main/java/org/apache/geronimo/javamail/handl...
Added: geronimo/javamail/trunk/geronimo-javamail_1.4/geronimo-javamail_1.4_provider/src/main/java/org/apache/geronimo/javamail/store/imap/IMAPFolder.java
URL: http://svn.apache.org/viewvc/geronimo/javamail/trunk/geronimo-javamail_1.4/geronimo-javamail_1.4_provider/src/main/java/org/apache/geronimo/javamail/store/imap/IMAPFolder.java?rev=594520&view=auto
==============================================================================
--- geronimo/javamail/trunk/geronimo-javamail_1.4/geronimo-javamail_1.4_provider/src/main/java/org/apache/geronimo/javamail/store/imap/IMAPFolder.java (added)
+++ geronimo/javamail/trunk/geronimo-javamail_1.4/geronimo-javamail_1.4_provider/src/main/java/org/apache/geronimo/javamail/store/imap/IMAPFolder.java Tue Nov 13 04:57:39 2007
@@ -0,0 +1,2386 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.geronimo.javamail.store.imap;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Vector;
+
+import javax.mail.*;
+import javax.mail.event.ConnectionEvent;
+import javax.mail.event.FolderEvent;
+import javax.mail.event.MessageChangedEvent;
+import javax.mail.search.FlagTerm;
+import javax.mail.search.SearchTerm;
+
+import org.apache.geronimo.javamail.store.imap.connection.IMAPConnection;
+import org.apache.geronimo.javamail.store.imap.connection.IMAPFetchDataItem;
+import org.apache.geronimo.javamail.store.imap.connection.IMAPFetchResponse;
+import org.apache.geronimo.javamail.store.imap.connection.IMAPFlags;
+import org.apache.geronimo.javamail.store.imap.connection.IMAPListResponse;
+import org.apache.geronimo.javamail.store.imap.connection.IMAPMailboxStatus;
+import org.apache.geronimo.javamail.store.imap.connection.IMAPSizeResponse;
+import org.apache.geronimo.javamail.store.imap.connection.IMAPUid;
+import org.apache.geronimo.javamail.store.imap.connection.IMAPUntaggedResponse;
+import org.apache.geronimo.javamail.store.imap.connection.IMAPUntaggedResponseHandler;
+
+/**
+ * The base IMAP implementation of the javax.mail.Folder
+ * This is a base class for both the Root IMAP server and each IMAP group folder.
+ * @see javax.mail.Folder
+ *
+ * @version $Rev$
+ */
+public class IMAPFolder extends Folder implements UIDFolder, IMAPUntaggedResponseHandler {
+
+ /**
+ * Special profile item used for fetching SIZE and HEADER information.
+ * These items are extensions that Sun has added to their IMAPFolder immplementation.
+ * We're supporting the same set.
+ */
+ public static class FetchProfileItem extends FetchProfile.Item {
+ public static final FetchProfileItem HEADERS = new FetchProfileItem("HEADERS");
+ public static final FetchProfileItem SIZE = new FetchProfileItem("SIZE");
+
+ protected FetchProfileItem(String name) {
+ super(name);
+ }
+ }
+
+ // marker that we don't know the separator yet for this folder.
+ // This occurs when we obtain a folder reference from the
+ // default folder. At that point, we've not queried the
+ // server for specifics yet.
+ static final protected char UNDETERMINED = 0;
+
+ // our attached session
+ protected Session session;
+ // retrieved messages, mapped by sequence number.
+ protected LinkedList messages;
+ // mappings of UIDs to retrieved messages.
+ protected Map uidCache;
+
+ // the separator the server indicates is used as the hierarchy separator
+ protected char separator;
+ // the "full" name of the folder. This is the fully qualified path name for the folder returned by
+ // the IMAP server. Elements of the hierarchy are delimited by "separator" characters.
+ protected String fullname;
+ // the name of this folder. The is the last element of the fully qualified name.
+ protected String name;
+ // the folder open state
+ protected boolean folderOpen = false;
+ // the type information on what the folder can hold
+ protected int folderType;
+ // the subscription status
+ protected boolean subscribed = false;
+
+ // the message identifier ticker, used to assign message numbers.
+ protected int nextMessageID = 1;
+ // the current count of messages in our cache.
+ protected int maxSequenceNumber = 0;
+ // the reported count of new messages (updated as a result of untagged message resposes)
+ protected int recentMessages = -1;
+ // the reported count of unseen messages
+ protected int unseenMessages = 0;
+ // the uidValidity value reported back from the server
+ protected long uidValidity = 0;
+ // the uidNext value reported back from the server
+ protected long uidNext = 0;
+ // the persistent flags we save in the store
+ protected Flags permanentFlags;
+ // the settable flags the server reports back to us
+ protected Flags availableFlags;
+ // Our cached status information. We will only hold this for the timeout interval.
+ protected IMAPMailboxStatus cachedStatus;
+ // Folder information retrieved from the server. Good info here indicates the
+ // folder exists.
+ protected IMAPListResponse listInfo;
+ // the configured status cache timeout value.
+ protected long statusCacheTimeout;
+ // the last time we took a status snap shot.
+ protected long lastStatusTimeStamp;
+ // Our current connection. We get one of these when opened, and release it when closed.
+ // We do this because for any folder (and message) operations, the folder must be selected on
+ // the connection.
+ // Note, however, that there are operations which will require us to borrow a connection
+ // temporarily because we need to touch the server when the folder is not open. In those
+ // cases, we grab a connection, then immediately return it to the pool.
+ protected IMAPConnection currentConnection;
+
+
+
+ /**
+ * Super class constructor the base IMAPFolder class.
+ *
+ * @param store The javamail store this folder is attached to.
+ * @param fullname The fully qualified name of this folder.
+ * @param separator The separtor character used to delimit the different
+ * levels of the folder hierarchy. This is used to
+ * decompose the full name into smaller parts and
+ * create the names of subfolders.
+ */
+ protected IMAPFolder(IMAPStore store, String fullname, char separator) {
+ super(store);
+ this.session = store.getSession();
+ this.fullname = fullname;
+ this.separator = separator;
+ // get the status timeout value from the folder.
+ statusCacheTimeout = store.statusCacheTimeout;
+ }
+
+ /**
+ * Retrieve the folder name. This is the simple folder
+ * name at the its hiearchy level. This can be invoked when the folder is closed.
+ *
+ * @return The folder's name.
+ */
+ public String getName() {
+ // At the time we create the folder, we might not know the separator character yet.
+ // Because of this we need to delay creating the name element until
+ // it's required.
+ if (name == null) {
+ // extract the name from the full name
+ int lastLevel = -1;
+ try {
+ lastLevel = fullname.lastIndexOf(getSeparator());
+ } catch (MessagingException e) {
+ // not likely to occur, but the link could go down before we
+ // get this. Just assume a failure to locate the character
+ // occurred.
+ }
+ if (lastLevel == -1) {
+ name = fullname;
+ }
+ else {
+ name = fullname.substring(lastLevel + 1);
+ }
+ }
+ return name;
+ }
+
+ /**
+ * Retrieve the folder's full name (including hierarchy information).
+ * This can be invoked when the folder is closed.
+ *
+ * @return The full name value.
+ */
+ public String getFullName() {
+ return fullname;
+ }
+
+
+
+ /**
+ * Return the parent for this folder; if the folder is at the root of a heirarchy
+ * this returns null.
+ * This can be invoked when the folder is closed.
+ *
+ * @return this folder's parent
+ * @throws MessagingException
+ */
+ public Folder getParent() throws MessagingException {
+ // NB: We need to use the method form because the separator
+ // might not have been retrieved from the server yet.
+ char separator = getSeparator();
+ // we don't hold a reference to the parent folder, as that would pin the instance in memory
+ // as long as any any leaf item in the hierarchy is still open.
+ int lastLevel = fullname.lastIndexOf(separator);
+ // no parent folder? Get the root one from the Store.
+ if (lastLevel == -1) {
+ return ((IMAPStore)store).getDefaultFolder();
+ }
+ else {
+ // create a folder for the parent.
+ return new IMAPFolder((IMAPStore)store, fullname.substring(0, lastLevel), separator);
+ }
+ }
+
+
+ /**
+ * Check to see if this folder physically exists in the store.
+ * This can be invoked when the folder is closed.
+ *
+ * @return true if the folder really exists
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized boolean exists() throws MessagingException {
+ IMAPConnection connection = getConnection();
+ try {
+ return checkExistance(connection);
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ /**
+ * Internal routine for checking existance using an
+ * already obtained connection. Used for situations
+ * where the list information needs updating but
+ * we'd end up acquiring a new connection because
+ * the folder isn't open yet.
+ *
+ * @param connection The connection to use.
+ *
+ * @return true if the folder exists, false for non-existence.
+ * @exception MessagingException
+ */
+ private boolean checkExistance(IMAPConnection connection) throws MessagingException {
+ // get the list response for this folder.
+ List responses = connection.list("", fullname);
+ // NB, this grabs the latest information and updates
+ // the type information also. Note also that we need to
+ // use the mailbox name, not the full name. This is so
+ // the namespace folders will return the correct response.
+ listInfo = findListResponse(responses, getMailBoxName());
+
+ if (listInfo == null) {
+ return false;
+ }
+
+ // update the type information from the status.
+ folderType = 0;
+ if (!listInfo.noinferiors) {
+ folderType |= HOLDS_FOLDERS;
+ }
+ if (!listInfo.noselect) {
+ folderType |= HOLDS_MESSAGES;
+ }
+
+ // also update the separator information. This will allow
+ // use to skip a call later
+ separator = listInfo.separator;
+ // this can be omitted in the response, so assume a default
+ if (separator == '\0') {
+ separator = '/';
+ }
+
+ // updated ok, so it must be there.
+ return true;
+ }
+
+
+
+ /**
+ * Return a list of folders from this Folder's namespace that match the supplied pattern.
+ * Patterns may contain the following wildcards:
+ * <ul><li>'%' which matches any characater except hierarchy delimiters</li>
+ * <li>'*' which matches any character including hierarchy delimiters</li>
+ * </ul>
+ * This can be invoked when the folder is closed.
+ *
+ * @param pattern the pattern to search for
+ *
+ * @return a possibly empty array containing Folders that matched the pattern
+ * @throws MessagingException
+ * if there was a problem accessing the store
+ */
+ public synchronized Folder[] list(String pattern) throws MessagingException {
+ // go filter the folders based on the pattern. The server does most of the
+ // heavy lifting on the pattern matching.
+ return filterFolders(pattern, false);
+ }
+
+
+ /**
+ * Return a list of folders to which the user is subscribed and which match the supplied pattern.
+ * If the store does not support the concept of subscription then this should match against
+ * all folders; the default implementation of this method achieves this by defaulting to the
+ * {@link #list(String)} method.
+ *
+ * @param pattern the pattern to search for
+ *
+ * @return a possibly empty array containing subscribed Folders that matched the pattern
+ * @throws MessagingException
+ * if there was a problem accessing the store
+ */
+ public synchronized Folder[] listSubscribed(String pattern) throws MessagingException {
+ // go filter the folders based on the pattern. The server does most of the
+ // heavy lifting on the pattern matching.
+ return filterFolders(pattern, true);
+ }
+
+
+ /**
+ * Return the character used by this folder's Store to separate path components.
+ *
+ * @return the name separater character
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized char getSeparator() throws MessagingException {
+ // not determined yet, we need to ask the server for the information
+ if (separator == UNDETERMINED) {
+ IMAPConnection connection = getConnection();
+ try {
+ List responses = connection.list("", fullname);
+ IMAPListResponse info = findListResponse(responses, fullname);
+
+ // if we didn't get any hits, then we just assume a reasonable default.
+ if (info == null) {
+ separator = '/';
+ }
+ else {
+ separator = info.separator;
+ // this can be omitted in the response, so assume a default
+ if (separator == '\0') {
+ separator = '/';
+ }
+ }
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+ return separator;
+ }
+
+
+ /**
+ * Return whether this folder can hold just messages or also
+ * subfolders.
+ *
+ * @return The combination of Folder.HOLDS_MESSAGES and Folder.HOLDS_FOLDERS, depending
+ * on the folder capabilities.
+ * @exception MessagingException
+ */
+ public int getType() throws MessagingException {
+ // checking the validity will update the type information
+ // if it succeeds.
+ checkFolderValidity();
+ return folderType;
+ }
+
+
+ /**
+ * Create a new folder capable of containing subfolder and/or messages as
+ * determined by the type parameter. Any hierarchy defined by the folder
+ * name will be recursively created.
+ * If the folder was sucessfully created, a {@link FolderEvent#CREATED CREATED FolderEvent}
+ * is sent to all FolderListeners registered with this Folder or with the Store.
+ *
+ * @param newType the type, indicating if this folder should contain subfolders, messages or both
+ *
+ * @return true if the folder was sucessfully created
+ * @throws MessagingException
+ * if there was a problem accessing the store
+ */
+ public synchronized boolean create(int newType) throws MessagingException {
+ IMAPConnection connection = getConnection();
+ try {
+
+ // by default, just create using the fullname.
+ String newPath = fullname;
+
+ // if this folder is expected to only hold additional folders, we need to
+ // add a separator on to the end when we create this.
+ if ((newType & HOLDS_MESSAGES) == 0) {
+ newPath = fullname + separator;
+ }
+ try {
+ // go create this
+ connection.createMailbox(newPath);
+ // verify this exists...also updates some of the status
+ boolean reallyCreated = checkExistance(connection);
+ // broadcast a creation event.
+ notifyFolderListeners(FolderEvent.CREATED);
+ return reallyCreated;
+ } catch (MessagingException e) {
+ //TODO add folder level debug logging.
+ }
+ // we have a failure
+ return false;
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+
+ /**
+ * Return the subscription status of this folder.
+ *
+ * @return true if the folder is marked as subscribed, false for
+ * unsubscribed.
+ */
+ public synchronized boolean isSubscribed() {
+ try {
+ IMAPConnection connection = getConnection();
+ try {
+ // get the lsub response for this folder.
+ List responses = connection.listSubscribed("", fullname);
+
+ IMAPListResponse response = findListResponse(responses, fullname);
+ if (response == null) {
+ return false;
+ }
+ else {
+ // a NOSELECT flag response indicates the mailbox is no longer
+ // selectable, so it's also no longer subscribed to.
+ return !response.noselect;
+ }
+ } finally {
+ releaseConnection(connection);
+ }
+ } catch (MessagingException e) {
+ // Can't override to throw a MessagingException on this method, so
+ // just swallow any exceptions and assume false is the answer.
+ }
+ return false;
+ }
+
+
+ /**
+ * Set or clear the subscription status of a file.
+ *
+ * @param flag
+ * The new subscription state.
+ */
+ public synchronized void setSubscribed(boolean flag) throws MessagingException {
+ IMAPConnection connection = getConnection();
+ try {
+ if (flag) {
+ connection.subscribe(fullname);
+ }
+ else {
+ connection.unsubscribe(fullname);
+ }
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ /**
+ * Check to see if this Folder conatins messages with the {@link Flag.RECENT} flag set.
+ * This can be used when the folder is closed to perform a light-weight check for new mail;
+ * to perform an incremental check for new mail the folder must be opened.
+ *
+ * @return true if the Store has recent messages
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized boolean hasNewMessages() throws MessagingException {
+ // the folder must exist for this to work.
+ checkFolderValidity();
+
+ // get the freshest status information.
+ refreshStatus(true);
+ // return the indicator from the message state.
+ return recentMessages > 0;
+ }
+
+ /**
+ * Get the Folder determined by the supplied name; if the name is relative
+ * then it is interpreted relative to this folder. This does not check that
+ * the named folder actually exists.
+ *
+ * @param name the name of the folder to return
+ * @return the named folder
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public Folder getFolder(String name) throws MessagingException {
+ // this must be a real, valid folder to hold a subfolder
+ checkFolderValidity();
+ if (!holdsFolders()) {
+ throw new MessagingException("Folder " + fullname + " cannot hold subfolders");
+ }
+ // our separator does not get determined until we ping the server for it. We
+ // might need to do that now, so we need to use the getSeparator() method to retrieve this.
+ char separator = getSeparator();
+
+ return new IMAPFolder((IMAPStore)store, fullname + separator + name, separator);
+ }
+
+
+ /**
+ * Delete this folder and possibly any subfolders. This operation can only be
+ * performed on a closed folder.
+ * If recurse is true, then all subfolders are deleted first, then any messages in
+ * this folder are removed and it is finally deleted; {@link FolderEvent#DELETED}
+ * events are sent as appropriate.
+ * If recurse is false, then the behaviour depends on the folder type and store
+ * implementation as followd:
+ * <ul>
+ * <li>If the folder can only conrain messages, then all messages are removed and
+ * then the folder is deleted; a {@link FolderEvent#DELETED} event is sent.</li>
+ * <li>If the folder can onlu contain subfolders, then if it is empty it will be
+ * deleted and a {@link FolderEvent#DELETED} event is sent; if the folder is not
+ * empty then the delete fails and this method returns false.</li>
+ * <li>If the folder can contain both subfolders and messages, then if the folder
+ * does not contain any subfolders, any messages are deleted, the folder itself
+ * is deleted and a {@link FolderEvent#DELETED} event is sent; if the folder does
+ * contain subfolders then the implementation may choose from the following three
+ * behaviors:
+ * <ol>
+ * <li>it may return false indicting the operation failed</li>
+ * <li>it may remove all messages within the folder, send a {@link FolderEvent#DELETED}
+ * event, and then return true to indicate the delete was performed. Note this does
+ * not delete the folder itself and the {@link #exists()} operation for this folder
+ * will return true</li>
+ * <li>it may remove all messages within the folder as per the previous option; in
+ * addition it may change the type of the Folder to only HOLDS_FOLDERS indictaing
+ * that messages may no longer be added</li>
+ * </li>
+ * </ul>
+ * FolderEvents are sent to all listeners registered with this folder or
+ * with the Store.
+ *
+ * @param recurse whether subfolders should be recursively deleted as well
+ * @return true if the delete operation succeeds
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized boolean delete(boolean recurse) throws MessagingException {
+ // we must be in the closed state.
+ checkClosed();
+
+ // if recursive, get the list of subfolders and delete them first.
+ if (recurse) {
+
+ Folder[] subfolders = list();
+ for (int i = 0; i < subfolders.length; i++) {
+ // this is a recursive delete also
+ subfolders[i].delete(true);
+ }
+ }
+
+ IMAPConnection connection = getConnection();
+ try {
+ // delete this one now.
+ connection.deleteMailbox(fullname);
+ // this folder no longer exists on the server.
+ listInfo = null;
+
+ // notify interested parties about the deletion.
+ notifyFolderListeners(FolderEvent.DELETED);
+ return true;
+
+ } catch (MessagingException e) {
+ // ignored
+ } finally {
+ releaseConnection(connection);
+ }
+ return false;
+ }
+
+
+ /**
+ * Rename this folder; the folder must be closed.
+ * If the rename is successfull, a {@link FolderEvent#RENAMED} event is sent to
+ * all listeners registered with this folder or with the store.
+ *
+ * @param newName the new name for this folder
+ * @return true if the rename succeeded
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized boolean renameTo(Folder f) throws MessagingException {
+ // we must be in the closed state.
+ checkClosed();
+ // but we must also exist
+ checkFolderValidity();
+
+ IMAPConnection connection = getConnection();
+ try {
+ // delete this one now.
+ connection.renameMailbox(fullname, f.getFullName());
+ // we renamed, so get a fresh set of status
+ refreshStatus(false);
+
+ // notify interested parties about the deletion.
+ notifyFolderRenamedListeners(f);
+ return true;
+ } catch (MessagingException e) {
+ // ignored
+ } finally {
+ releaseConnection(connection);
+ }
+ return false;
+ }
+
+
+ /**
+ * Open this folder; the folder must be able to contain messages and
+ * must currently be closed. If the folder is opened successfully then
+ * a {@link ConnectionEvent#OPENED} event is sent to listeners registered
+ * with this Folder.
+ * <p/>
+ * Whether the Store allows multiple connections or if it allows multiple
+ * writers is implementation defined.
+ *
+ * @param mode READ_ONLY or READ_WRITE
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized void open(int mode) throws MessagingException {
+
+ // we use a synchronized block rather than use a synchronized method so that we
+ // can notify the event listeners while not holding the lock.
+ synchronized(this) {
+ // can only be performed on a closed folder
+ checkClosed();
+ // ask the store to kindly hook us up with a connection.
+ // We're going to hang on to this until we're closed, so store it in
+ // the Folder field. We need to make sure our mailbox is selected while
+ // we're working things.
+ currentConnection = ((IMAPStore)store).getFolderConnection(this);
+ // we need to make ourselves a handler of unsolicited responses
+ currentConnection.addResponseHandler(this);
+ // record our open mode
+ this.mode = mode;
+
+
+ try {
+ // try to open, which gives us a lot of initial mailbox state.
+ IMAPMailboxStatus status = currentConnection.openMailbox(fullname, mode == Folder.READ_ONLY);
+
+ // not available in the requested mode?
+ if (status.mode != mode) {
+ // trying to open READ_WRITE and this isn't available?
+ if (mode == READ_WRITE) {
+ throw new ReadOnlyFolderException(this, "Cannot open READ_ONLY folder in READ_WRITE mode");
+ }
+ }
+
+ // save this status and when we got it for later updating.
+ cachedStatus = status;
+ // mark when we got this
+ lastStatusTimeStamp = System.currentTimeMillis();
+
+ // now copy the status information over and flip over the open sign.
+ this.mode = status.mode;
+ maxSequenceNumber = status.messages;
+ recentMessages = status.recentMessages;
+ uidValidity = status.uidValidity;
+ uidNext = status.uidNext;
+
+ availableFlags = status.availableFlags;
+ permanentFlags = status.permanentFlags;
+
+ // create a our caches
+ messages = new LinkedList();
+ uidCache = new HashMap();
+ // this is a real pain, but because we need to track updates
+ // to message sequence numbers while the folder is open, the
+ // messages list needs to be populated with Message objects
+ // to keep track of things. The IMAPMessage objects will not
+ // retrieve information from the server until required, so they're
+ // relatively lightweight at this point.
+ populateMessageCache();
+
+ // we're open for business folks!
+ folderOpen = true;
+ notifyConnectionListeners(ConnectionEvent.OPENED);
+ } finally {
+ // NB: this doesn't really release this, but it does drive
+ // the processing of any unsolicited responses.
+ releaseConnection(currentConnection);
+ }
+ }
+ }
+
+
+ /**
+ * Populate the message cache with dummy messages, ensuring we're filled
+ * up to the server-reported number of messages.
+ *
+ * @exception MessagingException
+ */
+ protected void populateMessageCache() {
+ // spin through the server-reported number of messages and add a dummy one to
+ // the cache. The message number is assigned from the id counter, the
+ // sequence number is the cache position + 1.
+ for (int i = messages.size(); i < maxSequenceNumber; i++) {
+ messages.add(new IMAPMessage(this, ((IMAPStore)store), nextMessageID++, i+1));
+ }
+ }
+
+
+ /**
+ * Close this folder; it must already be open.
+ * A @link ConnectionEvent#CLOSED} event is sent to all listeners registered
+ {*
+ * with this folder.
+ *
+ * @param expunge whether to expunge all deleted messages
+ * @throws MessagingException if there was a problem accessing the store; the folder is still closed
+ */
+ public synchronized void close(boolean expunge) throws MessagingException {
+ // Can only be performed on an open folder
+ checkOpen();
+ cleanupFolder(expunge, false);
+ }
+
+
+ /**
+ * Do folder cleanup. This is used both for normal
+ * close operations, and adnormal closes where the
+ * server has sent us a BYE message.
+ *
+ * @param expunge Indicates whether open messages should be expunged.
+ * @param disconnected
+ * The disconnected flag. If true, the server has cut
+ * us off, which means our connection can not be returned
+ * to the connection pool.
+ *
+ * @exception MessagingException
+ */
+ protected void cleanupFolder(boolean expunge, boolean disconnected) throws MessagingException {
+ folderOpen = false;
+ uidCache = null;
+ messages = null;
+ // if we have a connection active at the moment
+ if (currentConnection != null) {
+ // was this a forced disconnect by the server?
+ if (disconnected) {
+ currentConnection.setClosed();
+ }
+ else {
+ // The CLOSE operation depends on what mode was used to select the mailbox.
+ // If we're open in READ-WRITE mode, we used a SELECT operation. When CLOSE
+ // is issued, any deleted messages will be expunged. If we've been asked not
+ // to expunge the messages, we have a problem. The solution is to reselect the
+ // mailbox using EXAMINE, which will not expunge messages when closed.
+ if (mode == READ_WRITE && !expunge) {
+ // we can ignore the result...we're just switching modes.
+ currentConnection.openMailbox(fullname, true);
+ }
+
+ // have this close the selected mailbox
+ currentConnection.closeMailbox();
+ }
+ currentConnection.removeResponseHandler(this);
+ // we need to release the connection to the Store once we're closed
+ ((IMAPStore)store).releaseFolderConnection(this, currentConnection);
+ currentConnection = null;
+ }
+ notifyConnectionListeners(ConnectionEvent.CLOSED);
+ }
+
+
+ /**
+ * Tests the open status of the folder.
+ *
+ * @return true if the folder is open, false otherwise.
+ */
+ public boolean isOpen() {
+ return folderOpen;
+ }
+
+ /**
+ * Get the permanentFlags
+ *
+ * @return The set of permanent flags we support (only SEEN).
+ */
+ public synchronized Flags getPermanentFlags() {
+ if (permanentFlags != null) {
+ // we need a copy of our master set.
+ return new Flags(permanentFlags);
+ }
+ else {
+ // a null return is expected if not there.
+ return null;
+ }
+ }
+
+
+ /**
+ * Return the number of messages this folder contains.
+ * If this operation is invoked on a closed folder, the implementation
+ * may choose to return -1 to avoid the expense of opening the folder.
+ *
+ * @return the number of messages, or -1 if unknown
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized int getMessageCount() throws MessagingException {
+ checkFolderValidity();
+
+ // if we haven't opened the folder yet, we might not have good status information.
+ // go request some, which updates the folder fields also.
+ refreshStatus(false);
+ return maxSequenceNumber;
+ }
+
+ /**
+ * Return the numbew of messages in this folder that have the {@link Flag.RECENT} flag set.
+ * If this operation is invoked on a closed folder, the implementation
+ * may choose to return -1 to avoid the expense of opening the folder.
+ * The default implmentation of this method iterates over all messages
+ * in the folder; subclasses should override if possible to provide a more
+ * efficient implementation.
+ *
+ * NB: This is an override of the default Folder implementation, which
+ * examines each of the messages in the folder. IMAP has more efficient
+ * mechanisms for grabbing the information.
+ *
+ * @return the number of new messages, or -1 if unknown
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized int getNewMessageCount() throws MessagingException {
+ // the folder must be a real one for this to work.
+ checkFolderValidity();
+ // now get current status from the folder
+ refreshStatus(false);
+ // this should be current now.
+ return recentMessages;
+ }
+
+
+
+ /**
+ * Return the number of messages in this folder that do not have the {@link Flag.SEEN} flag set.
+ * If this operation is invoked on a closed folder, the implementation
+ * may choose to return -1 to avoid the expense of opening the folder.
+ * The default implmentation of this method iterates over all messages
+ * in the folder; subclasses should override if possible to provide a more
+ * efficient implementation.
+ *
+ * NB: This is an override of the default Folder implementation, which
+ * examines each of the messages in the folder. IMAP has more efficient
+ * mechanisms for grabbing the information.
+ *
+ * @return the number of new messages, or -1 if unknown
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized int getUnreadMessageCount() throws MessagingException {
+ checkFolderValidity();
+ // if we haven't opened the folder yet, we might not have good status information.
+ // go request some, which updates the folder fields also.
+ if (!folderOpen) {
+ refreshStatus(false);
+ }
+ else {
+ // if we have an open connection, then search the folder for any messages
+ // marked UNSEEN.
+
+ // UNSEEN is a false test on SEEN using the search criteria.
+ SearchTerm criteria = new FlagTerm(new Flags(Flags.Flag.SEEN), false);
+
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+ try {
+ // search using the connection directly rather than calling our search() method so we don't
+ // need to instantiate each of the matched messages. We're really only interested in the count
+ // right now.
+ int[] matches = connection.searchMailbox(criteria);
+ // update the unseen count.
+ unseenMessages = matches == null ? 0 : matches.length;
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+ // return our current message count.
+ return unseenMessages;
+ }
+
+
+
+ /**
+ * Return the number of messages in this folder that have the {@link Flag.DELETED} flag set.
+ * If this operation is invoked on a closed folder, the implementation
+ * may choose to return -1 to avoid the expense of opening the folder.
+ * The default implmentation of this method iterates over all messages
+ * in the folder; subclasses should override if possible to provide a more
+ * efficient implementation.
+ *
+ * @return the number of new messages, or -1 if unknown
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized int getDeletedMessageCount() throws MessagingException {
+ checkFolderValidity();
+
+ // if we haven't opened the folder yet, we might not have good status information.
+ // go request some, which updates the folder fields also.
+ if (!folderOpen) {
+ // the status update doesn't return deleted messages. These can only be obtained by
+ // searching an open folder. Just return a bail-out response
+ return -1;
+ }
+ else {
+ // if we have an open connection, then search the folder for any messages
+ // marked DELETED.
+
+ // UNSEEN is a false test on SEEN using the search criteria.
+ SearchTerm criteria = new FlagTerm(new Flags(Flags.Flag.DELETED), true);
+
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+ try {
+ // search using the connection directly rather than calling our search() method so we don't
+ // need to instantiate each of the matched messages. We're really only interested in the count
+ // right now.
+ int[] matches = connection.searchMailbox(criteria);
+ return matches == null ? 0 : matches.length;
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+ }
+
+
+ /**
+ * Retrieve the message with the specified index in this Folder;
+ * messages indices start at 1 not zero.
+ * Clients should note that the index for a specific message may change
+ * if the folder is expunged; {@link Message} objects should be used as
+ * references instead.
+ *
+ * @param msgNum The message sequence number of the target message.
+ *
+ * @return the message
+ * @throws MessagingException
+ * if there was a problem accessing the store
+ */
+ public synchronized Message getMessage(int msgNum) throws MessagingException {
+ // Can only be performed on an Open folder
+ checkOpen();
+ // Check the validity of the message number. This may require pinging the server to
+ // see if there are new messages in the folder.
+ checkMessageValidity(msgNum);
+ // ok, if the message number is within range, we should have this in the
+ // messages list. Just return the element.
+ return (Message)messages.get(msgNum - 1);
+ }
+
+
+ /**
+ * Retrieve a range of messages for this folder.
+ * messages indices start at 1 not zero.
+ *
+ * @param start Index of the first message to fetch, inclusive.
+ * @param end Index of the last message to fetch, inclusive.
+ *
+ * @return An array of the fetched messages.
+ * @throws MessagingException
+ * if there was a problem accessing the store
+ */
+ public synchronized Message[] getMessages(int start, int end) throws MessagingException {
+ // Can only be performed on an Open folder
+ checkOpen();
+ Message[] messageRange = new Message[end - start + 1];
+
+ for (int i = 0; i < messageRange.length; i++) {
+ // NB: getMessage() requires values that are origin 1, so there's
+ // no need to adjust the value by other than the start position.
+ messageRange[i] = getMessage(start + i);
+ }
+ return messageRange;
+ }
+
+
+ /**
+ * Append the supplied messages to this folder. A {@link MessageCountEvent} is sent
+ * to all listeners registered with this folder when all messages have been appended.
+ * If the array contains a previously expunged message, it must be re-appended to the Store
+ * and implementations must not abort this operation.
+ *
+ * @param msgs The array of messages to append to the folder.
+ *
+ * @throws MessagingException
+ * if there was a problem accessing the store
+ */
+ public synchronized void appendMessages(Message[] msgs) throws MessagingException {
+ checkFolderValidity();
+ for (int i = 0; i < msgs.length; i++) {
+ Message msg = msgs[i];
+
+ appendMessage(msg);
+ }
+ }
+
+ /**
+ * Hint to the store to prefetch information on the supplied messages.
+ * Subclasses should override this method to provide an efficient implementation;
+ * the default implementation in this class simply returns.
+ *
+ * @param messages messages for which information should be fetched
+ * @param profile the information to fetch
+ * @throws MessagingException if there was a problem accessing the store
+ * @see FetchProfile
+ */
+ public void fetch(Message[] messages, FetchProfile profile) throws MessagingException {
+
+ // we might already have the information being requested, so ask each of the
+ // messages in the list to evaluate itself against the profile. We'll only ask
+ // the server to send information that's required.
+ List fetchSet = new ArrayList();
+
+ for (int i = 0; i < messages.length; i++) {
+ Message msg = messages[i];
+ // the message is missing some of the information still. Keep this in the list.
+ // even if the message is only missing one piece of information, we still fetch everything.
+ if (((IMAPMessage)msg).evaluateFetch(profile)) {
+ fetchSet.add(msg);
+ }
+ }
+
+ // we've got everything already, no sense bothering the server
+ if (fetchSet.isEmpty()) {
+ return;
+ }
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+ try {
+ // ok, from this point onward, we don't want any threads messing with the
+ // message cache. A single processed EXPUNGE could make for a very bad day
+ synchronized(this) {
+ // get the message set for this
+ String messageSet = generateMessageSet(fetchSet);
+ // fetch all of the responses
+ List responses = connection.fetch(messageSet, profile);
+
+ // IMPORTANT: We must do our updates while synchronized to keep the
+ // cache from getting updated underneath us. This includes
+ // not releasing the connection until we're done to delay processing any
+ // pending expunge responses.
+ for (int i = 0; i < responses.size(); i++) {
+ IMAPFetchResponse response = (IMAPFetchResponse)responses.get(i);
+ Message msg = getMessage(response.getSequenceNumber());
+ // Belt and Braces. This should never be false.
+ if (msg != null) {
+ // have the message apply this to itself.
+ ((IMAPMessage)msg).updateMessageInformation(response);
+ }
+ }
+ }
+ } finally {
+ releaseConnection(connection);
+ }
+ return;
+ }
+
+ /**
+ * Set flags on the messages to the supplied value; all messages must belong to this folder.
+ * This method may be overridden by subclasses that can optimize the setting
+ * of flags on multiple messages at once; the default implementation simply calls
+ * {@link Message#setFlags(Flags, boolean)} for each supplied messages.
+ *
+ * @param messages whose flags should be set
+ * @param flags the set of flags to modify
+ * @param set Indicates whether the flags should be set or cleared.
+ *
+ * @throws MessagingException
+ * if there was a problem accessing the store
+ */
+ public void setFlags(Message[] messages, Flags flags, boolean set) throws MessagingException {
+ // this is a list of messages for the change broadcast after the update
+ List updatedMessages = new ArrayList();
+
+ synchronized(this) {
+ // the folder must be open and writeable.
+ checkOpenReadWrite();
+
+ // now make sure these are settable flags.
+ if (!availableFlags.contains(flags))
+ {
+ throw new MessagingException("The IMAP server does not support changing of this flag set");
+ }
+
+ // turn this into a set of message numbers
+ String messageSet = generateMessageSet(messages);
+ // if all of the messages have been expunged, nothing to do.
+ if (messageSet == null) {
+ return;
+ }
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+
+ try {
+ // and have the connection set this
+ List responses = connection.setFlags(messageSet, flags, set);
+ // retrieve each of the messages from our cache, and do the flag update.
+ // we need to keep the list so we can broadcast a change update event
+ // when we're finished.
+ for (int i = 0; i < responses.size(); i++) {
+ IMAPFetchResponse response = (IMAPFetchResponse)responses.get(i);
+
+ // get the updated message and update the internal state.
+ Message message = getMessage(response.sequenceNumber);
+ // this shouldn't happen, but it might have been expunged too.
+ if (message != null) {
+ ((IMAPMessage)message).updateMessageInformation(response);
+ updatedMessages.add(message);
+ }
+ }
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ // ok, we're no longer holding the lock. Now go broadcast the update for each
+ // of the affected messages.
+ for (int i = 0; i < updatedMessages.size(); i++) {
+ Message message = (Message)updatedMessages.get(i);
+ notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED, message);
+ }
+ }
+
+
+ /**
+ * Set flags on a range of messages to the supplied value.
+ * This method may be overridden by subclasses that can optimize the setting
+ * of flags on multiple messages at once; the default implementation simply
+ * gets each message and then calls {@link Message#setFlags(Flags, boolean)}.
+ *
+ * @param start first message end set
+ * @param end last message end set
+ * @param flags the set of flags end modify
+ * @param value Indicates whether the flags should be set or cleared.
+ *
+ * @throws MessagingException
+ * if there was a problem accessing the store
+ */
+ public synchronized void setFlags(int start, int end, Flags flags, boolean value) throws MessagingException {
+ Message[] msgs = new Message[end - start + 1];
+
+ for (int i = start; i <= end; i++) {
+ msgs[i] = getMessage(i);
+ }
+ // go do a bulk set operation on these messages
+ setFlags(msgs, flags, value);
+ }
+
+ /**
+ * Set flags on a set of messages to the supplied value.
+ * This method may be overridden by subclasses that can optimize the setting
+ * of flags on multiple messages at once; the default implementation simply
+ * gets each message and then calls {@link Message#setFlags(Flags, boolean)}.
+ *
+ * @param ids the indexes of the messages to set
+ * @param flags the set of flags end modify
+ * @param value Indicates whether the flags should be set or cleared.
+ *
+ * @throws MessagingException
+ * if there was a problem accessing the store
+ */
+ public synchronized void setFlags(int ids[], Flags flags, boolean value) throws MessagingException {
+ Message[] msgs = new Message[ids.length];
+
+ for (int i = 0; i <ids.length; i++) {
+ msgs[i] = getMessage(ids[i]);
+ }
+ // go do a bulk set operation on these messages
+ setFlags(msgs, flags, value);
+ }
+
+
+ /**
+ * Copy the specified messages to another folder.
+ * The default implementation simply appends the supplied messages to the
+ * target folder using {@link #appendMessages(Message[])}.
+ * @param messages the messages to copy
+ * @param folder the folder to copy to
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized void copyMessages(Message[] messages, Folder folder) throws MessagingException {
+ // the default implementation just appends the messages to the target. If
+ // we're copying between two folders of the same store, we can get the server to
+ // do most of the work for us without needing to fetch all of the message data.
+ // If we're dealing with two different Store instances, we need to do this the
+ // hardway.
+ if (getStore() != folder.getStore()) {
+ super.copyMessages(messages, folder);
+ return;
+ }
+
+ // turn this into a set of message numbers
+ String messageSet = generateMessageSet(messages);
+ // if all of the messages have been expunged, nothing to do.
+ if (messageSet == null) {
+ return;
+ }
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+
+ try {
+ // ask the server to copy this information over to the other mailbox.
+ connection.copyMessages(messageSet, folder.getFullName());
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+
+
+ /**
+ * Permanently delete all supplied messages that have the DELETED flag set from the Store.
+ * The original message indices of all messages actually deleted are returned and a
+ * {@link MessageCountEvent} event is sent to all listeners with this folder. The expunge
+ * may cause the indices of all messaged that remain in the folder to change.
+ *
+ * @return the original indices of messages that were actually deleted
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized Message[] expunge() throws MessagingException {
+ // must be open to do this.
+ checkOpen();
+ // and changes need to be allowed
+ checkReadWrite();
+
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+ List expunges = null;
+
+ try {
+ // send the expunge notification. This operation results in "nn EXPUNGE" responses getting returned
+ // for each expunged messages. These will be dispatched to our response handler, which will process
+ // the expunge operation. We could process this directly, but we may have received asynchronous
+ // expunge messages that also marked messages as expunged.
+ expunges = connection.expungeMailbox();
+ } finally {
+ releaseConnection(connection);
+ }
+
+ // we get one EXPUNGE message for each message that's expunged. They MUST be processed in
+ // order, as the message sequence numbers represent a relative position that takes into account
+ // previous expunge operations. For example, if message sequence numbers 5, 6, and 7 are
+ // expunged, we receive 3 expunge messages, all indicating that message 5 has been expunged.
+ Message[] messages = new Message[expunges.size()];
+
+ // now we need to protect the internal structures
+ synchronized (this) {
+ // expunge all of the messages from the message cache. This keeps the sequence
+ // numbers up to-date.
+ for (int i = 0; i < expunges.size(); i++) {
+ IMAPSizeResponse response = (IMAPSizeResponse)expunges.get(i);
+ messages[i] = expungeMessage(response.getSize());
+ }
+ }
+ // if we have messages that have been removed, broadcast the notification.
+ if (messages.length > 0) {
+ notifyMessageRemovedListeners(true, messages);
+ }
+
+ // note, we're expected to return an array in all cases, even if the expunged count was zero.
+ return messages;
+ }
+
+
+
+ /**
+ * Search the supplied messages for those that match the supplied criteria;
+ * messages must belong to this folder.
+ * The default implementation iterates through the messages, returning those
+ * whose {@link Message#match(javax.mail.search.SearchTerm)} method returns true;
+ * subclasses may provide a more efficient implementation.
+ *
+ * @param term the search criteria
+ * @param messages the messages to search
+ * @return an array containing messages that match the criteria
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized Message[] search(SearchTerm term) throws MessagingException {
+ // only allowed on open folders
+ checkOpen();
+
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+
+ try {
+ // just search everything
+ int[] messageNumbers = connection.searchMailbox(term);
+ return resolveMessages(messageNumbers);
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+
+
+ /**
+ * Search the supplied messages for those that match the supplied criteria;
+ * messages must belong to this folder.
+ * The default implementation iterates through the messages, returning those
+ * whose {@link Message#match(javax.mail.search.SearchTerm)} method returns true;
+ * subclasses may provide a more efficient implementation.
+ *
+ * @param term the search criteria
+ * @param messages the messages to search
+ * @return an array containing messages that match the criteria
+ * @throws MessagingException if there was a problem accessing the store
+ */
+ public synchronized Message[] search(SearchTerm term, Message[] messages) throws MessagingException {
+ // only allowed on open folders
+ checkOpen();
+
+ // turn this into a string specifier for these messages. We'll weed out the expunged messages first.
+ String messageSet = generateMessageSet(messages);
+
+ // If we have no "live" messages to search, just return now. We're required to return a non-null
+ // value, so give an empy array back.
+ if (messageSet == null) {
+ return new Message[0];
+ }
+
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+
+ try {
+
+ // now go do the search.
+ int[] messageNumbers = connection.searchMailbox(messageSet, term);
+ return resolveMessages(messageNumbers);
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ /**
+ * Get the UID validity value for this Folder.
+ *
+ * @return The current UID validity value, as a long.
+ * @exception MessagingException
+ */
+ public synchronized long getUIDValidity() throws MessagingException
+ {
+ // get the latest status to make sure we have the
+ // most current.
+ refreshStatus(true);
+ return uidValidity;
+ }
+
+ /**
+ * Retrieve a message using the UID rather than the
+ * message sequence number. Returns null if the message
+ * doesn't exist.
+ *
+ * @param uid The target UID.
+ *
+ * @return the Message object. Returns null if the message does
+ * not exist.
+ * @exception MessagingException
+ */
+ public synchronized Message getMessageByUID(long uid) throws MessagingException
+ {
+ // only allowed on open folders
+ checkOpen();
+
+ Long key = new Long(uid);
+ // first check to see if we have a cached value for this
+ synchronized(messages) {
+ Message msg = (Message)uidCache.get(key);
+ if (msg != null) {
+ return msg;
+ }
+ }
+
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+
+ try {
+ // locate the message identifier
+ IMAPUid imapuid = connection.getSequenceNumberForUid(uid);
+ // if nothing is returned, the message doesn't exist
+ if (imapuid == null) {
+ return null;
+ }
+
+
+ // retrieve the actual message object and place this in the UID cache
+ return retrieveMessageByUid(key, imapuid.messageNumber);
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ /**
+ * Get a series of messages using a UID range. The
+ * special value LASTUID can be used to mark the
+ * last available message.
+ *
+ * @param start The start of the UID range.
+ * @param end The end of the UID range. The special value
+ * LASTUID can be used to request all messages up
+ * to the last UID.
+ *
+ * @return An array containing all of the messages in the
+ * range.
+ * @exception MessagingException
+ */
+ public synchronized Message[] getMessagesByUID(long start, long end) throws MessagingException
+ {
+ // only allowed on open folders
+ checkOpen();
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+
+ try {
+ // locate the message identifier
+ List uids = connection.getSequenceNumbersForUids(start, end);
+ Message[] msgs = new Message[uids.size()];
+
+ // fill in each of the messages based on the returned value
+ for (int i = 0; i < msgs.length; i++) {
+ IMAPUid uid = (IMAPUid)uids.get(i);
+ msgs[i] = retrieveMessageByUid(new Long(uid.uid), uid.messageNumber);
+ }
+
+ return msgs;
+ } finally {
+ releaseConnection(connection);
+ }
+
+
+ }
+
+ /**
+ * Retrieve a set of messages by explicit UIDs. If
+ * any message in the list does not exist, null
+ * will be returned for the corresponding item.
+ *
+ * @param ids An array of UID values to be retrieved.
+ *
+ * @return An array of Message items the same size as the ids
+ * argument array. This array will contain null
+ * entries for any UIDs that do not exist.
+ * @exception MessagingException
+ */
+ public synchronized Message[] getMessagesByUID(long[] ids) throws MessagingException
+ {
+ // only allowed on open folders
+ checkOpen();
+
+ Message[] msgs = new Message[ids.length];
+
+ for (int i = 0; i < msgs.length; i++) {
+ msgs[i] = getMessageByUID(ids[i]);
+ }
+
+ return msgs;
+ }
+
+ /**
+ * Retrieve the UID for a message from this Folder.
+ * The argument Message MUST belong to this Folder
+ * instance, otherwise a NoSuchElementException will
+ * be thrown.
+ *
+ * @param message The target message.
+ *
+ * @return The UID associated with this message.
+ * @exception MessagingException
+ */
+ public synchronized long getUID(Message message) throws MessagingException
+ {
+ // verify this actually is in this folder.
+ checkMessageFolder(message);
+ IMAPMessage msg = (IMAPMessage)message;
+
+ // we might already know this bit of information
+ if (msg.getUID() != -1) {
+ return msg.getUID();
+ }
+
+ // ask the store to kindly hook us up with a connection.
+ IMAPConnection connection = getConnection();
+
+ try {
+ // locate the message identifier
+ IMAPUid imapuid = connection.getUidForSequenceNumber(msg.getMessageNumber());
+ // if nothing is returned, the message doesn't exist
+ if (imapuid == null) {
+ return -1;
+ }
+ // cache this information now that we've gotten it.
+ addToUidCache(new Long(imapuid.uid), getMessage(imapuid.messageNumber));
+ // return the UID information.
+ return imapuid.uid;
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ /**
+ * Retrieve a message from a UID/message mapping.
+ *
+ * @param key The UID key used for the mapping.
+ * @param msgNumber The message sequence number.
+ *
+ * @return The Message object corresponding to the message.
+ * @exception MessagingException
+ */
+ protected synchronized Message retrieveMessageByUid(Long key, int msgNumber) throws MessagingException
+ {
+ synchronized (messages) {
+ // first check the cache...this might have already been added.
+ Message msg = (Message)uidCache.get(key);
+ if (msg != null) {
+ return msg;
+ }
+
+ // retrieve the message by sequence number
+ msg = getMessage(msgNumber);
+ // add this to our UID mapping cache.
+ addToUidCache(key, msg);
+ return msg;
+ }
+ }
+
+
+ /**
+ * Add a message to the UID mapping cache, ensuring that
+ * the UID value is updated.
+ *
+ * @param key The UID key.
+ * @param msg The message to add.
+ */
+ protected void addToUidCache(Long key, Message msg) {
+ synchronized (messages) {
+ ((IMAPMessage)msg).setUID(key.longValue());
+ uidCache.put(key, msg);
+ }
+ }
+
+
+ /**
+ * Append a single message to the IMAP Folder.
+ *
+ * @param msg The message to append.
+ *
+ * @exception MessagingException
+ */
+ protected synchronized void appendMessage(Message msg) throws MessagingException
+ {
+ // sort out the dates. If no received date, use the sent date.
+ Date date = msg.getReceivedDate();
+ if (date == null) {
+ date = msg.getSentDate();
+ }
+
+ Flags flags = msg.getFlags();
+
+ // convert the message into an array of bytes we can attach as a literal.
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+
+ try {
+ msg.writeTo(out);
+ } catch (IOException e) {
+ }
+
+ // now issue the append command
+ IMAPConnection connection = getConnection();
+ try {
+ connection.appendMessage(getFullName(), date, flags, out.toByteArray());
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+
+ /**
+ * Retrieve the list of matching groups from the IMAP server using the LIST
+ * or LSUB command. The server does the wildcard matching for us.
+ *
+ * @param pattern
+ * The pattern string (in wildmat format) used to match.
+ *
+ * @return An array of folders for the matching groups.
+ */
+ protected synchronized Folder[] filterFolders(String pattern, boolean subscribed) throws MessagingException {
+ IMAPConnection connection = getConnection();
+ // this is used to filter out our own folder from the search
+ String root = fullname + getSeparator();
+
+ List responses = null;
+ try {
+
+
+ if (subscribed) {
+ // get the lsub response for this folder.
+ responses = connection.listSubscribed(root, pattern);
+ }
+ else {
+ // grab using the LIST command.
+ responses = connection.list(root, pattern);
+ }
+ } finally {
+ releaseConnection(connection);
+ }
+
+ List folders = new ArrayList();
+
+ for (int i = 0; i < responses.size(); i++) {
+ IMAPListResponse response = (IMAPListResponse)responses.get(i);
+ // if a full wildcard is specified, the root folder can be returned too. Make sure we
+ // filter that one out.
+ if (!response.mailboxName.equals(root)) {
+ IMAPFolder folder = new IMAPFolder((IMAPStore)store, response.mailboxName, response.separator);
+ folders.add(folder);
+ }
+ }
+
+ // convert into an array and return
+ return (Folder[])folders.toArray(new Folder[folders.size()]);
+ }
+
+
+ /**
+ * Test if a folder can hold sub folders.
+ *
+ * @return True if the folder is allowed to have subfolders.
+ */
+ protected synchronized boolean holdsFolders() throws MessagingException {
+ checkFolderValidity();
+ return (folderType & HOLDS_FOLDERS) != 0;
+ }
+
+
+ /**
+ * Validate that a target message number is considered valid
+ * by the IMAP server. If outside of the range we currently
+ * are a ware of, we'll ping the IMAP server to see if there
+ * have been any updates.
+ *
+ * @param messageNumber
+ * The message number we're checking.
+ *
+ * @exception MessagingException
+ */
+ protected void checkMessageValidity(int messageNumber) throws MessagingException {
+ // lower range for a message is 1.
+ if (messageNumber < 1) {
+ throw new MessagingException("Invalid message number for IMAP folder: " + messageNumber);
+ }
+ // if within our current known range, we'll accept this
+ if (messageNumber <= maxSequenceNumber) {
+ return;
+ }
+
+ IMAPConnection connection = getConnection();
+
+ synchronized (this) {
+ try {
+ // ping the server to see if there's any updates to process. The updates are handled
+ // by the response handlers.
+ connection.updateMailboxStatus();
+ } finally {
+ releaseConnection(connection);
+ }
+ }
+
+ // still out of range?
+ if (messageNumber > maxSequenceNumber) {
+ throw new MessagingException("Message " + messageNumber + " does not exist on server");
+ }
+ }
+
+
+ /**
+ * Below is a list of convenience methods that avoid repeated checking for a
+ * value and throwing an exception
+ */
+
+ /**
+ * Ensure the folder is open. Throws a MessagingException
+ * if not in the correct state for the operation.
+ *
+ * @exception IllegalStateException
+ */
+ protected void checkOpen() throws IllegalStateException {
+ if (!folderOpen){
+ throw new IllegalStateException("Folder is not Open");
+ }
+ }
+
+ /**
+ * Ensure the folder is not open for operations
+ * that require the folder to be closed.
+ *
+ * @exception IllegalStateException
+ */
+ protected void checkClosed() throws IllegalStateException {
+ if (folderOpen){
+ throw new IllegalStateException("Folder is Open");
+ }
+ }
+
+ /**
+ * Ensure that the folder is open for read/write mode before doing
+ * an operation that would make a change.
+ *
+ * @exception IllegalStateException
+ */
+ protected void checkReadWrite() throws IllegalStateException {
+ if (mode != READ_WRITE) {
+ throw new IllegalStateException("Folder is opened READY_ONLY");
+ }
+ }
+
+
+ /**
+ * Check that the folder is open and in read/write mode.
+ *
+ * @exception IllegalStateException
+ */
+ protected void checkOpenReadWrite() throws IllegalStateException {
+ checkOpen();
+ checkReadWrite();
+ }
+
+
+
+ /**
+ * Notify the message changed listeners that a
+ * message contained in the folder has been updated.
+ *
+ * @param type The type of update made to the message.
+ * @param m The message that was updated.
+ *
+ * @see javax.mail.Folder#notifyMessageChangedListeners(int, javax.mail.Message)
+ */
+ public void notifyMessageChangedListeners(int type, Message m) {
+ super.notifyMessageChangedListeners(type, m);
+ }
+
+
+ /**
+ * Retrieve the connection attached to this folder. Throws an
+ * exception if we don't have an active connection.
+ *
+ * @return The current connection object.
+ * @exception MessagingException
+ */
+ protected synchronized IMAPConnection getConnection() throws MessagingException {
+ // don't have an open connection yet? Just request a pool connection.
+ if (currentConnection == null) {
+ // request a connection from the central store.
+ IMAPConnection connection = ((IMAPStore)store).getFolderConnection(this);
+ // we need to make ourselves a handler of unsolicited responses
+ connection.addResponseHandler(this);
+ return connection;
+ }
+ // we have a connection for our use. Just return it.
+ return currentConnection;
+ }
+
+
+ /**
+ * Release our connection back to the Store.
+ *
+ * @param connection The connection to release.
+ *
+ * @exception MessagingException
+ */
+ protected void releaseConnection(IMAPConnection connection) throws MessagingException {
+ // This is a bit of a pain. We need to delay processing of the
+ // unsolicited responses until after each user of the connection has
+ // finished processing the expected responses. We need to do this because
+ // the unsolicited responses may include EXPUNGED messages. The EXPUNGED
+ // messages will alter the message sequence numbers for the messages in the
+ // cache. Processing the EXPUNGED messages too early will result in
+ // updates getting applied to the wrong message instances. So, as a result,
+ // we delay that stage of the processing until all expected responses have
+ // been handled.
+
+ // process any pending messages before returning.
+ connection.processPendingResponses();
+ // if no cached connection or this is somehow different from the cached one, just
+ // return it.
+ if (currentConnection == null || connection != currentConnection) {
+ connection.removeResponseHandler(this);
+ ((IMAPStore)store).releaseFolderConnection(this, connection);
+ }
+ // if we're open, then we don't have to worry about returning this connection
+ // to the Store. This is set up perfectly for our use right now.
+ }
+
+
+ /**
+ * Obtain a connection object for a Message attached to this Folder. This
+ * will be the Folder's connection, which is only available if the Folder
+ * is currently open.
+ *
+ * @return The connection object for the Message instance to use.
+ * @exception MessagingException
+ */
+ synchronized IMAPConnection getMessageConnection() throws MessagingException {
+ // if we're not open, the messages can't communicate either
+ if (currentConnection == null) {
+ throw new FolderClosedException(this, "No Folder connections available");
+ }
+ // return the current Folder connection. At this point, we'll be sharing the
+ // connection between the Folder and the Message (and potentially, other messages). The
+ // command operations on the connection are synchronized so only a single command can be
+ // issued at one time.
+ return currentConnection;
+ }
+
+
+ /**
+ * Release the connection object back to the Folder instance.
+ *
+ * @param connection The connection being released.
+ *
+ * @exception MessagingException
+ */
+ void releaseMessageConnection(IMAPConnection connection) throws MessagingException {
+ // release it back to ourselves...this will drive unsolicited message processing.
+ releaseConnection(connection);
+ }
+
+
+ /**
+ * Refresh the status information on this folder.
+ *
+ * @param force Force a status refresh always.
+ *
+ * @exception MessagingException
+ */
+ protected void refreshStatus(boolean force) throws MessagingException {
+ // first check that any cached status we've received has gotten a little moldy.
+ if (cachedStatus != null) {
+ // if not forcing, check the time out.
+ if (!force) {
+ if (statusCacheTimeout > 0) {
+ long age = System.currentTimeMillis() - lastStatusTimeStamp;
+ if (age < statusCacheTimeout) {
+ return;
+ }
+ }
+ }
+ // make sure the stale information is cleared out.
+ cachedStatus = null;
+ }
+
+ IMAPConnection connection = getConnection();
+ try {
+ // ping the server for the list information for this folder
+ cachedStatus = connection.getMailboxStatus(fullname);
+ // mark when we got this
+ lastStatusTimeStamp = System.currentTimeMillis();
+ } finally {
+ releaseConnection(connection);
+ }
+
+ // refresh the internal state from the message information
+ maxSequenceNumber = cachedStatus.messages;
+ recentMessages = cachedStatus.recentMessages;
+ unseenMessages = cachedStatus.unseenMessages;
+ uidValidity = cachedStatus.uidValidity;
+ }
+
+
+ /**
+ * Process an EXPUNGE response for a message, removing the
+ * message from the message cache.
+ *
+ * @param sequenceNumber
+ * The sequence number for the expunged message.
+ *
+ * @return The Message object corresponding to this expunged
+ * message.
+ * @exception MessagingException
+ */
+ protected synchronized Message expungeMessage(int sequenceNumber) throws MessagingException {
+ // we need to remove this from our message list. Deletion of a message by
+ // sequence number shifts the sequence numbers of every message after
+ // the target. So we, need to remove the message, update its status, then
+ // update the sequence numbers of every message after that. This is a serious
+ // pain!
+
+ Message expungedMessage = (Message)messages.remove(sequenceNumber - 1);
+ // mark the message as expunged.
+ ((IMAPMessage)expungedMessage).setExpunged(true);
+
+ // update the message sequence numbers for every message after the
+ // expunged one. NB. The sequence number is the cache index + 1
+ for (int i = sequenceNumber - 1; i < messages.size(); i++) {
+ IMAPMessage message = (IMAPMessage)messages.get(i);
+ message.setSequenceNumber(i + 1);
+ }
+
+ // have we retrieved a UID for this message? If we have, then it's in the UID cache and
+ // needs removal from there also
+ long uid = ((IMAPMessage)expungedMessage).getUID();
+ if (uid >= 0) {
+ uidCache.remove(new Long(uid));
+ }
+
+ // adjust the message count downward
+ maxSequenceNumber--;
+ return expungedMessage;
+ }
+
+
+ /**
+ * Resolve an array of message numbers into an array of the
+ * referenced messages.
+ *
+ * @param messageNumbers
+ * The array of message numbers (can be null).
+ *
+ * @return An array of Message[] containing the resolved messages from
+ * the list. Returns a zero-length array if there are no
+ * messages to resolve.
+ * @exception MessagingException
+ */
+ protected Message[] resolveMessages(int[] messageNumbers) throws MessagingException {
+ // the connection search returns a null pointer if nothing was found, just convert this into a
+ // null array.
+ if (messageNumbers == null) {
+ return new Message[0];
+ }
+
+ Message[] messages = new Message[messageNumbers.length];
+
+ // retrieve each of the message numbers in turn.
+ for (int i = 0; i < messageNumbers.length; i++) {
+ messages[i] = getMessage(messageNumbers[i]);
+ }
+
+ return messages;
+ }
+
+ /**
+ * Generate a message set string from a List of messages rather than an
+ * array.
+ *
+ * @param messages The List of messages.
+ *
+ * @return The evaluated message set string.
+ * @exception MessagingException
+ */
+ protected String generateMessageSet(List messages) throws MessagingException {
+ Message[] msgs = (Message[])messages.toArray(new Message[messages.size()]);
+ return generateMessageSet(msgs);
+ }
+
+
+ /**
+ * Take an array of messages and generate a String <message set>
+ * argument as specified by RFC 2060. The message set argument
+ * is a comma-separated list of message number ranges. A
+ * single element range is just one number. A longer range is
+ * a pair of numbers separated by a ":". The generated string
+ * should not have any blanks. This will attempt to locate
+ * consequetive ranges of message numbers, but will only do this
+ * for messages that are already ordered in the array (i.e., we
+ * don't try to sort). Expunged messages are excluded from the
+ * search, since they don't exist anymore. A valid search string
+ * will look something like this:
+ *
+ * "3,6:10,15,21:35"
+ *
+ * @param messages The array of messages we generate from.
+ *
+ * @return A string formatted version of these message identifiers that
+ * can be used on an IMAP command.
+ */
+ protected String generateMessageSet(Message[] messages) throws MessagingException {
+ StringBuffer set = new StringBuffer();
+
+ for (int i = 0; i < messages.length; i++) {
+ // first scan the list looking for a "live" message.
+ IMAPMessage start = (IMAPMessage)messages[i];
+ if (!start.isExpunged()) {
+
+ // we can go ahead and add this to the list now. If we find this is the start of a
+ // range, we'll tack on the ":end" bit once we find the last message in the range.
+ if (set.length() != 0) {
+ // only append the comma if not the first element of the list
+ set.append(',');
+ }
+
+ // append the first number. NOTE: We append this directly rather than
+ // use appendInteger(), which appends it using atom rules.
+ set.append(Integer.toString(start.getSequenceNumber()));
+
+ // ok, we have a live one. Now scan the list from here looking for the end of
+ // a range of consequetive messages.
+ int endIndex = -1; ;
+ // get the number we're checking against.
+ int previousSequence = start.getSequenceNumber();
+ for (int j = i + 1; j < messages.length; j++) {
+ IMAPMessage message = (IMAPMessage)messages[j];
+ if (!message.isExpunged()) {
+ // still consequetive?
+ if (message.getSequenceNumber() == previousSequence + 1) {
+ // step this for the next check.
+ previousSequence++;
+ // record this as the current end of the range.
+ endIndex = j;
+ }
+ else {
+ // found a non-consequetive one, stop here
+ break;
+ }
+ }
+ }
+
+ // have a range end point? Add the range specifier and step the loop index point
+ // to skip over this
+ if (endIndex != -1) {
+ // pick up the scan at the next location
+ i = endIndex;
+
+ set.append(':');
+ set.append(Integer.toString(((IMAPMessage)messages[endIndex]).getSequenceNumber()));
+ }
+ }
+ }
+
+ // return null for an empty list. This is possible because either an empty array has been handed to
+ // us or all of the messages in the array have been expunged.
+ if (set.length() == 0) {
+ return null;
+ }
+ return set.toString();
+ }
+
+ /**
+ * Verify that this folder exists on the server before
+ * performning an operation that requires a valid
+ * Folder instance.
+ *
+ * @exception MessagingException
+ */
+ protected void checkFolderValidity() throws MessagingException {
+ // if we are holding a current listinfo response, then
+ // we have chached existance information. In that case,
+ // all of our status is presumed up-to-date and we can go
+ // with that. If we don't have the information, then we
+ // ping the server for it.
+ if (listInfo == null && !exists()) {
+ throw new FolderNotFoundException(this, "Folder " + fullname + " not found on server");
+ }
+ }
+
+
+ /**
+ * Check if a Message is properly within the target
+ * folder.
+ *
+ * @param msg The message we're checking.
+ *
+ * @exception MessagingException
+ */
+ protected void checkMessageFolder(Message msg) throws MessagingException {
+ if (msg.getFolder() != this) {
+ throw new NoSuchElementException("Message is not within the target Folder");
+ }
+ }
+
+
+ /**
+ * Search a list of LIST responses for one containing information
+ * for a particular mailbox name.
+ *
+ * @param responses The list of responses.
+ * @param name The desired mailbox name.
+ *
+ * @return The IMAPListResponse information for the requested name.
+ */
+ protected IMAPListResponse findListResponse(List responses, String name) {
+ for (int i = 0; i < responses.size(); i++) {
+ IMAPListResponse response = (IMAPListResponse)responses.get(i);
+ if (response.mailboxName.equals(name)) {
+ return response;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Protected class intended for subclass overrides. For normal folders,
+ * the mailbox name is fullname. For Namespace root folders, the mailbox
+ * name is the prefix + separator.
+ *
+ * @return The string name to use as the mailbox name for exists() and issubscribed()
+ * calls.
+ */
+ protected String getMailBoxName() {
+ return fullname;
+ }
+
+ /**
+ * Handle an unsolicited response from the server. Most unsolicited responses
+ * are replies to specific commands sent to the server. The remainder must
+ * be handled by the Store or the Folder using the connection. These are
+ * critical to handle, as events such as expunged messages will alter the
+ * sequence numbers of the live messages. We need to keep things in sync.
+ *
+ * @param response The UntaggedResponse to process.
+ *
+ * @return true if we handled this response and no further handling is required. false
+ * means this one wasn't one of ours.
+ */
+ public boolean handleResponse(IMAPUntaggedResponse response) {
+ // "you've got mail". The message count has been updated. There
+ // are two posibilities. Either there really are new messages, or
+ // this is an update following an expunge. If there are new messages,
+ // we need to update the message cache and broadcast the change to
+ // any listeners.
+ if (response.isKeyword("EXISTS")) {
+ // we need to update our cache, and also retrieve the new messages and
+ // send them out in a broadcast update.
+ int oldCount = maxSequenceNumber;
+ maxSequenceNumber = ((IMAPSizeResponse)response).getSize();
+ populateMessageCache();
+ // has the size grown? We have to send the "you've got mail" announcement.
+ if (oldCount < maxSequenceNumber) {
+ try {
+ Message[] messages = getMessages(oldCount + 1, maxSequenceNumber);
+ notifyMessageAddedListeners(messages);
+ } catch (MessagingException e) {
+ // should never happen in this context
+ }
+ }
+ return true;
+ }
+ // "you had mail". A message was expunged from the server. This MUST
+ // be processed immediately, as any subsequent expunge messages will
+ // shift the message numbers as a result of previous messages getting
+ // removed. We need to keep our internal cache in sync with the server.
+ else if (response.isKeyword("EXPUNGE")) {
+ int messageNumber = ((IMAPSizeResponse)response).getSize();
+ try {
+ Message message = expungeMessage(messageNumber);
+
+ // broadcast the message update.
+ notifyMessageRemovedListeners(false, new Message[] {message});
+ } catch (MessagingException e) {
+ }
+ // we handled this one.
+ return true;
+ }
+ // just an update of recently arrived stuff? Just update the field.
+ else if (response.isKeyword("RECENT")) {
+ recentMessages = ((IMAPSizeResponse)response).getSize();
+ return true;
+ }
+ // The spec is not particularly clear what types of unsolicited
+ // FETCH response can be sent. The only one that is specifically
+ // spelled out is flag updates. If this is one of those, then
+ // handle it.
+ else if (response.isKeyword("FETCH")) {
+ IMAPFetchResponse fetch = (IMAPFetchResponse)response;
[... 215 lines stripped ...]