blob: ab6515632f537c69e2bedf3b061741e4bf8781a6 [file] [log] [blame]
/**
* 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 Map messageCache;
// 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. These are empty initially
messageCache = new HashMap();
uidCache = new HashMap();
// 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);
}
}
}
/**
* 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;
messageCache = 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);
// create the mapping key for this
Integer messageKey = new Integer(msgNum);
// ok, if the message number is within range, we should have this in the
// messages list. Just return the element.
Message message = (Message)messageCache.get(messageKey);
// if not in the cache, create a dummy add it in. The message body will be
// retrieved on demand
if (message == null) {
message = new IMAPMessage(this, ((IMAPStore)store), nextMessageID++, msgNum);
messageCache.put(messageKey, message);
}
return message;
}
/**
* 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(messageCache) {
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 (messageCache) {
// 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 (messageCache) {
((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 {
// first process the expunged message. We need to return a Message instance, so
// force this to be added to the cache
IMAPMessage expungedMessage = (IMAPMessage)getMessage(sequenceNumber);
// mark the message as expunged.
expungedMessage.setExpunged(true);
// 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));
}
// because we need to jigger the keys of some of these, we had better have a working
// copy.
Map newCache = new HashMap();
// now process each message in the cache, making adjustments as necessary
Iterator i = messageCache.keySet().iterator();
while (i.hasNext()) {
Integer key = (Integer)i.next();
int index = key.intValue();
// if before the expunged message, just copy over to the
// new cache
if (index < sequenceNumber) {
newCache.put(key, messageCache.get(key));
}
// after the expunged message...we need to adjust this
else if (index > sequenceNumber) {
// retrieve the message using the current position,
// adjust the message sequence number, and add to the new
// message cache under the new key value
IMAPMessage message = (IMAPMessage)messageCache.get(key);
message.setSequenceNumber(index - 1);
newCache.put(new Integer(index - 1), message);
}
else {
// the expunged message. We don't move this over to the new
// cache, and we've already done all processing of that message that's
// required
}
}
// replace the old cache now that everything has been adjusted
messageCache = newCache;
// 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();
// 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;
IMAPFlags flags = (IMAPFlags)fetch.getDataItem(IMAPFetchDataItem.FLAGS);
// if this is a flags response, get the message and update
if (flags != null) {
try {
// get the updated message and update the internal state.
IMAPMessage message = (IMAPMessage)getMessage(fetch.sequenceNumber);
// this shouldn't happen, but it might have been expunged too.
if (message != null) {
message.updateMessageInformation(fetch);
}
notifyMessageChangedListeners(MessageChangedEvent.FLAGS_CHANGED, message);
} catch (MessagingException e) {
}
return true;
}
}
// this is a BYE response on our connection. This forces us to close, but
// when we return the connection, the pool needs to get rid of it.
else if (response.isKeyword("BYE")) {
// this is essentially a close event. We need to clean everything up
// and make sure our connection is not returned to the general pool.
try {
cleanupFolder(false, true);
} catch (MessagingException e) {
}
return true;
}
// not a response the folder knows how to deal with.
return false;
}
// The following set of methods are extensions that exist in the Sun implementation. They
// match the Sun version in intent, but are not 100% compatible because the Sun implementation
// uses com.sun.* class instances as opposed to the org.apache.geronimo.* classes.
/**
* Remove an entry from the access control list for this folder.
*
* @param acl The ACL element to remove.
*
* @exception MessagingException
*/
public synchronized void removeACL(ACL acl) throws MessagingException {
// ask the store to kindly hook us up with a connection.
IMAPConnection connection = getConnection();
try {
// the connection does the heavy lifting
connection.removeACLRights(fullname, acl);
} finally {
releaseConnection(connection);
}
}
/**
* Add an entry to the access control list for this folder.
*
* @param acl The new ACL to add.
*/
public synchronized void addACL(ACL acl) throws MessagingException {
// ask the store to kindly hook us up with a connection.
IMAPConnection connection = getConnection();
try {
// the connection does the heavy lifting
connection.setACLRights(fullname, acl);
} finally {
releaseConnection(connection);
}
}
/**
* Add Rights to a given ACL entry.
*
* @param acl The target ACL to update.
*
* @exception MessagingException
*/
public synchronized void addRights(ACL acl) throws MessagingException {
// ask the store to kindly hook us up with a connection.
IMAPConnection connection = getConnection();
try {
// the connection does the heavy lifting
connection.addACLRights(fullname, acl);
} finally {
releaseConnection(connection);
}
}
/**
* Remove ACL Rights from a folder.
*
* @param acl The ACL describing the Rights to remove.
*
* @exception MessagingException
*/
public synchronized void removeRights(ACL acl) throws MessagingException {
// ask the store to kindly hook us up with a connection.
IMAPConnection connection = getConnection();
try {
// the connection does the heavy lifting
connection.removeACLRights(fullname, acl);
} finally {
releaseConnection(connection);
}
}
/**
* List the rights associated with a given name.
*
* @param name The user name for the Rights.
*
* @return The set of Rights associated with the user name.
* @exception MessagingException
*/
public synchronized Rights[] listRights(String name) throws MessagingException {
// ask the store to kindly hook us up with a connection.
IMAPConnection connection = getConnection();
try {
// the connection does the heavy lifting
return connection.listACLRights(fullname, name);
} finally {
releaseConnection(connection);
}
}
/**
* List the rights for the currently authenticated user.
*
* @return The set of Rights for the current user.
* @exception MessagingException
*/
public synchronized Rights myRights() throws MessagingException {
// ask the store to kindly hook us up with a connection.
IMAPConnection connection = getConnection();
try {
// the connection does the heavy lifting
return connection.getMyRights(fullname);
} finally {
releaseConnection(connection);
}
}
/**
* Get the quota values assigned to the current folder.
*
* @return The Quota information for the folder.
* @exception MessagingException
*/
public synchronized Quota[] getQuota() throws MessagingException {
// ask the store to kindly hook us up with a connection.
IMAPConnection connection = getConnection();
try {
// the connection does the heavy lifting
return connection.fetchQuotaRoot(fullname);
} finally {
releaseConnection(connection);
}
}
/**
* Set the quota value for a quota root
*
* @param quota The new quota information to set.
*
* @exception MessagingException
*/
public synchronized void setQuota(Quota quota) throws MessagingException {
// ask the store to kindly hook us up with a connection.
IMAPConnection connection = getConnection();
try {
// the connection does the heavy lifting
connection.setQuota(quota);
} finally {
releaseConnection(connection);
}
}
/**
* Get the set of attributes defined for the folder
* as the set of capabilities returned when the folder
* was opened.
*
* @return The set of attributes associated with the folder.
* @exception MessagingException
*/
public synchronized String[] getAttributes() throws MessagingException {
// if we don't have the LIST command information for this folder yet,
// call exists() to force this to be updated so we can return.
if (listInfo == null) {
// return a null reference if this is not valid.
if (!exists()) {
return null;
}
}
// return a copy of the attributes array.
return (String[])listInfo.attributes.clone();
}
}