com.sun.mail.imap.IMAPFolder Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright (c) 1997-2018 Oracle and/or its affiliates. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common Development
* and Distribution License("CDDL") (collectively, the "License"). You
* may not use this file except in compliance with the License. You can
* obtain a copy of the License at
* https://oss.oracle.com/licenses/CDDL+GPL-1.1
* or LICENSE.txt. See the License for the specific
* language governing permissions and limitations under the License.
*
* When distributing the software, include this License Header Notice in each
* file and include the License file at LICENSE.txt.
*
* GPL Classpath Exception:
* Oracle designates this particular file as subject to the "Classpath"
* exception as provided by Oracle in the GPL Version 2 section of the License
* file that accompanied this code.
*
* Modifications:
* If applicable, add the following below the License Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyright [year] [name of copyright owner]"
*
* Contributor(s):
* If you wish your version of this file to be governed by only the CDDL or
* only the GPL Version 2, indicate your decision by adding "[Contributor]
* elects to include this software in this distribution under the [CDDL or GPL
* Version 2] license." If you don't indicate a single choice of license, a
* recipient has the option to distribute your version of this file under
* either the CDDL, the GPL Version 2 or to extend the choice of license to
* its licensees as provided above. However, if you add GPL Version 2 code
* and therefore, elected the GPL Version 2 license, then the option applies
* only if the new code is made subject to such option by the copyright
* holder.
*/
package com.sun.mail.imap;
import java.util.Date;
import java.util.Vector;
import java.util.Hashtable;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Locale;
import java.util.logging.Level;
import java.io.*;
import java.net.SocketTimeoutException;
import java.nio.channels.SocketChannel;
import javax.mail.*;
import javax.mail.event.*;
import javax.mail.internet.*;
import javax.mail.search.*;
import com.sun.mail.util.PropUtil;
import com.sun.mail.util.MailLogger;
import com.sun.mail.util.CRLFOutputStream;
import com.sun.mail.iap.*;
import com.sun.mail.imap.protocol.*;
/**
* This class implements an IMAP folder.
*
* A closed IMAPFolder object shares a protocol connection with its IMAPStore
* object. When the folder is opened, it gets its own protocol connection.
*
* Applications that need to make use of IMAP-specific features may cast
* a Folder
object to an IMAPFolder
object and
* use the methods on this class.
*
* The {@link #getQuota getQuota} and
* {@link #setQuota setQuota} methods support the IMAP QUOTA extension.
* Refer to RFC 2087
* for more information.
*
* The {@link #getACL getACL}, {@link #addACL addACL},
* {@link #removeACL removeACL}, {@link #addRights addRights},
* {@link #removeRights removeRights}, {@link #listRights listRights}, and
* {@link #myRights myRights} methods support the IMAP ACL extension.
* Refer to RFC 2086
* for more information.
*
* The {@link #getSortedMessages getSortedMessages}
* methods support the IMAP SORT extension.
* Refer to RFC 5256
* for more information.
*
* The {@link #open(int,com.sun.mail.imap.ResyncData) open(int,ResyncData)}
* method and {@link com.sun.mail.imap.ResyncData ResyncData} class supports
* the IMAP CONDSTORE and QRESYNC extensions.
* Refer to RFC 4551
* and RFC 5162
* for more information.
*
* The {@link #doCommand doCommand} method and
* {@link IMAPFolder.ProtocolCommand IMAPFolder.ProtocolCommand}
* interface support use of arbitrary IMAP protocol commands.
*
* See the com.sun.mail.imap package
* documentation for further information on the IMAP protocol provider.
*
* WARNING: The APIs unique to this class should be
* considered EXPERIMENTAL. They may be changed in the
* future in ways that are incompatible with applications using the
* current APIs.
*
* @author John Mani
* @author Bill Shannon
* @author Jim Glennon
*/
/*
* The folder object itself serves as a lock for the folder's state
* EXCEPT for the message cache (see below), typically by using
* synchronized methods. When checking that a folder is open or
* closed, the folder's lock must be held. It's important that the
* folder's lock is acquired before the messageCacheLock (see below).
* Thus, the locking hierarchy is that the folder lock, while optional,
* must be acquired before the messageCacheLock, if it's acquired at
* all. Be especially careful of callbacks that occur while holding
* the messageCacheLock into (e.g.) superclass Folder methods that are
* synchronized. Note that methods in IMAPMessage will acquire the
* messageCacheLock without acquiring the folder lock.
*
* When a folder is opened, it creates a messageCache (a Vector) of
* empty IMAPMessage objects. Each Message has a messageNumber - which
* is its index into the messageCache, and a sequenceNumber - which is
* its IMAP sequence-number. All operations on a Message which involve
* communication with the server, use the message's sequenceNumber.
*
* The most important thing to note here is that the server can send
* unsolicited EXPUNGE notifications as part of the responses for "most"
* commands. Refer RFC 3501, sections 5.3 & 5.5 for gory details. Also,
* the server sends these notifications AFTER the message has been
* expunged. And once a message is expunged, the sequence-numbers of
* those messages after the expunged one are renumbered. This essentially
* means that the mapping between *any* Message and its sequence-number
* can change in the period when a IMAP command is issued and its responses
* are processed. Hence we impose a strict locking model as follows:
*
* We define one mutex per folder - this is just a Java Object (named
* messageCacheLock). Any time a command is to be issued to the IMAP
* server (i.e., anytime the corresponding IMAPProtocol method is
* invoked), follow the below style:
*
* synchronized (messageCacheLock) { // ACQUIRE LOCK
* issue command ()
*
* // The response processing is typically done within
* // the handleResponse() callback. A few commands (Fetch,
* // Expunge) return *all* responses and hence their
* // processing is done here itself. Now, as part of the
* // processing unsolicited EXPUNGE responses, we renumber
* // the necessary sequence-numbers. Thus the renumbering
* // happens within this critical-region, surrounded by
* // locks.
* process responses ()
* } // RELEASE LOCK
*
* This technique is used both by methods in IMAPFolder and by methods
* in IMAPMessage and other classes that operate on data in the folder.
* Note that holding the messageCacheLock has the side effect of
* preventing the folder from being closed, and thus ensuring that the
* folder's protocol object is still valid. The protocol object should
* only be accessed while holding the messageCacheLock (except for calls
* to IMAPProtocol.isREV1(), which don't need to be protected because it
* doesn't access the server).
*
* Note that interactions with the Store's protocol connection do
* not have to be protected as above, since the Store's protocol is
* never in a "meaningful" SELECT-ed state.
*/
public class IMAPFolder extends Folder implements UIDFolder, ResponseHandler {
protected volatile String fullName; // full name
protected String name; // name
protected int type; // folder type.
protected char separator; // separator
protected Flags availableFlags; // available flags
protected Flags permanentFlags; // permanent flags
protected volatile boolean exists; // whether this folder really exists ?
protected boolean isNamespace = false; // folder is a namespace name
protected volatile String[] attributes;// name attributes from LIST response
protected volatile IMAPProtocol protocol; // this folder's protocol object
protected MessageCache messageCache;// message cache
// accessor lock for message cache
protected final Object messageCacheLock = new Object();
protected Hashtable uidTable; // UID->Message hashtable
/* An IMAP delimiter is a 7bit US-ASCII character. (except NUL).
* We use '\uffff' (a non 7bit character) to indicate that we havent
* yet determined what the separator character is.
* We use '\u0000' (NUL) to indicate that no separator character
* exists, i.e., a flat hierarchy
*/
static final protected char UNKNOWN_SEPARATOR = '\uffff';
private volatile boolean opened = false; // is this folder opened ?
/* This field tracks the state of this folder. If the folder is closed
* due to external causes (i.e, not thru the close() method), then
* this field will remain false. If the folder is closed thru the
* close() method, then this field is set to true.
*
* If reallyClosed is false, then a FolderClosedException is
* generated when a method is invoked on any Messaging object
* owned by this folder. If reallyClosed is true, then the
* IllegalStateException runtime exception is thrown.
*/
private boolean reallyClosed = true;
/*
* The idleState field supports the IDLE command.
* Normally when executing an IMAP command we hold the
* messageCacheLock and often the folder lock (see above).
* While executing the IDLE command we can't hold either
* of these locks or it would prevent other threads from
* entering Folder methods even far enough to check whether
* an IDLE command is in progress. We need to check before
* issuing another command so that we can abort the IDLE
* command.
*
* The idleState field is protected by the messageCacheLock.
* The RUNNING state is the normal state and means no IDLE
* command is in progress. The IDLE state means we've issued
* an IDLE command and are reading responses. The ABORTING
* state means we've sent the DONE continuation command and
* are waiting for the thread running the IDLE command to
* break out of its read loop.
*
* When an IDLE command is in progress, the thread calling
* the idle method will be reading from the IMAP connection
* while holding neither the folder lock nor the messageCacheLock.
* It's obviously critical that no other thread try to send a
* command or read from the connection while in this state.
* However, other threads can send the DONE continuation
* command that will cause the server to break out of the IDLE
* loop and send the ending tag response to the IDLE command.
* The thread in the idle method that's reading the responses
* from the IDLE command will see this ending response and
* complete the idle method, setting the idleState field back
* to RUNNING, and notifying any threads waiting to use the
* connection.
*
* All uses of the IMAP connection (IMAPProtocol object) must
* be done while holding the messageCacheLock and must be
* preceeded by a check to make sure an IDLE command is not
* running, and abort the IDLE command if necessary. While
* waiting for the IDLE command to complete, these other threads
* will give up the messageCacheLock, but might still be holding
* the folder lock. This check is done by the getProtocol()
* method, resulting in a typical usage pattern of:
*
* synchronized (messageCacheLock) {
* IMAPProtocol p = getProtocol(); // may block waiting for IDLE
* // ... use protocol
* }
*/
private static final int RUNNING = 0; // not doing IDLE command
private static final int IDLE = 1; // IDLE command in effect
private static final int ABORTING = 2; // IDLE command aborting
private int idleState = RUNNING;
private IdleManager idleManager;
private volatile int total = -1; // total number of messages in the
// message cache
private volatile int recent = -1; // number of recent messages
private int realTotal = -1; // total number of messages on
// the server
private long uidvalidity = -1; // UIDValidity
private long uidnext = -1; // UIDNext
private boolean uidNotSticky = false; // RFC 4315
private volatile long highestmodseq = -1; // RFC 4551 - CONDSTORE
private boolean doExpungeNotification = true; // used in expunge handler
private Status cachedStatus = null;
private long cachedStatusTime = 0;
private boolean hasMessageCountListener = false; // optimize notification
protected MailLogger logger;
private MailLogger connectionPoolLogger;
/**
* A fetch profile item for fetching headers.
* This inner class extends the FetchProfile.Item
* class to add new FetchProfile item types, specific to IMAPFolders.
*
* @see FetchProfile
*/
public static class FetchProfileItem extends FetchProfile.Item {
protected FetchProfileItem(String name) {
super(name);
}
/**
* HEADERS is a fetch profile item that can be included in a
* FetchProfile
during a fetch request to a Folder.
* This item indicates that the headers for messages in the specified
* range are desired to be prefetched.
*
* An example of how a client uses this is below:
*
*
* FetchProfile fp = new FetchProfile();
* fp.add(IMAPFolder.FetchProfileItem.HEADERS);
* folder.fetch(msgs, fp);
*
*
*/
public static final FetchProfileItem HEADERS =
new FetchProfileItem("HEADERS");
/**
* SIZE is a fetch profile item that can be included in a
* FetchProfile
during a fetch request to a Folder.
* This item indicates that the sizes of the messages in the specified
* range are desired to be prefetched.
*
* SIZE was moved to FetchProfile.Item in JavaMail 1.5.
*
* @deprecated
*/
@Deprecated
public static final FetchProfileItem SIZE =
new FetchProfileItem("SIZE");
/**
* MESSAGE is a fetch profile item that can be included in a
* FetchProfile
during a fetch request to a Folder.
* This item indicates that the entire messages (headers and body,
* including all "attachments") in the specified
* range are desired to be prefetched. Note that the entire message
* content is cached in memory while the Folder is open. The cached
* message will be parsed locally to return header information and
* message content.
*
* An example of how a client uses this is below:
*
*
* FetchProfile fp = new FetchProfile();
* fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
* folder.fetch(msgs, fp);
*
*
*
* @since JavaMail 1.5.2
*/
public static final FetchProfileItem MESSAGE =
new FetchProfileItem("MESSAGE");
/**
* INTERNALDATE is a fetch profile item that can be included in a
* FetchProfile
during a fetch request to a Folder.
* This item indicates that the IMAP INTERNALDATE values
* (received date) of the messages in the specified
* range are desired to be prefetched.
*
* An example of how a client uses this is below:
*
*
* FetchProfile fp = new FetchProfile();
* fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
* folder.fetch(msgs, fp);
*
*
*
* @since JavaMail 1.5.5
*/
public static final FetchProfileItem INTERNALDATE =
new FetchProfileItem("INTERNALDATE");
}
/**
* Constructor used to create a possibly non-existent folder.
*
* @param fullName fullname of this folder
* @param separator the default separator character for this
* folder's namespace
* @param store the Store
* @param isNamespace if this folder represents a namespace
*/
protected IMAPFolder(String fullName, char separator, IMAPStore store,
Boolean isNamespace) {
super(store);
if (fullName == null)
throw new NullPointerException("Folder name is null");
this.fullName = fullName;
this.separator = separator;
logger = new MailLogger(this.getClass(), "DEBUG IMAP",
store.getSession().getDebug(), store.getSession().getDebugOut());
connectionPoolLogger = store.getConnectionPoolLogger();
/*
* Work around apparent bug in Exchange. Exchange
* will return a name of "Public Folders/" from
* LIST "%".
*
* If name has one separator, and it's at the end,
* assume this is a namespace name and treat it
* accordingly. Usually this will happen as a result
* of the list method, but this also allows getFolder
* to work with namespace names.
*/
this.isNamespace = false;
if (separator != UNKNOWN_SEPARATOR && separator != '\0') {
int i = this.fullName.indexOf(separator);
if (i > 0 && i == this.fullName.length() - 1) {
this.fullName = this.fullName.substring(0, i);
this.isNamespace = true;
}
}
// if we were given a value, override default chosen above
if (isNamespace != null)
this.isNamespace = isNamespace.booleanValue();
}
/**
* Constructor used to create an existing folder.
*
* @param li the ListInfo for this folder
* @param store the store containing this folder
*/
protected IMAPFolder(ListInfo li, IMAPStore store) {
this(li.name, li.separator, store, null);
if (li.hasInferiors)
type |= HOLDS_FOLDERS;
if (li.canOpen)
type |= HOLDS_MESSAGES;
exists = true;
attributes = li.attrs;
}
/*
* Ensure that this folder exists. If 'exists' has been set to true,
* we don't attempt to validate it with the server again. Note that
* this can result in a possible loss of sync with the server.
* ASSERT: Must be called with this folder's synchronization lock held.
*/
protected void checkExists() throws MessagingException {
// If the boolean field 'exists' is false, check with the
// server by invoking exists() ..
if (!exists && !exists())
throw new FolderNotFoundException(
this, fullName + " not found");
}
/*
* Ensure the folder is closed.
* ASSERT: Must be called with this folder's synchronization lock held.
*/
protected void checkClosed() {
if (opened)
throw new IllegalStateException(
"This operation is not allowed on an open folder"
);
}
/*
* Ensure the folder is open.
* ASSERT: Must be called with this folder's synchronization lock held.
*/
protected void checkOpened() throws FolderClosedException {
assert Thread.holdsLock(this);
if (!opened) {
if (reallyClosed)
throw new IllegalStateException(
"This operation is not allowed on a closed folder"
);
else // Folder was closed "implicitly"
throw new FolderClosedException(this,
"Lost folder connection to server"
);
}
}
/*
* Check that the given message number is within the range
* of messages present in this folder. If the message
* number is out of range, we ping the server to obtain any
* pending new message notifications from the server.
*/
protected void checkRange(int msgno) throws MessagingException {
if (msgno < 1) // message-numbers start at 1
throw new IndexOutOfBoundsException("message number < 1");
if (msgno <= total)
return;
// Out of range, let's ping the server and see if
// the server has more messages for us.
synchronized(messageCacheLock) { // Acquire lock
try {
keepConnectionAlive(false);
} catch (ConnectionException cex) {
// Oops, lost connection
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
} // Release lock
if (msgno > total) // Still out of range ? Throw up ...
throw new IndexOutOfBoundsException(msgno + " > " + total);
}
/*
* Check whether the given flags are supported by this server,
* and also verify that the folder allows setting flags.
*/
private void checkFlags(Flags flags) throws MessagingException {
assert Thread.holdsLock(this);
if (mode != READ_WRITE)
throw new IllegalStateException(
"Cannot change flags on READ_ONLY folder: " + fullName
);
/*
if (!availableFlags.contains(flags))
throw new MessagingException(
"These flags are not supported by this implementation"
);
*/
}
/**
* Get the name of this folder.
*/
@Override
public synchronized String getName() {
/* Return the last component of this Folder's full name.
* Folder components are delimited by the separator character.
*/
if (name == null) {
try {
name = fullName.substring(
fullName.lastIndexOf(getSeparator()) + 1
);
} catch (MessagingException mex) { }
}
return name;
}
/**
* Get the fullname of this folder.
*/
@Override
public String getFullName() {
return fullName;
}
/**
* Get this folder's parent.
*/
@Override
public synchronized Folder getParent() throws MessagingException {
char c = getSeparator();
int index;
if ((index = fullName.lastIndexOf(c)) != -1)
return ((IMAPStore)store).newIMAPFolder(
fullName.substring(0, index), c);
else
return new DefaultFolder((IMAPStore)store);
}
/**
* Check whether this folder really exists on the server.
*/
@Override
public synchronized boolean exists() throws MessagingException {
// Check whether this folder exists ..
ListInfo[] li = null;
final String lname;
if (isNamespace && separator != '\0')
lname = fullName + separator;
else
lname = fullName;
li = (ListInfo[])doCommand(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p) throws ProtocolException {
return p.list("", lname);
}
});
if (li != null) {
int i = findName(li, lname);
fullName = li[i].name;
separator = li[i].separator;
int len = fullName.length();
if (separator != '\0' && len > 0 &&
fullName.charAt(len - 1) == separator) {
fullName = fullName.substring(0, len - 1);
}
type = 0;
if (li[i].hasInferiors)
type |= HOLDS_FOLDERS;
if (li[i].canOpen)
type |= HOLDS_MESSAGES;
exists = true;
attributes = li[i].attrs;
} else {
exists = opened;
attributes = null;
}
return exists;
}
/**
* Which entry in li
matches lname
?
* If the name contains wildcards, more than one entry may be
* returned.
*/
private int findName(ListInfo[] li, String lname) {
int i;
// if the name contains a wildcard, there might be more than one
for (i = 0; i < li.length; i++) {
if (li[i].name.equals(lname))
break;
}
if (i >= li.length) { // nothing matched exactly
// XXX - possibly should fail? But what if server
// is case insensitive and returns the preferred
// case of the name here?
i = 0; // use first one
}
return i;
}
/**
* List all subfolders matching the specified pattern.
*/
@Override
public Folder[] list(String pattern) throws MessagingException {
return doList(pattern, false);
}
/**
* List all subscribed subfolders matching the specified pattern.
*/
@Override
public Folder[] listSubscribed(String pattern) throws MessagingException {
return doList(pattern, true);
}
private synchronized Folder[] doList(final String pattern,
final boolean subscribed) throws MessagingException {
checkExists(); // insure that this folder does exist.
// Why waste a roundtrip to the server?
if (attributes != null && !isDirectory())
return new Folder[0];
final char c = getSeparator();
ListInfo[] li = (ListInfo[])doCommandIgnoreFailure(
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
if (subscribed)
return p.lsub("", fullName + c + pattern);
else
return p.list("", fullName + c + pattern);
}
});
if (li == null)
return new Folder[0];
/*
* The UW based IMAP4 servers (e.g. SIMS2.0) include
* current folder (terminated with the separator), when
* the LIST pattern is '%' or '*'. i.e,
* returns "mail/" as the first LIST response.
*
* Doesn't make sense to include the current folder in this
* case, so we filter it out. Note that I'm assuming that
* the offending response is the *first* one, my experiments
* with the UW & SIMS2.0 servers indicate that ..
*/
int start = 0;
// Check the first LIST response.
if (li.length > 0 && li[0].name.equals(fullName + c))
start = 1; // start from index = 1
IMAPFolder[] folders = new IMAPFolder[li.length - start];
IMAPStore st = (IMAPStore)store;
for (int i = start; i < li.length; i++)
folders[i-start] = st.newIMAPFolder(li[i]);
return folders;
}
/**
* Get the separator character.
*/
@Override
public synchronized char getSeparator() throws MessagingException {
if (separator == UNKNOWN_SEPARATOR) {
ListInfo[] li = null;
li = (ListInfo[])doCommand(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
// REV1 allows the following LIST format to obtain
// the hierarchy delimiter of non-existent folders
if (p.isREV1()) // IMAP4rev1
return p.list(fullName, "");
else // IMAP4, note that this folder must exist for this
// to work :(
return p.list("", fullName);
}
});
if (li != null)
separator = li[0].separator;
else
separator = '/'; // punt !
}
return separator;
}
/**
* Get the type of this folder.
*/
@Override
public synchronized int getType() throws MessagingException {
if (opened) {
// never throw FolderNotFoundException if folder is open
if (attributes == null)
exists(); // try to fetch attributes
} else {
checkExists();
}
return type;
}
/**
* Check whether this folder is subscribed.
*/
@Override
public synchronized boolean isSubscribed() {
ListInfo[] li = null;
final String lname;
if (isNamespace && separator != '\0')
lname = fullName + separator;
else
lname = fullName;
try {
li = (ListInfo[])doProtocolCommand(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
return p.lsub("", lname);
}
});
} catch (ProtocolException pex) {
}
if (li != null) {
int i = findName(li, lname);
return li[i].canOpen;
} else
return false;
}
/**
* Subscribe/Unsubscribe this folder.
*/
@Override
public synchronized void setSubscribed(final boolean subscribe)
throws MessagingException {
doCommandIgnoreFailure(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p) throws ProtocolException {
if (subscribe)
p.subscribe(fullName);
else
p.unsubscribe(fullName);
return null;
}
});
}
/**
* Create this folder, with the specified type.
*/
@Override
public synchronized boolean create(final int type)
throws MessagingException {
char c = 0;
if ((type & HOLDS_MESSAGES) == 0) // only holds folders
c = getSeparator();
final char sep = c;
Object ret = doCommandIgnoreFailure(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
if ((type & HOLDS_MESSAGES) == 0) // only holds folders
p.create(fullName + sep);
else {
p.create(fullName);
// Certain IMAP servers do not allow creation of folders
// that can contain messages *and* subfolders. So, if we
// were asked to create such a folder, we should verify
// that we could indeed do so.
if ((type & HOLDS_FOLDERS) != 0) {
// we want to hold subfolders and messages. Check
// whether we could create such a folder.
ListInfo[] li = p.list("", fullName);
if (li != null && !li[0].hasInferiors) {
// Hmm ..the new folder
// doesn't support Inferiors ? Fail
p.delete(fullName);
throw new ProtocolException("Unsupported type");
}
}
}
return Boolean.TRUE;
}
});
if (ret == null)
return false; // CREATE failure, maybe this
// folder already exists ?
// exists = true;
// this.type = type;
boolean retb = exists(); // set exists, type, and attributes
if (retb) // Notify listeners on self and our Store
notifyFolderListeners(FolderEvent.CREATED);
return retb;
}
/**
* Check whether this folder has new messages.
*/
@Override
public synchronized boolean hasNewMessages() throws MessagingException {
synchronized (messageCacheLock) {
if (opened) { // If we are open, we already have this information
// Folder is open, make sure information is up to date
// tickle the folder and store connections.
try {
keepConnectionAlive(true);
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
return recent > 0 ? true : false;
}
}
// First, the cheap way - use LIST and look for the \Marked
// or \Unmarked tag
ListInfo[] li = null;
final String lname;
if (isNamespace && separator != '\0')
lname = fullName + separator;
else
lname = fullName;
li = (ListInfo[])doCommandIgnoreFailure(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p) throws ProtocolException {
return p.list("", lname);
}
});
// if folder doesn't exist, throw exception
if (li == null)
throw new FolderNotFoundException(this, fullName + " not found");
int i = findName(li, lname);
if (li[i].changeState == ListInfo.CHANGED)
return true;
else if (li[i].changeState == ListInfo.UNCHANGED)
return false;
// LIST didn't work. Try the hard way, using STATUS
try {
Status status = getStatus();
if (status.recent > 0)
return true;
else
return false;
} catch (BadCommandException bex) {
// Probably doesn't support STATUS, tough luck.
return false;
} catch (ConnectionException cex) {
throw new StoreClosedException(store, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
/**
* Get the named subfolder.
*/
@Override
public synchronized Folder getFolder(String name)
throws MessagingException {
// If we know that this folder is *not* a directory, don't
// send the request to the server at all ...
if (attributes != null && !isDirectory())
throw new MessagingException("Cannot contain subfolders");
char c = getSeparator();
return ((IMAPStore)store).newIMAPFolder(fullName + c + name, c);
}
/**
* Delete this folder.
*/
@Override
public synchronized boolean delete(boolean recurse)
throws MessagingException {
checkClosed(); // insure that this folder is closed.
if (recurse) {
// Delete all subfolders.
Folder[] f = list();
for (int i = 0; i < f.length; i++)
f[i].delete(recurse); // ignore intermediate failures
}
// Attempt to delete this folder
Object ret = doCommandIgnoreFailure(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p) throws ProtocolException {
p.delete(fullName);
return Boolean.TRUE;
}
});
if (ret == null)
// Non-existent folder/No permission ??
return false;
// DELETE succeeded.
exists = false;
attributes = null;
// Notify listeners on self and our Store
notifyFolderListeners(FolderEvent.DELETED);
return true;
}
/**
* Rename this folder.
*/
@Override
public synchronized boolean renameTo(final Folder f)
throws MessagingException {
checkClosed(); // insure that we are closed.
checkExists();
if (f.getStore() != store)
throw new MessagingException("Can't rename across Stores");
Object ret = doCommandIgnoreFailure(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p) throws ProtocolException {
p.rename(fullName, f.getFullName());
return Boolean.TRUE;
}
});
if (ret == null)
return false;
exists = false;
attributes = null;
notifyFolderRenamedListeners(f);
return true;
}
/**
* Open this folder in the given mode.
*/
@Override
public synchronized void open(int mode) throws MessagingException {
open(mode, null);
}
/**
* Open this folder in the given mode, with the given
* resynchronization data.
*
* @param mode the open mode (Folder.READ_WRITE or Folder.READ_ONLY)
* @param rd the ResyncData instance
* @return a List of MailEvent instances, or null if none
* @exception MessagingException if the open fails
* @since JavaMail 1.5.1
*/
public synchronized List open(int mode, ResyncData rd)
throws MessagingException {
checkClosed(); // insure that we are not already open
MailboxInfo mi = null;
// Request store for our own protocol connection.
protocol = ((IMAPStore)store).getProtocol(this);
List openEvents = null;
synchronized(messageCacheLock) { // Acquire messageCacheLock
/*
* Add response handler right away so we get any alerts or
* notifications that occur during the SELECT or EXAMINE.
* Have to be sure to remove it if we fail to open the
* folder.
*/
protocol.addResponseHandler(this);
try {
/*
* Enable QRESYNC or CONDSTORE if needed and not enabled.
* QRESYNC implies CONDSTORE, but servers that support
* QRESYNC are not required to support just CONDSTORE
* per RFC 5162.
*/
if (rd != null) {
if (rd == ResyncData.CONDSTORE) {
if (!protocol.isEnabled("CONDSTORE") &&
!protocol.isEnabled("QRESYNC")) {
if (protocol.hasCapability("CONDSTORE"))
protocol.enable("CONDSTORE");
else
protocol.enable("QRESYNC");
}
} else {
if (!protocol.isEnabled("QRESYNC"))
protocol.enable("QRESYNC");
}
}
if (mode == READ_ONLY)
mi = protocol.examine(fullName, rd);
else
mi = protocol.select(fullName, rd);
} catch (CommandFailedException cex) {
/*
* Handle SELECT or EXAMINE failure.
* Try to figure out why the operation failed so we can
* report a more reasonable exception.
*
* Will use our existing protocol object.
*/
try {
checkExists(); // throw exception if folder doesn't exist
if ((type & HOLDS_MESSAGES) == 0)
throw new MessagingException(
"folder cannot contain messages");
throw new MessagingException(cex.getMessage(), cex);
} finally {
// folder not open, don't keep this information
exists = false;
attributes = null;
type = 0;
// connection still good, return it
releaseProtocol(true);
}
// NOTREACHED
} catch (ProtocolException pex) {
// got a BAD or a BYE; connection may be bad, close it
try {
throw logoutAndThrow(pex.getMessage(), pex);
} finally {
releaseProtocol(false);
}
}
if (mi.mode != mode) {
if (mode == READ_WRITE && mi.mode == READ_ONLY &&
((IMAPStore)store).allowReadOnlySelect()) {
; // all ok, allow it
} else { // otherwise, it's an error
ReadOnlyFolderException ife = new ReadOnlyFolderException(
this, "Cannot open in desired mode");
throw cleanupAndThrow(ife);
}
}
// Initialize stuff.
opened = true;
reallyClosed = false;
this.mode = mi.mode;
availableFlags = mi.availableFlags;
permanentFlags = mi.permanentFlags;
total = realTotal = mi.total;
recent = mi.recent;
uidvalidity = mi.uidvalidity;
uidnext = mi.uidnext;
uidNotSticky = mi.uidNotSticky;
highestmodseq = mi.highestmodseq;
// Create the message cache of appropriate size
messageCache = new MessageCache(this, (IMAPStore)store, total);
// process saved responses and return corresponding events
if (mi.responses != null) {
openEvents = new ArrayList<>();
for (IMAPResponse ir : mi.responses) {
if (ir.keyEquals("VANISHED")) {
// "VANISHED" SP ["(EARLIER)"] SP known-uids
String[] s = ir.readAtomStringList();
// check that it really is "EARLIER"
if (s == null || s.length != 1 ||
!s[0].equalsIgnoreCase("EARLIER"))
continue; // it's not, what to do with it here?
String uids = ir.readAtom();
UIDSet[] uidset = UIDSet.parseUIDSets(uids);
long[] luid = UIDSet.toArray(uidset, uidnext);
if (luid != null && luid.length > 0)
openEvents.add(
new MessageVanishedEvent(this, luid));
} else if (ir.keyEquals("FETCH")) {
assert ir instanceof FetchResponse :
"!ir instanceof FetchResponse";
Message msg = processFetchResponse((FetchResponse)ir);
if (msg != null)
openEvents.add(new MessageChangedEvent(this,
MessageChangedEvent.FLAGS_CHANGED, msg));
}
}
}
} // Release lock
exists = true; // if we opened it, it must exist
attributes = null; // but we don't yet know its attributes
type = HOLDS_MESSAGES; // lacking more info, we know at least this much
// notify listeners
notifyConnectionListeners(ConnectionEvent.OPENED);
return openEvents;
}
private MessagingException cleanupAndThrow(MessagingException ife) {
try {
try {
// close mailbox and return connection
protocol.close();
releaseProtocol(true);
} catch (ProtocolException pex) {
// something went wrong, close connection
try {
addSuppressed(ife, logoutAndThrow(pex.getMessage(), pex));
} finally {
releaseProtocol(false);
}
}
} catch (Throwable thr) {
addSuppressed(ife, thr);
}
return ife;
}
private MessagingException logoutAndThrow(String why, ProtocolException t) {
MessagingException ife = new MessagingException(why, t);
try {
protocol.logout();
} catch (Throwable thr) {
addSuppressed(ife, thr);
}
return ife;
}
private void addSuppressed(Throwable ife, Throwable thr) {
if (isRecoverable(thr)) {
ife.addSuppressed(thr);
} else {
thr.addSuppressed(ife);
if (thr instanceof Error) {
throw (Error) thr;
}
if (thr instanceof RuntimeException) {
throw (RuntimeException) thr;
}
throw new RuntimeException("unexpected exception", thr);
}
}
private boolean isRecoverable(Throwable t) {
return (t instanceof Exception) || (t instanceof LinkageError);
}
/**
* Prefetch attributes, based on the given FetchProfile.
*/
@Override
public synchronized void fetch(Message[] msgs, FetchProfile fp)
throws MessagingException {
// cache this information in case connection is closed and
// protocol is set to null
boolean isRev1;
FetchItem[] fitems;
synchronized (messageCacheLock) {
checkOpened();
isRev1 = protocol.isREV1();
fitems = protocol.getFetchItems();
}
StringBuilder command = new StringBuilder();
boolean first = true;
boolean allHeaders = false;
if (fp.contains(FetchProfile.Item.ENVELOPE)) {
command.append(getEnvelopeCommand());
first = false;
}
if (fp.contains(FetchProfile.Item.FLAGS)) {
command.append(first ? "FLAGS" : " FLAGS");
first = false;
}
if (fp.contains(FetchProfile.Item.CONTENT_INFO)) {
command.append(first ? "BODYSTRUCTURE" : " BODYSTRUCTURE");
first = false;
}
if (fp.contains(UIDFolder.FetchProfileItem.UID)) {
command.append(first ? "UID" : " UID");
first = false;
}
if (fp.contains(IMAPFolder.FetchProfileItem.HEADERS)) {
allHeaders = true;
if (isRev1)
command.append(first ?
"BODY.PEEK[HEADER]" : " BODY.PEEK[HEADER]");
else
command.append(first ? "RFC822.HEADER" : " RFC822.HEADER");
first = false;
}
if (fp.contains(IMAPFolder.FetchProfileItem.MESSAGE)) {
allHeaders = true;
if (isRev1)
command.append(first ? "BODY.PEEK[]" : " BODY.PEEK[]");
else
command.append(first ? "RFC822" : " RFC822");
first = false;
}
if (fp.contains(FetchProfile.Item.SIZE) ||
fp.contains(IMAPFolder.FetchProfileItem.SIZE)) {
command.append(first ? "RFC822.SIZE" : " RFC822.SIZE");
first = false;
}
if (fp.contains(IMAPFolder.FetchProfileItem.INTERNALDATE)) {
command.append(first ? "INTERNALDATE" : " INTERNALDATE");
first = false;
}
// if we're not fetching all headers, fetch individual headers
String[] hdrs = null;
if (!allHeaders) {
hdrs = fp.getHeaderNames();
if (hdrs.length > 0) {
if (!first)
command.append(" ");
command.append(createHeaderCommand(hdrs, isRev1));
}
}
/*
* Add any additional extension fetch items.
*/
for (int i = 0; i < fitems.length; i++) {
if (fp.contains(fitems[i].getFetchProfileItem())) {
if (command.length() != 0)
command.append(" ");
command.append(fitems[i].getName());
}
}
Utility.Condition condition =
new IMAPMessage.FetchProfileCondition(fp, fitems);
// Acquire the Folder's MessageCacheLock.
synchronized (messageCacheLock) {
// check again to make sure folder is still open
checkOpened();
// Apply the test, and get the sequence-number set for
// the messages that need to be prefetched.
MessageSet[] msgsets = Utility.toMessageSetSorted(msgs, condition);
if (msgsets == null)
// We already have what we need.
return;
Response[] r = null;
// to collect non-FETCH responses & unsolicited FETCH FLAG responses
List v = new ArrayList<>();
try {
r = getProtocol().fetch(msgsets, command.toString());
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (CommandFailedException cfx) {
// Ignore these, as per RFC 2180
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
if (r == null)
return;
for (int i = 0; i < r.length; i++) {
if (r[i] == null)
continue;
if (!(r[i] instanceof FetchResponse)) {
v.add(r[i]); // Unsolicited Non-FETCH response
continue;
}
// Got a FetchResponse.
FetchResponse f = (FetchResponse)r[i];
// Get the corresponding message.
IMAPMessage msg = getMessageBySeqNumber(f.getNumber());
int count = f.getItemCount();
boolean unsolicitedFlags = false;
for (int j = 0; j < count; j++) {
Item item = f.getItem(j);
// Check for the FLAGS item
if (item instanceof Flags &&
(!fp.contains(FetchProfile.Item.FLAGS) ||
msg == null)) {
// Ok, Unsolicited FLAGS update.
unsolicitedFlags = true;
} else if (msg != null)
msg.handleFetchItem(item, hdrs, allHeaders);
}
if (msg != null)
msg.handleExtensionFetchItems(f.getExtensionItems());
// If this response contains any unsolicited FLAGS
// add it to the unsolicited response vector
if (unsolicitedFlags)
v.add(f);
}
// Dispatch any unsolicited responses
if (!v.isEmpty()) {
Response[] responses = new Response[v.size()];
v.toArray(responses);
handleResponses(responses);
}
} // Release messageCacheLock
}
/**
* Return the IMAP FETCH items to request in order to load
* all the "envelope" data. Subclasses can override this
* method to fetch more data when FetchProfile.Item.ENVELOPE
* is requested.
*
* @return the IMAP FETCH items to request
* @since JavaMail 1.4.6
*/
protected String getEnvelopeCommand() {
return IMAPMessage.EnvelopeCmd;
}
/**
* Create a new IMAPMessage object to represent the given message number.
* Subclasses of IMAPFolder may override this method to create a
* subclass of IMAPMessage.
*
* @param msgnum the message sequence number
* @return the new IMAPMessage object
* @since JavaMail 1.4.6
*/
protected IMAPMessage newIMAPMessage(int msgnum) {
return new IMAPMessage(this, msgnum);
}
/**
* Create the appropriate IMAP FETCH command items to fetch the
* requested headers.
*/
private String createHeaderCommand(String[] hdrs, boolean isRev1) {
StringBuilder sb;
if (isRev1)
sb = new StringBuilder("BODY.PEEK[HEADER.FIELDS (");
else
sb = new StringBuilder("RFC822.HEADER.LINES (");
for (int i = 0; i < hdrs.length; i++) {
if (i > 0)
sb.append(" ");
sb.append(hdrs[i]);
}
if (isRev1)
sb.append(")]");
else
sb.append(")");
return sb.toString();
}
/**
* Set the specified flags for the given array of messages.
*/
@Override
public synchronized void setFlags(Message[] msgs, Flags flag, boolean value)
throws MessagingException {
checkOpened();
checkFlags(flag); // validate flags
if (msgs.length == 0) // boundary condition
return;
synchronized(messageCacheLock) {
try {
IMAPProtocol p = getProtocol();
MessageSet[] ms = Utility.toMessageSetSorted(msgs, null);
if (ms == null)
throw new MessageRemovedException(
"Messages have been removed");
p.storeFlags(ms, flag, value);
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
}
/**
* Set the specified flags for the given range of message numbers.
*/
@Override
public synchronized void setFlags(int start, int end,
Flags flag, boolean value) throws MessagingException {
checkOpened();
Message[] msgs = new Message[end - start + 1];
int i = 0;
for (int n = start; n <= end; n++)
msgs[i++] = getMessage(n);
setFlags(msgs, flag, value);
}
/**
* Set the specified flags for the given array of message numbers.
*/
@Override
public synchronized void setFlags(int[] msgnums, Flags flag, boolean value)
throws MessagingException {
checkOpened();
Message[] msgs = new Message[msgnums.length];
for (int i = 0; i < msgnums.length; i++)
msgs[i] = getMessage(msgnums[i]);
setFlags(msgs, flag, value);
}
/**
* Close this folder.
*/
@Override
public synchronized void close(boolean expunge) throws MessagingException {
close(expunge, false);
}
/**
* Close this folder without waiting for the server.
*
* @exception MessagingException for failures
*/
public synchronized void forceClose() throws MessagingException {
close(false, true);
}
/*
* Common close method.
*/
private void close(boolean expunge, boolean force)
throws MessagingException {
assert Thread.holdsLock(this);
synchronized(messageCacheLock) {
/*
* If we already know we're closed, this is illegal.
* Can't use checkOpened() because if we were forcibly
* closed asynchronously we just want to complete the
* closing here.
*/
if (!opened && reallyClosed)
throw new IllegalStateException(
"This operation is not allowed on a closed folder"
);
reallyClosed = true; // Ok, lets reset
// Maybe this folder is already closed, or maybe another
// thread which had the messageCacheLock earlier, found
// that our server connection is dead and cleaned up
// everything ..
if (!opened)
return;
boolean reuseProtocol = true;
try {
waitIfIdle();
if (force) {
logger.log(Level.FINE, "forcing folder {0} to close",
fullName);
if (protocol != null)
protocol.disconnect();
} else if (((IMAPStore)store).isConnectionPoolFull()) {
// If the connection pool is full, logout the connection
logger.fine(
"pool is full, not adding an Authenticated connection");
// If the expunge flag is set, close the folder first.
if (expunge && protocol != null)
protocol.close();
if (protocol != null)
protocol.logout();
} else {
// If the expunge flag is set or we're open read-only we
// can just close the folder, otherwise open it read-only
// before closing, or unselect it if supported.
if (!expunge && mode == READ_WRITE) {
try {
if (protocol != null &&
protocol.hasCapability("UNSELECT"))
protocol.unselect();
else {
// Unselect isn't supported so we need to
// select a folder to cause this one to be
// deselected without expunging messages.
// We try to do that by reopening the current
// folder read-only. If the current folder
// was renamed out from under us, the EXAMINE
// might fail, but that's ok because it still
// leaves us with the folder deselected.
if (protocol != null) {
boolean selected = true;
try {
protocol.examine(fullName);
// success, folder still selected
} catch (CommandFailedException ex) {
// EXAMINE failed, folder is no
// longer selected
selected = false;
}
if (selected && protocol != null)
protocol.close();
}
}
} catch (ProtocolException pex2) {
reuseProtocol = false; // something went wrong
}
} else {
if (protocol != null)
protocol.close();
}
}
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
} finally {
// cleanup if we haven't already
if (opened)
cleanup(reuseProtocol);
}
}
}
// NOTE: this method can currently be invoked from close() or
// from handleResponses(). Both invocations are conditional,
// based on the "opened" flag, so we are sure that multiple
// Connection.CLOSED events are not generated. Also both
// invocations are from within messageCacheLock-ed areas.
private void cleanup(boolean returnToPool) {
assert Thread.holdsLock(messageCacheLock);
releaseProtocol(returnToPool);
messageCache = null;
uidTable = null;
exists = false; // to force a recheck in exists().
attributes = null;
opened = false;
idleState = RUNNING; // just in case
messageCacheLock.notifyAll(); // wake up anyone waiting
notifyConnectionListeners(ConnectionEvent.CLOSED);
}
/**
* Check whether this connection is really open.
*/
@Override
public synchronized boolean isOpen() {
synchronized(messageCacheLock) {
// Probe the connection to make sure its really open.
if (opened) {
try {
keepConnectionAlive(false);
} catch (ProtocolException pex) { }
}
}
return opened;
}
/**
* Return the permanent flags supported by the server.
*/
@Override
public synchronized Flags getPermanentFlags() {
if (permanentFlags == null)
return null;
return (Flags)(permanentFlags.clone());
}
/**
* Get the total message count.
*/
@Override
public synchronized int getMessageCount() throws MessagingException {
synchronized (messageCacheLock) {
if (opened) {
// Folder is open, we know what the total message count is ..
// tickle the folder and store connections.
try {
keepConnectionAlive(true);
return total;
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
}
// If this folder is not yet open, we use STATUS to
// get the total message count
checkExists();
try {
Status status = getStatus();
return status.total;
} catch (BadCommandException bex) {
// doesn't support STATUS, probably vanilla IMAP4 ..
// lets try EXAMINE
IMAPProtocol p = null;
try {
p = getStoreProtocol(); // XXX
MailboxInfo minfo = p.examine(fullName);
p.close();
return minfo.total;
} catch (ProtocolException pex) {
// Give up.
throw new MessagingException(pex.getMessage(), pex);
} finally {
releaseStoreProtocol(p);
}
} catch (ConnectionException cex) {
throw new StoreClosedException(store, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
/**
* Get the new message count.
*/
@Override
public synchronized int getNewMessageCount() throws MessagingException {
synchronized (messageCacheLock) {
if (opened) {
// Folder is open, we know what the new message count is ..
// tickle the folder and store connections.
try {
keepConnectionAlive(true);
return recent;
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
}
// If this folder is not yet open, we use STATUS to
// get the new message count
checkExists();
try {
Status status = getStatus();
return status.recent;
} catch (BadCommandException bex) {
// doesn't support STATUS, probably vanilla IMAP4 ..
// lets try EXAMINE
IMAPProtocol p = null;
try {
p = getStoreProtocol(); // XXX
MailboxInfo minfo = p.examine(fullName);
p.close();
return minfo.recent;
} catch (ProtocolException pex) {
// Give up.
throw new MessagingException(pex.getMessage(), pex);
} finally {
releaseStoreProtocol(p);
}
} catch (ConnectionException cex) {
throw new StoreClosedException(store, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
/**
* Get the unread message count.
*/
@Override
public synchronized int getUnreadMessageCount()
throws MessagingException {
if (!opened) {
checkExists();
// If this folder is not yet open, we use STATUS to
// get the unseen message count
try {
Status status = getStatus();
return status.unseen;
} catch (BadCommandException bex) {
// doesn't support STATUS, probably vanilla IMAP4 ..
// Could EXAMINE, SEARCH for UNREAD messages and
// return the count .. bah, not worth it.
return -1;
} catch (ConnectionException cex) {
throw new StoreClosedException(store, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
// if opened, issue server-side search for messages that do
// *not* have the SEEN flag.
Flags f = new Flags();
f.add(Flags.Flag.SEEN);
try {
synchronized(messageCacheLock) {
int[] matches = getProtocol().search(new FlagTerm(f, false));
return matches.length; // NOTE: 'matches' is never null
}
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
// Shouldn't happen
throw new MessagingException(pex.getMessage(), pex);
}
}
/**
* Get the deleted message count.
*/
@Override
public synchronized int getDeletedMessageCount()
throws MessagingException {
if (!opened) {
checkExists();
// no way to do this on closed folders
return -1;
}
// if opened, issue server-side search for messages that do
// have the DELETED flag.
Flags f = new Flags();
f.add(Flags.Flag.DELETED);
try {
synchronized(messageCacheLock) {
int[] matches = getProtocol().search(new FlagTerm(f, true));
return matches.length; // NOTE: 'matches' is never null
}
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
// Shouldn't happen
throw new MessagingException(pex.getMessage(), pex);
}
}
/*
* Get results of STATUS command for this folder, checking cache first.
* ASSERT: Must be called with this folder's synchronization lock held.
* ASSERT: The folder must be closed.
*/
private Status getStatus() throws ProtocolException {
int statusCacheTimeout = ((IMAPStore)store).getStatusCacheTimeout();
// if allowed to cache and our cache is still valid, return it
if (statusCacheTimeout > 0 && cachedStatus != null &&
System.currentTimeMillis() - cachedStatusTime < statusCacheTimeout)
return cachedStatus;
IMAPProtocol p = null;
try {
p = getStoreProtocol(); // XXX
Status s = p.status(fullName, null);
// if allowed to cache, do so
if (statusCacheTimeout > 0) {
cachedStatus = s;
cachedStatusTime = System.currentTimeMillis();
}
return s;
} finally {
releaseStoreProtocol(p);
}
}
/**
* Get the specified message.
*/
@Override
public synchronized Message getMessage(int msgnum)
throws MessagingException {
checkOpened();
checkRange(msgnum);
return messageCache.getMessage(msgnum);
}
/**
* {@inheritDoc}
*/
@Override
public synchronized Message[] getMessages() throws MessagingException {
/*
* Need to override Folder method to throw FolderClosedException
* instead of IllegalStateException if not really closed.
*/
checkOpened();
int total = getMessageCount();
Message[] msgs = new Message[total];
for (int i = 1; i <= total; i++)
msgs[i - 1] = messageCache.getMessage(i);
return msgs;
}
/**
* Append the given messages into this folder.
*/
@Override
public synchronized void appendMessages(Message[] msgs)
throws MessagingException {
checkExists(); // verify that self exists
// XXX - have to verify that messages are in a different
// store (if any) than target folder, otherwise could
// deadlock trying to fetch messages on the same connection
// we're using for the append.
int maxsize = ((IMAPStore)store).getAppendBufferSize();
for (int i = 0; i < msgs.length; i++) {
final Message m = msgs[i];
Date d = m.getReceivedDate(); // retain dates
if (d == null)
d = m.getSentDate();
final Date dd = d;
final Flags f = m.getFlags();
final MessageLiteral mos;
try {
// if we know the message is too big, don't buffer any of it
mos = new MessageLiteral(m,
m.getSize() > maxsize ? 0 : maxsize);
} catch (IOException ex) {
throw new MessagingException(
"IOException while appending messages", ex);
} catch (MessageRemovedException mrex) {
continue; // just skip this expunged message
}
doCommand(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
p.append(fullName, f, dd, mos);
return null;
}
});
}
}
/**
* Append the given messages into this folder.
* Return array of AppendUID objects containing
* UIDs of these messages in the destination folder.
* Each element of the returned array corresponds to
* an element of the msgs
array. A null
* element means the server didn't return UID information
* for the appended message.
*
* Depends on the APPENDUID response code defined by the
* UIDPLUS extension -
* RFC 4315.
*
* @param msgs the messages to append
* @return array of AppendUID objects
* @exception MessagingException for failures
* @since JavaMail 1.4
*/
public synchronized AppendUID[] appendUIDMessages(Message[] msgs)
throws MessagingException {
checkExists(); // verify that self exists
// XXX - have to verify that messages are in a different
// store (if any) than target folder, otherwise could
// deadlock trying to fetch messages on the same connection
// we're using for the append.
int maxsize = ((IMAPStore)store).getAppendBufferSize();
AppendUID[] uids = new AppendUID[msgs.length];
for (int i = 0; i < msgs.length; i++) {
final Message m = msgs[i];
final MessageLiteral mos;
try {
// if we know the message is too big, don't buffer any of it
mos = new MessageLiteral(m,
m.getSize() > maxsize ? 0 : maxsize);
} catch (IOException ex) {
throw new MessagingException(
"IOException while appending messages", ex);
} catch (MessageRemovedException mrex) {
continue; // just skip this expunged message
}
Date d = m.getReceivedDate(); // retain dates
if (d == null)
d = m.getSentDate();
final Date dd = d;
final Flags f = m.getFlags();
AppendUID auid = (AppendUID)doCommand(new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
return p.appenduid(fullName, f, dd, mos);
}
});
uids[i] = auid;
}
return uids;
}
/**
* Append the given messages into this folder.
* Return array of Message objects representing
* the messages in the destination folder. Note
* that the folder must be open.
* Each element of the returned array corresponds to
* an element of the msgs
array. A null
* element means the server didn't return UID information
* for the appended message.
*
* Depends on the APPENDUID response code defined by the
* UIDPLUS extension -
* RFC 4315.
*
* @param msgs the messages to add
* @return the messages in this folder
* @exception MessagingException for failures
* @since JavaMail 1.4
*/
public synchronized Message[] addMessages(Message[] msgs)
throws MessagingException {
checkOpened();
Message[] rmsgs = new MimeMessage[msgs.length];
AppendUID[] uids = appendUIDMessages(msgs);
for (int i = 0; i < uids.length; i++) {
AppendUID auid = uids[i];
if (auid != null) {
if (auid.uidvalidity == uidvalidity) {
try {
rmsgs[i] = getMessageByUID(auid.uid);
} catch (MessagingException mex) {
// ignore errors at this stage
}
}
}
}
return rmsgs;
}
/**
* Copy the specified messages from this folder, to the
* specified destination.
*/
@Override
public synchronized void copyMessages(Message[] msgs, Folder folder)
throws MessagingException {
copymoveMessages(msgs, folder, false);
}
/**
* Copy the specified messages from this folder, to the
* specified destination.
* Return array of AppendUID objects containing
* UIDs of these messages in the destination folder.
* Each element of the returned array corresponds to
* an element of the msgs
array. A null
* element means the server didn't return UID information
* for the copied message.
*
* Depends on the COPYUID response code defined by the
* UIDPLUS extension -
* RFC 4315.
*
* @param msgs the messages to copy
* @param folder the folder to copy the messages to
* @return array of AppendUID objects
* @exception MessagingException for failures
* @since JavaMail 1.5.1
*/
public synchronized AppendUID[] copyUIDMessages(Message[] msgs,
Folder folder) throws MessagingException {
return copymoveUIDMessages(msgs, folder, false);
}
/**
* Move the specified messages from this folder, to the
* specified destination.
*
* Depends on the MOVE extension
* (RFC 6851).
*
* @param msgs the messages to move
* @param folder the folder to move the messages to
* @exception MessagingException for failures
*
* @since JavaMail 1.5.4
*/
public synchronized void moveMessages(Message[] msgs, Folder folder)
throws MessagingException {
copymoveMessages(msgs, folder, true);
}
/**
* Move the specified messages from this folder, to the
* specified destination.
* Return array of AppendUID objects containing
* UIDs of these messages in the destination folder.
* Each element of the returned array corresponds to
* an element of the msgs
array. A null
* element means the server didn't return UID information
* for the moved message.
*
* Depends on the MOVE extension
* (RFC 6851)
* and the COPYUID response code defined by the
* UIDPLUS extension
* (RFC 4315).
*
* @param msgs the messages to move
* @param folder the folder to move the messages to
* @return array of AppendUID objects
* @exception MessagingException for failures
* @since JavaMail 1.5.4
*/
public synchronized AppendUID[] moveUIDMessages(Message[] msgs,
Folder folder) throws MessagingException {
return copymoveUIDMessages(msgs, folder, true);
}
/**
* Copy or move the specified messages from this folder, to the
* specified destination.
*
* @since JavaMail 1.5.4
*/
private synchronized void copymoveMessages(Message[] msgs, Folder folder,
boolean move) throws MessagingException {
checkOpened();
if (msgs.length == 0) // boundary condition
return;
// If the destination belongs to our same store, optimize
if (folder.getStore() == store) {
synchronized(messageCacheLock) {
try {
IMAPProtocol p = getProtocol();
MessageSet[] ms = Utility.toMessageSet(msgs, null);
if (ms == null)
throw new MessageRemovedException(
"Messages have been removed");
if (move)
p.move(ms, folder.getFullName());
else
p.copy(ms, folder.getFullName());
} catch (CommandFailedException cfx) {
if (cfx.getMessage().indexOf("TRYCREATE") != -1)
throw new FolderNotFoundException(
folder,
folder.getFullName() + " does not exist"
);
else
throw new MessagingException(cfx.getMessage(), cfx);
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
} else // destination is a different store.
if (move)
throw new MessagingException(
"Move between stores not supported");
else
super.copyMessages(msgs, folder);
}
/**
* Copy or move the specified messages from this folder, to the
* specified destination.
* Return array of AppendUID objects containing
* UIDs of these messages in the destination folder.
* Each element of the returned array corresponds to
* an element of the msgs
array. A null
* element means the server didn't return UID information
* for the copied message.
*
* Depends on the COPYUID response code defined by the
* UIDPLUS extension -
* RFC 4315.
* Move depends on the MOVE extension -
* RFC 6851.
*
* @param msgs the messages to copy
* @param folder the folder to copy the messages to
* @param move move instead of copy?
* @return array of AppendUID objects
* @exception MessagingException for failures
* @since JavaMail 1.5.4
*/
private synchronized AppendUID[] copymoveUIDMessages(Message[] msgs,
Folder folder, boolean move) throws MessagingException {
checkOpened();
if (msgs.length == 0) // boundary condition
return null;
// the destination must belong to our same store
if (folder.getStore() != store) // destination is a different store.
throw new MessagingException(
move ?
"can't moveUIDMessages to a different store" :
"can't copyUIDMessages to a different store");
// call fetch to make sure we have all the UIDs
// necessary to interpret the COPYUID response
FetchProfile fp = new FetchProfile();
fp.add(UIDFolder.FetchProfileItem.UID);
fetch(msgs, fp);
// XXX - could pipeline the FETCH with the COPY/MOVE below
synchronized (messageCacheLock) {
try {
IMAPProtocol p = getProtocol();
// XXX - messages have to be from this Folder, who checks?
MessageSet[] ms = Utility.toMessageSet(msgs, null);
if (ms == null)
throw new MessageRemovedException(
"Messages have been removed");
CopyUID cuid;
if (move)
cuid = p.moveuid(ms, folder.getFullName());
else
cuid = p.copyuid(ms, folder.getFullName());
/*
* Correlate source UIDs with destination UIDs.
* This won't be time or space efficient if there's
* a lot of messages.
*
* In order to make sense of the returned UIDs, we need
* the UIDs for every one of the original messages.
* We fetch them above, to make sure we have them.
* This is critical for MOVE since after the MOVE the
* messages are gone/expunged.
*
* Assume the common case is that the messages are
* in order by UID. Map the returned source
* UIDs to their corresponding Message objects.
* Step through the msgs array looking for the
* Message object in the returned source message
* list. Most commonly the source message (UID)
* for the Nth original message will be in the Nth
* position in the returned source message (UID)
* list. Thus, the destination UID is in the Nth
* position in the returned destination UID list.
* But if the source message isn't where expected,
* we have to search the entire source message
* list, starting from where we expect it and
* wrapping around until we've searched it all.
* (Gmail will often return the lists in an unexpected order.)
*
* A possible optimization:
* If the number of UIDs returned is the same as the
* number of messages being copied/moved, we could
* sort the source messages by message number, sort
* the source and destination parallel arrays by source
* UID, and the resulting message and destination UID
* arrays will correspond.
*
* If the returned UID array size is different, some
* message was expunged while we were trying to copy/move it.
* This should be rare but would mean falling back to the
* general algorithm.
*/
long[] srcuids = UIDSet.toArray(cuid.src);
long[] dstuids = UIDSet.toArray(cuid.dst);
// map source UIDs to Message objects
// XXX - could inline/optimize this
Message[] srcmsgs = getMessagesByUID(srcuids);
AppendUID[] result = new AppendUID[msgs.length];
for (int i = 0; i < msgs.length; i++) {
int j = i;
do {
if (msgs[i] == srcmsgs[j]) {
result[i] = new AppendUID(
cuid.uidvalidity, dstuids[j]);
break;
}
j++;
if (j >= srcmsgs.length)
j = 0;
} while (j != i);
}
return result;
} catch (CommandFailedException cfx) {
if (cfx.getMessage().indexOf("TRYCREATE") != -1)
throw new FolderNotFoundException(
folder,
folder.getFullName() + " does not exist"
);
else
throw new MessagingException(cfx.getMessage(), cfx);
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
}
/**
* Expunge all messages marked as DELETED.
*/
@Override
public synchronized Message[] expunge() throws MessagingException {
return expunge(null);
}
/**
* Expunge the indicated messages, which must have been marked as DELETED.
*
* Depends on the UIDPLUS extension -
* RFC 4315.
*
* @param msgs the messages to expunge
* @return the expunged messages
* @exception MessagingException for failures
*/
public synchronized Message[] expunge(Message[] msgs)
throws MessagingException {
checkOpened();
if (msgs != null) {
// call fetch to make sure we have all the UIDs
FetchProfile fp = new FetchProfile();
fp.add(UIDFolder.FetchProfileItem.UID);
fetch(msgs, fp);
}
IMAPMessage[] rmsgs;
synchronized(messageCacheLock) {
doExpungeNotification = false; // We do this ourselves later
try {
IMAPProtocol p = getProtocol();
if (msgs != null)
p.uidexpunge(Utility.toUIDSet(msgs));
else
p.expunge();
} catch (CommandFailedException cfx) {
// expunge not allowed, perhaps due to a permission problem?
if (mode != READ_WRITE)
throw new IllegalStateException(
"Cannot expunge READ_ONLY folder: " + fullName);
else
throw new MessagingException(cfx.getMessage(), cfx);
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
// Bad bad server ..
throw new MessagingException(pex.getMessage(), pex);
} finally {
doExpungeNotification = true;
}
// Cleanup expunged messages and sync messageCache with reality.
if (msgs != null)
rmsgs = messageCache.removeExpungedMessages(msgs);
else
rmsgs = messageCache.removeExpungedMessages();
if (uidTable != null) {
for (int i = 0; i < rmsgs.length; i++) {
IMAPMessage m = rmsgs[i];
/* remove this message from the UIDTable */
long uid = m.getUID();
if (uid != -1)
uidTable.remove(Long.valueOf(uid));
}
}
// Update 'total'
total = messageCache.size();
}
// Notify listeners. This time its for real, guys.
if (rmsgs.length > 0)
notifyMessageRemovedListeners(true, rmsgs);
return rmsgs;
}
/**
* Search whole folder for messages matching the given term.
* If the property mail.imap.throwsearchexception
is true,
* and the search term is too complex for the IMAP protocol,
* SearchException is thrown. Otherwise, if the search term is too
* complex, super.search
is called to do the search on
* the client.
*
* @param term the search term
* @return the messages that match
* @exception SearchException if mail.imap.throwsearchexception is
* true and the search is too complex for the IMAP protocol
* @exception MessagingException for other failures
*/
@Override
public synchronized Message[] search(SearchTerm term)
throws MessagingException {
checkOpened();
try {
Message[] matchMsgs = null;
synchronized(messageCacheLock) {
int[] matches = getProtocol().search(term);
if (matches != null)
matchMsgs = getMessagesBySeqNumbers(matches);
}
return matchMsgs;
} catch (CommandFailedException cfx) {
// unsupported charset or search criterion
return super.search(term);
} catch (SearchException sex) {
// too complex for IMAP
if (((IMAPStore)store).throwSearchException())
throw sex;
return super.search(term);
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
// bug in our IMAP layer ?
throw new MessagingException(pex.getMessage(), pex);
}
}
/**
* Search the folder for messages matching the given term. Returns
* array of matching messages. Returns an empty array if no matching
* messages are found.
*/
@Override
public synchronized Message[] search(SearchTerm term, Message[] msgs)
throws MessagingException {
checkOpened();
if (msgs.length == 0)
// need to return an empty array (not null!)
return msgs;
try {
Message[] matchMsgs = null;
synchronized(messageCacheLock) {
IMAPProtocol p = getProtocol();
MessageSet[] ms = Utility.toMessageSetSorted(msgs, null);
if (ms == null)
throw new MessageRemovedException(
"Messages have been removed");
int[] matches = p.search(ms, term);
if (matches != null)
matchMsgs = getMessagesBySeqNumbers(matches);
}
return matchMsgs;
} catch (CommandFailedException cfx) {
// unsupported charset or search criterion
return super.search(term, msgs);
} catch (SearchException sex) {
// too complex for IMAP
return super.search(term, msgs);
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
// bug in our IMAP layer ?
throw new MessagingException(pex.getMessage(), pex);
}
}
/**
* Sort the messages in the folder according to the sort criteria.
* The messages are returned in the sorted order, but the order of
* the messages in the folder is not changed.
*
* Depends on the SORT extension -
* RFC 5256.
*
* @param term the SortTerms
* @return the messages in sorted order
* @exception MessagingException for failures
* @since JavaMail 1.4.4
*/
public synchronized Message[] getSortedMessages(SortTerm[] term)
throws MessagingException {
return getSortedMessages(term, null);
}
/**
* Sort the messages in the folder according to the sort criteria.
* The messages are returned in the sorted order, but the order of
* the messages in the folder is not changed. Only messages matching
* the search criteria are considered.
*
* Depends on the SORT extension -
* RFC 5256.
*
* @param term the SortTerms
* @param sterm the SearchTerm
* @return the messages in sorted order
* @exception MessagingException for failures
* @since JavaMail 1.4.4
*/
public synchronized Message[] getSortedMessages(SortTerm[] term,
SearchTerm sterm) throws MessagingException {
checkOpened();
try {
Message[] matchMsgs = null;
synchronized(messageCacheLock) {
int[] matches = getProtocol().sort(term, sterm);
if (matches != null)
matchMsgs = getMessagesBySeqNumbers(matches);
}
return matchMsgs;
} catch (CommandFailedException cfx) {
// unsupported charset or search criterion
throw new MessagingException(cfx.getMessage(), cfx);
} catch (SearchException sex) {
// too complex for IMAP
throw new MessagingException(sex.getMessage(), sex);
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
// bug in our IMAP layer ?
throw new MessagingException(pex.getMessage(), pex);
}
}
/*
* Override Folder method to keep track of whether we have any
* message count listeners. Normally we won't have any, so we
* can avoid creating message objects to pass to the notify
* method. It's too hard to keep track of when all listeners
* are removed, and that's a rare case, so we don't try.
*/
@Override
public synchronized void addMessageCountListener(MessageCountListener l) {
super.addMessageCountListener(l);
hasMessageCountListener = true;
}
/***********************************************************
* UIDFolder interface methods
**********************************************************/
/**
* Returns the UIDValidity for this folder.
*/
@Override
public synchronized long getUIDValidity() throws MessagingException {
if (opened) // we already have this information
return uidvalidity;
IMAPProtocol p = null;
Status status = null;
try {
p = getStoreProtocol(); // XXX
String[] item = { "UIDVALIDITY" };
status = p.status(fullName, item);
} catch (BadCommandException bex) {
// Probably a RFC1730 server
throw new MessagingException("Cannot obtain UIDValidity", bex);
} catch (ConnectionException cex) {
// Oops, the store or folder died on us.
throwClosedException(cex);
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
} finally {
releaseStoreProtocol(p);
}
if (status == null)
throw new MessagingException("Cannot obtain UIDValidity");
return status.uidvalidity;
}
/**
* Returns the predicted UID that will be assigned to the
* next message that is appended to this folder.
* If the folder is closed, the STATUS command is used to
* retrieve this value. If the folder is open, the value
* returned from the SELECT or EXAMINE command is returned.
* Note that messages may have been appended to the folder
* while it was open and thus this value may be out of
* date.
*
* Servers implementing RFC2060 likely won't return this value
* when a folder is opened. Servers implementing RFC3501
* should return this value when a folder is opened.
*
* @return the UIDNEXT value, or -1 if unknown
* @exception MessagingException for failures
* @since JavaMail 1.3.3
*/
@Override
public synchronized long getUIDNext() throws MessagingException {
if (opened) // we already have this information
return uidnext;
IMAPProtocol p = null;
Status status = null;
try {
p = getStoreProtocol(); // XXX
String[] item = { "UIDNEXT" };
status = p.status(fullName, item);
} catch (BadCommandException bex) {
// Probably a RFC1730 server
throw new MessagingException("Cannot obtain UIDNext", bex);
} catch (ConnectionException cex) {
// Oops, the store or folder died on us.
throwClosedException(cex);
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
} finally {
releaseStoreProtocol(p);
}
if (status == null)
throw new MessagingException("Cannot obtain UIDNext");
return status.uidnext;
}
/**
* Get the Message corresponding to the given UID.
* If no such message exists, null
is returned.
*/
@Override
public synchronized Message getMessageByUID(long uid)
throws MessagingException {
checkOpened(); // insure folder is open
IMAPMessage m = null;
try {
synchronized(messageCacheLock) {
Long l = Long.valueOf(uid);
if (uidTable != null) {
// Check in uidTable
m = uidTable.get(l);
if (m != null) // found it
return m;
} else
uidTable = new Hashtable<>();
// Check with the server
// Issue UID FETCH command
getProtocol().fetchSequenceNumber(uid);
if (uidTable != null) {
// Check in uidTable
m = uidTable.get(l);
if (m != null) // found it
return m;
}
}
} catch(ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
return m;
}
/**
* Get the Messages specified by the given range.
* Returns Message objects for all valid messages in this range.
* Returns an empty array if no messages are found.
*/
@Override
public synchronized Message[] getMessagesByUID(long start, long end)
throws MessagingException {
checkOpened(); // insure that folder is open
Message[] msgs; // array of messages to be returned
try {
synchronized(messageCacheLock) {
if (uidTable == null)
uidTable = new Hashtable<>();
// Issue UID FETCH for given range
long[] ua = getProtocol().fetchSequenceNumbers(start, end);
List ma = new ArrayList<>();
// NOTE: Below must be within messageCacheLock region
for (int i = 0; i < ua.length; i++) {
Message m = uidTable.get(Long.valueOf(ua[i]));
if (m != null) // found it
ma.add(m);
}
msgs = ma.toArray(new Message[ma.size()]);
}
} catch(ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
return msgs;
}
/**
* Get the Messages specified by the given array.
*
* uids.length()
elements are returned.
* If any UID in the array is invalid, a null
entry
* is returned for that element.
*/
@Override
public synchronized Message[] getMessagesByUID(long[] uids)
throws MessagingException {
checkOpened(); // insure that folder is open
try {
synchronized(messageCacheLock) {
long[] unavailUids = uids;
if (uidTable != null) {
// to collect unavailable UIDs
List v = new ArrayList<>();
for (long uid : uids) {
if (!uidTable.containsKey(uid)) {
// This UID has not been loaded yet.
v.add(uid);
}
}
int vsize = v.size();
unavailUids = new long[vsize];
for (int i = 0; i < vsize; i++) {
unavailUids[i] = v.get(i);
}
} else
uidTable = new Hashtable<>();
if (unavailUids.length > 0) {
// Issue UID FETCH request for given uids
getProtocol().fetchSequenceNumbers(unavailUids);
}
// Return array of size = uids.length
Message[] msgs = new Message[uids.length];
for (int i = 0; i < uids.length; i++)
msgs[i] = (Message)uidTable.get(Long.valueOf(uids[i]));
return msgs;
}
} catch(ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
/**
* Get the UID for the specified message.
*/
@Override
public synchronized long getUID(Message message)
throws MessagingException {
if (message.getFolder() != this)
throw new NoSuchElementException(
"Message does not belong to this folder");
checkOpened(); // insure that folder is open
if (!(message instanceof IMAPMessage))
throw new MessagingException("message is not an IMAPMessage");
IMAPMessage m = (IMAPMessage)message;
// If the message already knows its UID, great ..
long uid;
if ((uid = m.getUID()) != -1)
return uid;
synchronized(messageCacheLock) { // Acquire Lock
try {
IMAPProtocol p = getProtocol();
m.checkExpunged(); // insure that message is not expunged
UID u = p.fetchUID(m.getSequenceNumber());
if (u != null) {
uid = u.uid;
m.setUID(uid); // set message's UID
// insert this message into uidTable
if (uidTable == null)
uidTable = new Hashtable<>();
uidTable.put(Long.valueOf(uid), m);
}
} catch (ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
return uid;
}
/**
* Servers that support the UIDPLUS extension
* (RFC 4315)
* may indicate that this folder does not support persistent UIDs;
* that is, UIDVALIDITY will be different each time the folder is
* opened. Only valid when the folder is open.
*
* @return true if UIDs are not sticky
* @exception MessagingException for failures
* @exception IllegalStateException if the folder isn't open
* @see "RFC 4315"
* @since JavaMail 1.6.0
*/
public synchronized boolean getUIDNotSticky() throws MessagingException {
checkOpened();
return uidNotSticky;
}
/**
* Get or create Message objects for the UIDs.
*/
private Message[] createMessagesForUIDs(long[] uids) {
IMAPMessage[] msgs = new IMAPMessage[uids.length];
for (int i = 0; i < uids.length; i++) {
IMAPMessage m = null;
if (uidTable != null)
m = uidTable.get(Long.valueOf(uids[i]));
if (m == null) {
// fake it, we don't know what message this really is
m = newIMAPMessage(-1); // no sequence number
m.setUID(uids[i]);
m.setExpunged(true);
}
msgs[i++] = m;
}
return msgs;
}
/**
* Returns the HIGHESTMODSEQ for this folder.
*
* @return the HIGHESTMODSEQ value
* @exception MessagingException for failures
* @see "RFC 4551"
* @since JavaMail 1.5.1
*/
public synchronized long getHighestModSeq() throws MessagingException {
if (opened) // we already have this information
return highestmodseq;
IMAPProtocol p = null;
Status status = null;
try {
p = getStoreProtocol(); // XXX
if (!p.hasCapability("CONDSTORE"))
throw new BadCommandException("CONDSTORE not supported");
String[] item = { "HIGHESTMODSEQ" };
status = p.status(fullName, item);
} catch (BadCommandException bex) {
// Probably a RFC1730 server
throw new MessagingException("Cannot obtain HIGHESTMODSEQ", bex);
} catch (ConnectionException cex) {
// Oops, the store or folder died on us.
throwClosedException(cex);
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
} finally {
releaseStoreProtocol(p);
}
if (status == null)
throw new MessagingException("Cannot obtain HIGHESTMODSEQ");
return status.highestmodseq;
}
/**
* Get the messages that have been changed since the given MODSEQ value.
* Also, prefetch the flags for the messages.
*
* The server must support the CONDSTORE extension.
*
* @param start the first message number
* @param end the last message number
* @param modseq the MODSEQ value
* @return the changed messages
* @exception MessagingException for failures
* @see "RFC 4551"
* @since JavaMail 1.5.1
*/
public synchronized Message[] getMessagesByUIDChangedSince(
long start, long end, long modseq)
throws MessagingException {
checkOpened(); // insure that folder is open
try {
synchronized (messageCacheLock) {
IMAPProtocol p = getProtocol();
if (!p.hasCapability("CONDSTORE"))
throw new BadCommandException("CONDSTORE not supported");
// Issue FETCH for given range
int[] nums = p.uidfetchChangedSince(start, end, modseq);
return getMessagesBySeqNumbers(nums);
}
} catch(ConnectionException cex) {
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
}
/**
* Get the quotas for the quotaroot associated with this
* folder. Note that many folders may have the same quotaroot.
* Quotas are controlled on the basis of a quotaroot, not
* (necessarily) a folder. The relationship between folders
* and quotaroots depends on the IMAP server. Some servers
* might implement a single quotaroot for all folders owned by
* a user. Other servers might implement a separate quotaroot
* for each folder. A single folder can even have multiple
* quotaroots, perhaps controlling quotas for different
* resources.
*
* @return array of Quota objects for the quotaroots associated with
* this folder
* @exception MessagingException if the server doesn't support the
* QUOTA extension
*/
public Quota[] getQuota() throws MessagingException {
return (Quota[])doOptionalCommand("QUOTA not supported",
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
return p.getQuotaRoot(fullName);
}
});
}
/**
* Set the quotas for the quotaroot specified in the quota argument.
* Typically this will be one of the quotaroots associated with this
* folder, as obtained from the getQuota
method, but it
* need not be.
*
* @param quota the quota to set
* @exception MessagingException if the server doesn't support the
* QUOTA extension
*/
public void setQuota(final Quota quota) throws MessagingException {
doOptionalCommand("QUOTA not supported",
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
p.setQuota(quota);
return null;
}
});
}
/**
* Get the access control list entries for this folder.
*
* @return array of access control list entries
* @exception MessagingException if the server doesn't support the
* ACL extension
*/
public ACL[] getACL() throws MessagingException {
return (ACL[])doOptionalCommand("ACL not supported",
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
return p.getACL(fullName);
}
});
}
/**
* Add an access control list entry to the access control list
* for this folder.
*
* @param acl the access control list entry to add
* @exception MessagingException if the server doesn't support the
* ACL extension
*/
public void addACL(ACL acl) throws MessagingException {
setACL(acl, '\0');
}
/**
* Remove any access control list entry for the given identifier
* from the access control list for this folder.
*
* @param name the identifier for which to remove all ACL entries
* @exception MessagingException if the server doesn't support the
* ACL extension
*/
public void removeACL(final String name) throws MessagingException {
doOptionalCommand("ACL not supported",
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
p.deleteACL(fullName, name);
return null;
}
});
}
/**
* Add the rights specified in the ACL to the entry for the
* identifier specified in the ACL. If an entry for the identifier
* doesn't already exist, add one.
*
* @param acl the identifer and rights to add
* @exception MessagingException if the server doesn't support the
* ACL extension
*/
public void addRights(ACL acl) throws MessagingException {
setACL(acl, '+');
}
/**
* Remove the rights specified in the ACL from the entry for the
* identifier specified in the ACL.
*
* @param acl the identifer and rights to remove
* @exception MessagingException if the server doesn't support the
* ACL extension
*/
public void removeRights(ACL acl) throws MessagingException {
setACL(acl, '-');
}
/**
* Get all the rights that may be allowed to the given identifier.
* Rights are grouped per RFC 2086 and each group is returned as an
* element of the array. The first element of the array is the set
* of rights that are always granted to the identifier. Later
* elements are rights that may be optionally granted to the
* identifier.
*
* Note that this method lists the rights that it is possible to
* assign to the given identifier, not the rights that are
* actually granted to the given identifier. For the latter, see
* the getACL
method.
*
* @param name the identifier to list rights for
* @return array of Rights objects representing possible
* rights for the identifier
* @exception MessagingException if the server doesn't support the
* ACL extension
*/
public Rights[] listRights(final String name) throws MessagingException {
return (Rights[])doOptionalCommand("ACL not supported",
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
return p.listRights(fullName, name);
}
});
}
/**
* Get the rights allowed to the currently authenticated user.
*
* @return the rights granted to the current user
* @exception MessagingException if the server doesn't support the
* ACL extension
*/
public Rights myRights() throws MessagingException {
return (Rights)doOptionalCommand("ACL not supported",
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
return p.myRights(fullName);
}
});
}
private void setACL(final ACL acl, final char mod)
throws MessagingException {
doOptionalCommand("ACL not supported",
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
p.setACL(fullName, mod, acl);
return null;
}
});
}
/**
* Get the attributes that the IMAP server returns with the
* LIST response.
*
* @return array of attributes for this folder
* @exception MessagingException for failures
* @since JavaMail 1.3.3
*/
public synchronized String[] getAttributes() throws MessagingException {
checkExists();
if (attributes == null)
exists(); // do a LIST to set the attributes
return attributes == null ? new String[0] : attributes.clone();
}
/**
* Use the IMAP IDLE command (see
* RFC 2177),
* if supported by the server, to enter idle mode so that the server
* can send unsolicited notifications of new messages arriving, etc.
* without the need for the client to constantly poll the server.
* Use an appropriate listener to be notified of new messages or
* other events. When another thread (e.g., the listener thread)
* needs to issue an IMAP comand for this folder, the idle mode will
* be terminated and this method will return. Typically the caller
* will invoke this method in a loop.
*
* The mail.imap.minidletime property enforces a minimum delay
* before returning from this method, to ensure that other threads
* have a chance to issue commands before the caller invokes this
* method again. The default delay is 10 milliseconds.
*
* @exception MessagingException if the server doesn't support the
* IDLE extension
* @exception IllegalStateException if the folder isn't open
*
* @since JavaMail 1.4.1
*/
public void idle() throws MessagingException {
idle(false);
}
/**
* Like {@link #idle}, but if once
is true, abort the
* IDLE command after the first notification, to allow the caller
* to process any notification synchronously.
*
* @param once only do one notification?
* @exception MessagingException if the server doesn't support the
* IDLE extension
* @exception IllegalStateException if the folder isn't open
*
* @since JavaMail 1.4.3
*/
public void idle(boolean once) throws MessagingException {
synchronized (this) {
/*
* We can't support the idle method if we're using SocketChannels
* because SocketChannels don't allow simultaneous read and write.
* If we're blocked in a read waiting for IDLE responses, we can't
* send the DONE message to abort the IDLE. Sigh.
* XXX - We could do select here too, like IdleManager, instead
* of blocking in read, but that's more complicated.
*/
if (protocol != null && protocol.getChannel() != null)
throw new MessagingException(
"idle method not supported with SocketChannels");
}
if (!startIdle(null))
return;
/*
* We gave up the folder lock so that other threads
* can get into the folder far enough to see that we're
* in IDLE and abort the IDLE.
*
* Now we read responses from the IDLE command, especially
* including unsolicited notifications from the server.
* We don't hold the messageCacheLock while reading because
* it protects the idleState and other threads need to be
* able to examine the state.
*
* The messageCacheLock is held in handleIdle while processing
* the responses so that we can update the number of messages
* in the folder (for example).
*/
for (;;) {
if (!handleIdle(once))
break;
}
/*
* Enforce a minimum delay to give time to threads
* processing the responses that came in while we
* were idle.
*/
int minidle = ((IMAPStore)store).getMinIdleTime();
if (minidle > 0) {
try {
Thread.sleep(minidle);
} catch (InterruptedException ex) {
// restore the interrupted state, which callers might depend on
Thread.currentThread().interrupt();
}
}
}
/**
* Start the IDLE command and put this folder into the IDLE state.
* IDLE processing is done later in handleIdle(), e.g., called from
* the IdleManager.
*
* @return true if IDLE started, false otherwise
* @exception MessagingException if the server doesn't support the
* IDLE extension
* @exception IllegalStateException if the folder isn't open
* @since JavaMail 1.5.2
*/
boolean startIdle(final IdleManager im) throws MessagingException {
// ASSERT: Must NOT be called with this folder's
// synchronization lock held.
assert !Thread.holdsLock(this);
synchronized(this) {
checkOpened();
if (im != null && idleManager != null && im != idleManager)
throw new MessagingException(
"Folder already being watched by another IdleManager");
Boolean started = (Boolean)doOptionalCommand("IDLE not supported",
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
// if the IdleManager is already watching this folder,
// there's nothing to do here
if (idleState == IDLE &&
im != null && im == idleManager)
return Boolean.TRUE; // already watching it
if (idleState == RUNNING) {
p.idleStart();
logger.finest("startIdle: set to IDLE");
idleState = IDLE;
idleManager = im;
return Boolean.TRUE;
} else {
// some other thread must be running the IDLE
// command, we'll just wait for it to finish
// without aborting it ourselves
try {
// give up lock and wait to be not idle
messageCacheLock.wait();
} catch (InterruptedException ex) {
// restore the interrupted state, which callers
// might depend on
Thread.currentThread().interrupt();
}
return Boolean.FALSE;
}
}
});
logger.log(Level.FINEST, "startIdle: return {0}", started);
return started.booleanValue();
}
}
/**
* Read a response from the server while we're in the IDLE state.
* We hold the messageCacheLock while processing the
* responses so that we can update the number of messages
* in the folder (for example).
*
* @param once only do one notification?
* @return true if we should look for more IDLE responses,
* false if IDLE is done
* @exception MessagingException for errors
* @since JavaMail 1.5.2
*/
boolean handleIdle(boolean once) throws MessagingException {
Response r = null;
do {
r = protocol.readIdleResponse();
try {
synchronized (messageCacheLock) {
if (r.isBYE() && r.isSynthetic() && idleState == IDLE) {
/*
* If it was a timeout and no bytes were transferred
* we ignore it and go back and read again.
* If the I/O was otherwise interrupted, and no
* bytes were transferred, we take it as a request
* to abort the IDLE.
*/
Exception ex = r.getException();
if (ex instanceof InterruptedIOException &&
((InterruptedIOException)ex).
bytesTransferred == 0) {
if (ex instanceof SocketTimeoutException) {
logger.finest(
"handleIdle: ignoring socket timeout");
r = null; // repeat do/while loop
} else {
logger.finest("handleIdle: interrupting IDLE");
IdleManager im = idleManager;
if (im != null) {
logger.finest(
"handleIdle: request IdleManager to abort");
im.requestAbort(this);
} else {
logger.finest("handleIdle: abort IDLE");
protocol.idleAbort();
idleState = ABORTING;
}
// normally will exit the do/while loop
}
continue;
}
}
boolean done = true;
try {
if (protocol == null ||
!protocol.processIdleResponse(r))
return false; // done
done = false;
} finally {
if (done) {
logger.finest("handleIdle: set to RUNNING");
idleState = RUNNING;
idleManager = null;
messageCacheLock.notifyAll();
}
}
if (once) {
if (idleState == IDLE) {
try {
protocol.idleAbort();
} catch (Exception ex) {
// ignore any failures, still have to abort.
// connection failures will be detected above
// in the call to readIdleResponse.
}
idleState = ABORTING;
}
}
}
} catch (ConnectionException cex) {
// Oops, the folder died on us.
throw new FolderClosedException(this, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
// keep processing responses already in our buffer
} while (r == null || protocol.hasResponse());
return true;
}
/*
* If an IDLE command is in progress, abort it if necessary,
* and wait until it completes.
* ASSERT: Must be called with the message cache lock held.
*/
void waitIfIdle() throws ProtocolException {
assert Thread.holdsLock(messageCacheLock);
while (idleState != RUNNING) {
if (idleState == IDLE) {
IdleManager im = idleManager;
if (im != null) {
logger.finest("waitIfIdle: request IdleManager to abort");
im.requestAbort(this);
} else {
logger.finest("waitIfIdle: abort IDLE");
protocol.idleAbort();
idleState = ABORTING;
}
} else
logger.log(Level.FINEST, "waitIfIdle: idleState {0}", idleState);
try {
// give up lock and wait to be not idle
if (logger.isLoggable(Level.FINEST))
logger.finest("waitIfIdle: wait to be not idle: " +
Thread.currentThread());
messageCacheLock.wait();
if (logger.isLoggable(Level.FINEST))
logger.finest("waitIfIdle: wait done, idleState " +
idleState + ": " + Thread.currentThread());
} catch (InterruptedException ex) {
// restore the interrupted state, which callers might depend on
Thread.currentThread().interrupt();
// If someone is trying to interrupt us we can't keep going
// around the loop waiting for IDLE to complete, but we can't
// just return because callers expect the idleState to be
// RUNNING when we return. Throwing this exception seems
// like the best choice.
throw new ProtocolException("Interrupted waitIfIdle", ex);
}
}
}
/*
* Send the DONE command that aborts the IDLE; used by IdleManager.
*/
void idleAbort() {
synchronized (messageCacheLock) {
if (idleState == IDLE && protocol != null) {
protocol.idleAbort();
idleState = ABORTING;
}
}
}
/*
* Send the DONE command that aborts the IDLE and wait for the response;
* used by IdleManager.
*/
void idleAbortWait() {
synchronized (messageCacheLock) {
if (idleState == IDLE && protocol != null) {
protocol.idleAbort();
idleState = ABORTING;
// read responses until OK or connection failure
try {
for (;;) {
if (!handleIdle(false))
break;
}
} catch (Exception ex) {
// assume it's a connection failure; nothing more to do
logger.log(Level.FINEST, "Exception in idleAbortWait", ex);
}
logger.finest("IDLE aborted");
}
}
}
/**
* Return the SocketChannel for this connection, if any, for use
* in IdleManager.
*/
SocketChannel getChannel() {
return protocol != null ? protocol.getChannel() : null;
}
/**
* Send the IMAP ID command (if supported by the server) and return
* the result from the server. The ID command identfies the client
* to the server and returns information about the server to the client.
* See RFC 2971.
* The returned Map is unmodifiable.
*
* @param clientParams a Map of keys and values identifying the client
* @return a Map of keys and values identifying the server
* @exception MessagingException if the server doesn't support the
* ID extension
* @since JavaMail 1.5.1
*/
@SuppressWarnings("unchecked")
public Map id(final Map clientParams)
throws MessagingException {
checkOpened();
return (Map)doOptionalCommand("ID not supported",
new ProtocolCommand() {
@Override
public Object doCommand(IMAPProtocol p)
throws ProtocolException {
return p.id(clientParams);
}
});
}
/**
* Use the IMAP STATUS command to get the indicated item.
* The STATUS item may be a standard item such as "RECENT" or "UNSEEN",
* or may be a server-specific item.
* The folder must be closed. If the item is not found, or the
* folder is open, -1 is returned.
*
* @param item the STATUS item to fetch
* @return the value of the STATUS item, or -1
* @exception MessagingException for errors
* @since JavaMail 1.5.2
*/
public synchronized long getStatusItem(String item)
throws MessagingException {
if (!opened) {
checkExists();
IMAPProtocol p = null;
Status status = null;
try {
p = getStoreProtocol(); // XXX
String[] items = { item };
status = p.status(fullName, items);
return status != null ? status.getItem(item) : -1;
} catch (BadCommandException bex) {
// doesn't support STATUS, probably vanilla IMAP4 ..
// Could EXAMINE, SEARCH for UNREAD messages and
// return the count .. bah, not worth it.
return -1;
} catch (ConnectionException cex) {
throw new StoreClosedException(store, cex.getMessage());
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
} finally {
releaseStoreProtocol(p);
}
}
return -1;
}
/**
* The response handler. This is the callback routine that is
* invoked by the protocol layer.
*/
/*
* ASSERT: This method must be called only when holding the
* messageCacheLock.
* ASSERT: This method must *not* invoke any other method that
* might grab the 'folder' lock or 'message' lock (i.e., any
* synchronized methods on IMAPFolder or IMAPMessage)
* since that will result in violating the locking hierarchy.
*/
@Override
public void handleResponse(Response r) {
assert Thread.holdsLock(messageCacheLock);
/*
* First, delegate possible ALERT or notification to the Store.
*/
if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE())
((IMAPStore)store).handleResponseCode(r);
/*
* Now check whether this is a BYE or OK response and
* handle appropriately.
*/
if (r.isBYE()) {
if (opened) // XXX - accessed without holding folder lock
cleanup(false);
return;
} else if (r.isOK()) {
// HIGHESTMODSEQ can be updated on any OK response
r.skipSpaces();
if (r.readByte() == '[') {
String s = r.readAtom();
if (s.equalsIgnoreCase("HIGHESTMODSEQ"))
highestmodseq = r.readLong();
}
r.reset();
return;
} else if (!r.isUnTagged()) {
return; // might be a continuation for IDLE
}
/* Now check whether this is an IMAP specific response */
if (!(r instanceof IMAPResponse)) {
// Probably a bug in our code !
// XXX - should be an assert
logger.fine("UNEXPECTED RESPONSE : " + r.toString());
return;
}
IMAPResponse ir = (IMAPResponse)r;
if (ir.keyEquals("EXISTS")) { // EXISTS
int exists = ir.getNumber();
if (exists <= realTotal)
// Could be the EXISTS following EXPUNGE, ignore 'em
return;
int count = exists - realTotal; // number of new messages
Message[] msgs = new Message[count];
// Add 'count' new IMAPMessage objects into the messageCache
messageCache.addMessages(count, realTotal + 1);
int oldtotal = total; // used in loop below
realTotal += count;
total += count;
// avoid instantiating Message objects if no listeners.
if (hasMessageCountListener) {
for (int i = 0; i < count; i++)
msgs[i] = messageCache.getMessage(++oldtotal);
// Notify listeners.
notifyMessageAddedListeners(msgs);
}
} else if (ir.keyEquals("EXPUNGE")) {
// EXPUNGE response.
int seqnum = ir.getNumber();
if (seqnum > realTotal) {
// A message was expunged that we never knew about.
// Exchange will do this. Just ignore the notification.
// (Alternatively, we could simulate an EXISTS for the
// expunged message before expunging it.)
return;
}
Message[] msgs = null;
if (doExpungeNotification && hasMessageCountListener) {
// save the Message object first; can't look it
// up after it's expunged
msgs = new Message[] { getMessageBySeqNumber(seqnum) };
if (msgs[0] == null) // XXX - should never happen
msgs = null;
}
messageCache.expungeMessage(seqnum);
// decrement 'realTotal'; but leave 'total' unchanged
realTotal--;
if (msgs != null) // Do the notification here.
notifyMessageRemovedListeners(false, msgs);
} else if (ir.keyEquals("VANISHED")) {
// after the folder is opened with QRESYNC, a VANISHED response
// without the (EARLIER) tag is used instead of the EXPUNGE
// response
// "VANISHED" SP ["(EARLIER)"] SP known-uids
String[] s = ir.readAtomStringList();
if (s == null) { // no (EARLIER)
String uids = ir.readAtom();
UIDSet[] uidset = UIDSet.parseUIDSets(uids);
// assume no duplicates and no UIDs out of range
realTotal -= UIDSet.size(uidset);
long[] luid = UIDSet.toArray(uidset);
Message[] msgs = createMessagesForUIDs(luid);
for (Message m : msgs) {
if (m.getMessageNumber() > 0)
messageCache.expungeMessage(m.getMessageNumber());
}
if (doExpungeNotification && hasMessageCountListener) {
notifyMessageRemovedListeners(true, msgs);
}
} // else if (EARLIER), ignore
} else if (ir.keyEquals("FETCH")) {
assert ir instanceof FetchResponse : "!ir instanceof FetchResponse";
Message msg = processFetchResponse((FetchResponse)ir);
if (msg != null)
notifyMessageChangedListeners(
MessageChangedEvent.FLAGS_CHANGED, msg);
} else if (ir.keyEquals("RECENT")) {
// update 'recent'
recent = ir.getNumber();
}
}
/**
* Process a FETCH response.
* The only unsolicited FETCH response that makes sense
* to me (for now) is FLAGS updates, which might include
* UID and MODSEQ information. Ignore any other junk.
*/
private Message processFetchResponse(FetchResponse fr) {
IMAPMessage msg = getMessageBySeqNumber(fr.getNumber());
if (msg != null) { // should always be true
boolean notify = false;
UID uid = fr.getItem(UID.class);
if (uid != null && msg.getUID() != uid.uid) {
msg.setUID(uid.uid);
if (uidTable == null)
uidTable = new Hashtable<>();
uidTable.put(Long.valueOf(uid.uid), msg);
notify = true;
}
MODSEQ modseq = fr.getItem(MODSEQ.class);
if (modseq != null && msg._getModSeq() != modseq.modseq) {
msg.setModSeq(modseq.modseq);
/*
* XXX - should we update the folder's HIGHESTMODSEQ or not?
*
if (modseq.modseq > highestmodseq)
highestmodseq = modseq.modseq;
*/
notify = true;
}
// Get FLAGS response, if present
FLAGS flags = fr.getItem(FLAGS.class);
if (flags != null) {
msg._setFlags(flags); // assume flags changed
notify = true;
}
// handle any extension items that might've changed
// XXX - no notifications associated with extension items
msg.handleExtensionFetchItems(fr.getExtensionItems());
if (!notify)
msg = null;
}
return msg;
}
/**
* Handle the given array of Responses.
*
* ASSERT: This method must be called only when holding the
* messageCacheLock
*/
void handleResponses(Response[] r) {
for (int i = 0; i < r.length; i++) {
if (r[i] != null)
handleResponse(r[i]);
}
}
/**
* Get this folder's Store's protocol connection.
*
* When acquiring a store protocol object, it is important to
* use the following steps:
*
*
* IMAPProtocol p = null;
* try {
* p = getStoreProtocol();
* // perform the command
* } catch (WhateverException ex) {
* // handle it
* } finally {
* releaseStoreProtocol(p);
* }
*
*
* ASSERT: Must be called with this folder's synchronization lock held.
*
* @return the IMAPProtocol for the Store's connection
* @exception ProtocolException for protocol errors
*/
protected synchronized IMAPProtocol getStoreProtocol()
throws ProtocolException {
connectionPoolLogger.fine("getStoreProtocol() borrowing a connection");
return ((IMAPStore)store).getFolderStoreProtocol();
}
/**
* Throw the appropriate 'closed' exception.
*
* @param cex the ConnectionException
* @exception FolderClosedException if the folder is closed
* @exception StoreClosedException if the store is closed
*/
protected synchronized void throwClosedException(ConnectionException cex)
throws FolderClosedException, StoreClosedException {
// If it's the folder's protocol object, throw a FolderClosedException;
// otherwise, throw a StoreClosedException.
// If a command has failed because the connection is closed,
// the folder will have already been forced closed by the
// time we get here and our protocol object will have been
// released, so if we no longer have a protocol object we base
// this decision on whether we *think* the folder is open.
if ((protocol != null && cex.getProtocol() == protocol) ||
(protocol == null && !reallyClosed))
throw new FolderClosedException(this, cex.getMessage());
else
throw new StoreClosedException(store, cex.getMessage());
}
/**
* Return the IMAPProtocol object for this folder.
*
* This method will block if necessary to wait for an IDLE
* command to finish.
*
* @return the IMAPProtocol object used when the folder is open
* @exception ProtocolException for protocol errors
*/
protected IMAPProtocol getProtocol() throws ProtocolException {
assert Thread.holdsLock(messageCacheLock);
waitIfIdle();
// if we no longer have a protocol object after waiting, it probably
// means the connection has been closed due to a communnication error,
// or possibly because the folder has been closed
if (protocol == null)
throw new ConnectionException("Connection closed");
return protocol;
}
/**
* A simple interface for user-defined IMAP protocol commands.
*/
public static interface ProtocolCommand {
/**
* Execute the user-defined command using the supplied IMAPProtocol
* object.
*
* @param protocol the IMAPProtocol for the connection
* @return the results of the command
* @exception ProtocolException for protocol errors
*/
public Object doCommand(IMAPProtocol protocol) throws ProtocolException;
}
/**
* Execute a user-supplied IMAP command. The command is executed
* in the appropriate context with the necessary locks held and
* using the appropriate IMAPProtocol
object.
*
* This method returns whatever the ProtocolCommand
* object's doCommand
method returns. If the
* doCommand
method throws a ConnectionException
* it is translated into a StoreClosedException
or
* FolderClosedException
as appropriate. If the
* doCommand
method throws a ProtocolException
* it is translated into a MessagingException
.
*
* The following example shows how to execute the IMAP NOOP command.
* Executing more complex IMAP commands requires intimate knowledge
* of the com.sun.mail.iap
and
* com.sun.mail.imap.protocol
packages, best acquired by
* reading the source code.
*
*
* import com.sun.mail.iap.*;
* import com.sun.mail.imap.*;
* import com.sun.mail.imap.protocol.*;
*
* ...
*
* IMAPFolder f = (IMAPFolder)folder;
* Object val = f.doCommand(new IMAPFolder.ProtocolCommand() {
* public Object doCommand(IMAPProtocol p)
* throws ProtocolException {
* p.simpleCommand("NOOP", null);
* return null;
* }
* });
*
*
*
* Here's a more complex example showing how to use the proposed
* IMAP SORT extension:
*
*
* import com.sun.mail.iap.*;
* import com.sun.mail.imap.*;
* import com.sun.mail.imap.protocol.*;
*
* ...
*
* IMAPFolder f = (IMAPFolder)folder;
* Object val = f.doCommand(new IMAPFolder.ProtocolCommand() {
* public Object doCommand(IMAPProtocol p)
* throws ProtocolException {
* // Issue command
* Argument args = new Argument();
* Argument list = new Argument();
* list.writeString("SUBJECT");
* args.writeArgument(list);
* args.writeString("UTF-8");
* args.writeString("ALL");
* Response[] r = p.command("SORT", args);
* Response response = r[r.length-1];
*
* // Grab response
* Vector v = new Vector();
* if (response.isOK()) { // command succesful
* for (int i = 0, len = r.length; i < len; i++) {
* if (!(r[i] instanceof IMAPResponse))
* continue;
*
* IMAPResponse ir = (IMAPResponse)r[i];
* if (ir.keyEquals("SORT")) {
* String num;
* while ((num = ir.readAtomString()) != null)
* System.out.println(num);
* r[i] = null;
* }
* }
* }
*
* // dispatch remaining untagged responses
* p.notifyResponseHandlers(r);
* p.handleResult(response);
*
* return null;
* }
* });
*
*
* @param cmd the protocol command
* @return the result of the command
* @exception MessagingException for failures
*/
public Object doCommand(ProtocolCommand cmd) throws MessagingException {
try {
return doProtocolCommand(cmd);
} catch (ConnectionException cex) {
// Oops, the store or folder died on us.
throwClosedException(cex);
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
return null;
}
public Object doOptionalCommand(String err, ProtocolCommand cmd)
throws MessagingException {
try {
return doProtocolCommand(cmd);
} catch (BadCommandException bex) {
throw new MessagingException(err, bex);
} catch (ConnectionException cex) {
// Oops, the store or folder died on us.
throwClosedException(cex);
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
return null;
}
public Object doCommandIgnoreFailure(ProtocolCommand cmd)
throws MessagingException {
try {
return doProtocolCommand(cmd);
} catch (CommandFailedException cfx) {
return null;
} catch (ConnectionException cex) {
// Oops, the store or folder died on us.
throwClosedException(cex);
} catch (ProtocolException pex) {
throw new MessagingException(pex.getMessage(), pex);
}
return null;
}
protected synchronized Object doProtocolCommand(ProtocolCommand cmd)
throws ProtocolException {
/*
* Check whether we have a protocol object, not whether we're
* opened, to allow use of the exsting protocol object in the
* open method before the state is changed to "opened".
*/
if (protocol != null) {
synchronized (messageCacheLock) {
return cmd.doCommand(getProtocol());
}
}
// only get here if using store's connection
IMAPProtocol p = null;
try {
p = getStoreProtocol();
return cmd.doCommand(p);
} finally {
releaseStoreProtocol(p);
}
}
/**
* Release the store protocol object. If we borrowed a protocol
* object from the connection pool, give it back. If we used our
* own protocol object, nothing to do.
*
* ASSERT: Must be called with this folder's synchronization lock held.
*
* @param p the IMAPProtocol object
*/
protected synchronized void releaseStoreProtocol(IMAPProtocol p) {
if (p != protocol)
((IMAPStore)store).releaseFolderStoreProtocol(p);
else {
// XXX - should never happen
logger.fine("releasing our protocol as store protocol?");
}
}
/**
* Release the protocol object.
*
* ASSERT: This method must be called only when holding the
* messageCacheLock
*
* @param returnToPool return the protocol object to the pool?
*/
protected void releaseProtocol(boolean returnToPool) {
if (protocol != null) {
protocol.removeResponseHandler(this);
if (returnToPool)
((IMAPStore)store).releaseProtocol(this, protocol);
else {
protocol.disconnect(); // make sure it's disconnected
((IMAPStore)store).releaseProtocol(this, null);
}
protocol = null;
}
}
/**
* Issue a noop command for the connection if the connection has not been
* used in more than a second. If keepStoreAlive
is true,
* also issue a noop over the store's connection.
*
* ASSERT: This method must be called only when holding the
* messageCacheLock
*
* @param keepStoreAlive keep the Store alive too?
* @exception ProtocolException for protocol errors
*/
protected void keepConnectionAlive(boolean keepStoreAlive)
throws ProtocolException {
assert Thread.holdsLock(messageCacheLock);
if (protocol == null) // in case connection was closed
return;
if (System.currentTimeMillis() - protocol.getTimestamp() > 1000) {
waitIfIdle();
if (protocol != null)
protocol.noop();
}
if (keepStoreAlive && ((IMAPStore)store).hasSeparateStoreConnection()) {
IMAPProtocol p = null;
try {
p = ((IMAPStore)store).getFolderStoreProtocol();
if (System.currentTimeMillis() - p.getTimestamp() > 1000)
p.noop();
} finally {
((IMAPStore)store).releaseFolderStoreProtocol(p);
}
}
}
/**
* Get the message object for the given sequence number. If
* none found, null is returned.
*
* ASSERT: This method must be called only when holding the
* messageCacheLock
*
* @param seqnum the message sequence number
* @return the IMAPMessage object
*/
protected IMAPMessage getMessageBySeqNumber(int seqnum) {
if (seqnum > messageCache.size()) {
// Microsoft Exchange will sometimes return message
// numbers that it has not yet notified the client
// about via EXISTS; ignore those messages here.
// GoDaddy IMAP does this too.
if (logger.isLoggable(Level.FINE))
logger.fine("ignoring message number " +
seqnum + " outside range " + messageCache.size());
return null;
}
return messageCache.getMessageBySeqnum(seqnum);
}
/**
* Get the message objects for the given sequence numbers.
*
* ASSERT: This method must be called only when holding the
* messageCacheLock
*
* @param seqnums the array of message sequence numbers
* @return the IMAPMessage objects
* @since JavaMail 1.5.3
*/
protected IMAPMessage[] getMessagesBySeqNumbers(int[] seqnums) {
IMAPMessage[] msgs = new IMAPMessage[seqnums.length];
int nulls = 0;
// Map seq-numbers into actual Messages.
for (int i = 0; i < seqnums.length; i++) {
msgs[i] = getMessageBySeqNumber(seqnums[i]);
if (msgs[i] == null)
nulls++;
}
if (nulls > 0) { // compress the array to remove the nulls
IMAPMessage[] nmsgs = new IMAPMessage[seqnums.length - nulls];
for (int i = 0, j = 0; i < msgs.length; i++) {
if (msgs[i] != null)
nmsgs[j++] = msgs[i];
}
msgs = nmsgs;
}
return msgs;
}
private boolean isDirectory() {
return ((type & HOLDS_FOLDERS) != 0);
}
}
/**
* An object that holds a Message object
* and reports its size and writes it to another OutputStream
* on demand. Used by appendMessages to avoid the need to
* buffer the entire message in memory in a single byte array
* before sending it to the server.
*/
class MessageLiteral implements Literal {
private Message msg;
private int msgSize = -1;
private byte[] buf; // the buffered message, if not null
public MessageLiteral(Message msg, int maxsize)
throws MessagingException, IOException {
this.msg = msg;
// compute the size here so exceptions can be returned immediately
LengthCounter lc = new LengthCounter(maxsize);
OutputStream os = new CRLFOutputStream(lc);
msg.writeTo(os);
os.flush();
msgSize = lc.getSize();
buf = lc.getBytes();
}
@Override
public int size() {
return msgSize;
}
@Override
public void writeTo(OutputStream os) throws IOException {
// the message should not change between the constructor and this call
try {
if (buf != null)
os.write(buf, 0, msgSize);
else {
os = new CRLFOutputStream(os);
msg.writeTo(os);
}
} catch (MessagingException mex) {
// exceptions here are bad, "should" never happen
throw new IOException("MessagingException while appending message: "
+ mex);
}
}
}
/**
* Count the number of bytes written to the stream.
* Also, save a copy of small messages to avoid having to process
* the data again.
*/
class LengthCounter extends OutputStream {
private int size = 0;
private byte[] buf;
private int maxsize;
public LengthCounter(int maxsize) {
buf = new byte[8192];
this.maxsize = maxsize;
}
@Override
public void write(int b) {
int newsize = size + 1;
if (buf != null) {
if (newsize > maxsize && maxsize >= 0) {
buf = null;
} else if (newsize > buf.length) {
byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)];
System.arraycopy(buf, 0, newbuf, 0, size);
buf = newbuf;
buf[size] = (byte)b;
} else {
buf[size] = (byte)b;
}
}
size = newsize;
}
@Override
public void write(byte b[], int off, int len) {
if ((off < 0) || (off > b.length) || (len < 0) ||
((off + len) > b.length) || ((off + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
int newsize = size + len;
if (buf != null) {
if (newsize > maxsize && maxsize >= 0) {
buf = null;
} else if (newsize > buf.length) {
byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)];
System.arraycopy(buf, 0, newbuf, 0, size);
buf = newbuf;
System.arraycopy(b, off, buf, size, len);
} else {
System.arraycopy(b, off, buf, size, len);
}
}
size = newsize;
}
@Override
public void write(byte[] b) throws IOException {
write(b, 0, b.length);
}
public int getSize() {
return size;
}
public byte[] getBytes() {
return buf;
}
}