You are viewing a plain text version of this content. The canonical link for it is here.
Posted to scm@geronimo.apache.org by jl...@apache.org on 2020/10/07 15:58:41 UTC

svn commit: r1882306 [7/17] - in /geronimo/javamail/trunk/geronimo-javamail_1.6: ./ geronimo-javamail_1.6_mail/ geronimo-javamail_1.6_mail/src/ geronimo-javamail_1.6_mail/src/site/ geronimo-javamail_1.6_provider/ geronimo-javamail_1.6_provider/src/ ger...

Added: geronimo/javamail/trunk/geronimo-javamail_1.6/geronimo-javamail_1.6_provider/src/main/java/org/apache/geronimo/javamail/store/imap/connection/IMAPConnection.java
URL: http://svn.apache.org/viewvc/geronimo/javamail/trunk/geronimo-javamail_1.6/geronimo-javamail_1.6_provider/src/main/java/org/apache/geronimo/javamail/store/imap/connection/IMAPConnection.java?rev=1882306&view=auto
==============================================================================
--- geronimo/javamail/trunk/geronimo-javamail_1.6/geronimo-javamail_1.6_provider/src/main/java/org/apache/geronimo/javamail/store/imap/connection/IMAPConnection.java (added)
+++ geronimo/javamail/trunk/geronimo-javamail_1.6/geronimo-javamail_1.6_provider/src/main/java/org/apache/geronimo/javamail/store/imap/connection/IMAPConnection.java Wed Oct  7 15:58:39 2020
@@ -0,0 +1,2021 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.geronimo.javamail.store.imap.connection;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.io.UnsupportedEncodingException;
+import java.io.Writer;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.StringTokenizer;
+
+import javax.mail.Address;
+import javax.mail.AuthenticationFailedException;
+import javax.mail.FetchProfile;
+import javax.mail.Flags;
+import javax.mail.Folder;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.MethodNotSupportedException;
+import javax.mail.Quota;
+import javax.mail.Session;
+import javax.mail.UIDFolder;
+import javax.mail.URLName;
+
+import javax.mail.internet.InternetHeaders;
+
+import javax.mail.search.SearchTerm;
+
+import org.apache.geronimo.javamail.authentication.AuthenticatorFactory;
+import org.apache.geronimo.javamail.authentication.ClientAuthenticator;
+import org.apache.geronimo.javamail.authentication.LoginAuthenticator;
+import org.apache.geronimo.javamail.authentication.PlainAuthenticator;
+import org.apache.geronimo.javamail.store.imap.ACL;
+import org.apache.geronimo.javamail.store.imap.Rights;
+
+import org.apache.geronimo.javamail.util.CommandFailedException;
+import org.apache.geronimo.javamail.util.InvalidCommandException;
+import org.apache.geronimo.javamail.util.MailConnection;
+import org.apache.geronimo.javamail.util.ProtocolProperties;
+import org.apache.geronimo.javamail.util.TraceInputStream;
+import org.apache.geronimo.javamail.util.TraceOutputStream;
+import org.apache.geronimo.mail.util.Base64;
+
+/**
+ * Simple implementation of IMAP transport.  Just does plain RFC977-ish
+ * delivery.
+ * <p/>
+ * There is no way to indicate failure for a given recipient (it's possible to have a
+ * recipient address rejected).  The sun impl throws exceptions even if others successful),
+ * but maybe we do a different way...
+ * <p/>
+ *
+ * @version $Rev$ $Date$
+ */
+public class IMAPConnection extends MailConnection {
+
+    protected static final String CAPABILITY_LOGIN_DISABLED = "LOGINDISABLED";
+
+    // The connection pool we're a member of.  This keeps holds most of the
+    // connnection parameter information for us.
+    protected IMAPConnectionPool pool;
+
+    // special input stream for reading individual response lines.
+    protected IMAPResponseStream reader;
+
+    // connection pool connections.
+    protected long lastAccess = 0;
+    // our handlers for any untagged responses
+    protected LinkedList responseHandlers = new LinkedList();
+    // the list of queued untagged responses.
+    protected List queuedResponses = new LinkedList();
+    // this is set on if we had a forced disconnect situation from
+    // the server.
+    protected boolean closed = false;
+
+    /**
+     * Normal constructor for an IMAPConnection() object.
+     *
+     * @param props  The protocol properties abstraction containing our
+     *               property modifiers.
+     * @param pool
+     */
+    public IMAPConnection(ProtocolProperties props, IMAPConnectionPool pool) {
+        super(props);
+        this.pool = pool;
+    }
+
+
+    /**
+     * Connect to the server and do the initial handshaking.
+     *
+     * @exception MessagingException
+     */
+    public boolean protocolConnect(String host, int port, String authid, String realm, String username, String password) throws MessagingException {
+        this.serverHost = host;
+        this.serverPort = port;
+        this.realm = realm;
+        this.authid = authid;
+        this.username = username;
+        this.password = password;
+
+        boolean preAuthorized = false;
+
+        try {
+            // create socket and connect to server.
+            getConnection();
+
+            // we need to ask the server what its capabilities are.  This can be done
+            // before we login.
+            getCapability();
+            // do a preauthoriziation check.
+            if (extractResponse("PREAUTH") != null) {
+                preAuthorized = true;
+            }
+
+            // make sure we process these now
+            processPendingResponses();
+            
+            boolean requireTLS = props.getBooleanProperty(MAIL_STARTTLS_REQUIRED, false);
+            boolean enableTLS = props.getBooleanProperty(MAIL_STARTTLS_ENABLE, false);
+            boolean serverSupportsTLS = hasCapability(CAPABILITY_STARTTLS);
+
+            // if we're not already using an SSL connection, and we have permission to issue STARTTLS or its even required
+            // try to setup a SSL connection
+            if (!sslConnection && (enableTLS || requireTLS)) {
+                
+                //if the server does not support TLS check if its required.
+                //If true then throw an error, if not establish a non SSL connection
+                if(requireTLS && !serverSupportsTLS) {
+                    throw new MessagingException("Server doesn't support required transport level security");
+                } else if (serverSupportsTLS){
+                    // tell the server of our intention to start a TLS session
+                    sendSimpleCommand("STARTTLS");
+    
+                    // The connection is then handled by the superclass level.
+                    getConnectedTLSSocket();
+    
+                    // create the special reader for pulling the responses.
+                    reader = new IMAPResponseStream(inputStream);
+    
+                    // the IMAP spec states that the capability response is independent of login state or
+                    // user, but I'm not sure I believe that to be the case.  It doesn't hurt to refresh
+                    // the information again after establishing a secure connection.
+                    getCapability();
+                    // and we need to repeat this check.
+                    if (extractResponse("PREAUTH") != null) {
+                        preAuthorized = true;
+                    }
+                } else {
+                    if (debug) {
+                        debugOut("STARTTLS is enabled but not required and server does not support it. So we establish a connection without transport level security");
+                    }
+                }
+                
+            }
+
+            // damn, no login required.
+            if (preAuthorized) {
+                return true;
+            }
+
+            // go login with the server
+            return login();
+        } catch (IOException e) {
+            if (debug) {
+                debugOut("I/O exception establishing connection", e);
+            }
+            throw new MessagingException("Connection error", e);
+        }
+        finally {
+            // make sure the queue is cleared
+            processPendingResponses();
+        }
+    }
+
+    /**
+     * Update the last access time for the connection.
+     */
+    protected void updateLastAccess() {
+        lastAccess = System.currentTimeMillis();
+    }
+
+    /**
+     * Test if the connection has been sitting idle for longer than
+     * the set timeout period.
+     *
+     * @param timeout The allowed "freshness" interval.
+     *
+     * @return True if the connection has been active within the required
+     *         interval, false if it has been sitting idle for too long.
+     */
+    public boolean isStale(long timeout) {
+        return (System.currentTimeMillis() - lastAccess) > timeout;
+    }
+
+
+    /**
+     * Close the connection.  On completion, we'll be disconnected from
+     * the server and unable to send more data.
+     *
+     * @exception MessagingException
+     */
+    public void close() throws MessagingException {
+        // if we're already closed, get outta here.
+        if (socket == null) {
+            return;
+        }
+        try {
+            // say goodbye
+            logout();
+        } finally {
+            // and close up the connection.  We do this in a finally block to make sure the connection
+            // is shut down even if quit gets an error.
+            closeServerConnection();
+            // get rid of our response processor too.
+            reader = null;
+        }
+    }
+
+
+    /**
+     * Create a transport connection object and connect it to the
+     * target server.
+     *
+     * @exception MessagingException
+     */
+    protected void getConnection() throws IOException, MessagingException
+    {
+        // do all of the non-protocol specific set up.  This will get our socket established
+        // and ready use.
+        super.getConnection();
+        // create the special reader for pulling the responses.
+        reader = new IMAPResponseStream(inputStream);
+
+        // set the initial access time stamp
+        updateLastAccess();
+    }
+
+
+    /**
+     * Process a simple command/response sequence between the
+     * client and the server.  These are commands where the
+     * client is expecting them to "just work", and also will not
+     * directly process the reply information.  Unsolicited untagged
+     * responses are dispatched to handlers, and a MessagingException
+     * will be thrown for any non-OK responses from the server.
+     *
+     * @param data   The command data we're writing out.
+     *
+     * @exception MessagingException
+     */
+    public void sendSimpleCommand(String data) throws MessagingException {
+        // create a command object and issue the command with that.
+        IMAPCommand command = new IMAPCommand(data);
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Process a simple command/response sequence between the
+     * client and the server.  These are commands where the
+     * client is expecting them to "just work", and also will not
+     * directly process the reply information.  Unsolicited untagged
+     * responses are dispatched to handlers, and a MessagingException
+     * will be thrown for any non-OK responses from the server.
+     *
+     * @param data   The command data we're writing out.
+     *
+     * @exception MessagingException
+     */
+    public void sendSimpleCommand(IMAPCommand data) throws MessagingException {
+        // the command sending process will raise exceptions for bad responses....
+        // we just need to send the command and forget about it.
+        sendCommand(data);
+    }
+
+
+    /**
+     * Sends a  command down the socket, returning the server response.
+     *
+     * @param data   The String form of the command.
+     *
+     * @return The tagged response information that terminates the command interaction.
+     * @exception MessagingException
+     */
+    public IMAPTaggedResponse sendCommand(String data) throws MessagingException {
+        IMAPCommand command = new IMAPCommand(data);
+        return sendCommand(command);
+    }
+
+
+    /**
+     * Sends a  command down the socket, returning the server response.
+     *
+     * @param data   An IMAPCommand object with the prepared command information.
+     *
+     * @return The tagged (or continuation) response information that terminates the
+     *         command response sequence.
+     * @exception MessagingException
+     */
+    public synchronized IMAPTaggedResponse sendCommand(IMAPCommand data) throws MessagingException {
+        // check first
+        checkConnected();
+        try {
+            // have the command write the command data.  This also prepends a tag.
+            data.writeTo(outputStream, this);
+            outputStream.flush();
+            // update the activity timestamp
+            updateLastAccess();
+            // get the received response
+            return receiveResponse();
+        } catch (IOException e) {
+            throw new MessagingException(e.toString(), e);
+        }
+    }
+
+
+    /**
+     * Sends a  message down the socket and terminates with the
+     * appropriate CRLF
+     *
+     * @param data   The string data to send.
+     *
+     * @return An IMAPTaggedResponse item returned from the server.
+     * @exception MessagingException
+     */
+    public IMAPTaggedResponse sendLine(String data) throws MessagingException {
+        try {
+            return sendLine(data.getBytes("ISO8859-1"));
+        } catch (UnsupportedEncodingException e) {
+            // should never happen
+            return null;
+        }
+    }
+
+
+    /**
+     * Sends a  message down the socket and terminates with the
+     * appropriate CRLF
+     *
+     * @param data   The array of data to send to the server.
+     *
+     * @return The response item returned from the IMAP server.
+     * @exception MessagingException
+     */
+    public IMAPTaggedResponse sendLine(byte[] data) throws MessagingException {
+        return sendLine(data, 0, data.length);
+    }
+
+
+    /**
+     * Sends a  message down the socket and terminates with the
+     * appropriate CRLF
+     *
+     * @param data   The source data array.
+     * @param offset The offset within the data array.
+     * @param length The length of data to send.
+     *
+     * @return The response line returned from the IMAP server.
+     * @exception MessagingException
+     */
+    public synchronized IMAPTaggedResponse sendLine(byte[] data, int offset, int length) throws MessagingException {
+        // check first
+        checkConnected();
+
+        try {
+            outputStream.write(data, offset, length);
+            outputStream.write(CR);
+            outputStream.write(LF);
+            outputStream.flush();
+            // update the activity timestamp
+            updateLastAccess();
+            return receiveResponse();
+        } catch (IOException e) {
+            throw new MessagingException(e.toString(), e);
+        }
+    }
+
+
+    /**
+     * Get a reply line for an IMAP command.
+     *
+     * @return An IMAP reply object from the stream.
+     */
+    public IMAPTaggedResponse receiveResponse() throws MessagingException {
+        while (true) {
+            // read and parse a response from the server.
+            IMAPResponse response = reader.readResponse();
+            // The response set is terminated by either a continuation response or a
+            // tagged response (we only have a single command active at one time).
+            if (response instanceof IMAPTaggedResponse) {
+                // update the access time stamp for later timeout processing.
+                updateLastAccess();
+                IMAPTaggedResponse tagged = (IMAPTaggedResponse)response;
+                // we turn these into exceptions here, which means the issuer doesn't have to
+                // worry about checking status.
+                if (tagged.isBAD()) {
+                    throw new InvalidCommandException("Unexpected command IMAP command error");
+                }
+                else if (tagged.isNO()) {
+                    throw new CommandFailedException("Unexpected error executing IMAP command");
+                }
+                return tagged;
+            }
+            else {
+                // all other unsolicited responses are either async status updates or
+                // additional elements of a command we just sent.  These will be processed
+                // either during processing of the command response, or at the end of the
+                // current command processing.
+                queuePendingResponse((IMAPUntaggedResponse)response);
+            }
+        }
+    }
+
+
+    /**
+     * Get the servers capabilities from the wire....
+     */
+    public void getCapability() throws MessagingException {
+        sendCommand("CAPABILITY");
+        // get the capabilities from the response.
+        IMAPCapabilityResponse response = (IMAPCapabilityResponse)extractResponse("CAPABILITY");
+        capabilities = response.getCapabilities();
+        authentications = response.getAuthentications();
+    }
+
+    /**
+     * Logs out from the server.
+     */
+    public void logout() throws MessagingException {
+        // We can just send the command and generally ignore the
+        // status response.
+        sendCommand("LOGOUT");
+    }
+
+    /**
+     * Deselect a mailbox when a folder returns a connection.
+     *
+     * @exception MessagingException
+     */
+    public void closeMailbox() throws MessagingException {
+        // We can just send the command and generally ignore the
+        // status response.
+        sendCommand("CLOSE");
+    }
+
+
+    /**
+     * Authenticate with the server, if necessary (or possible).
+     *
+     * @return true if we were able to authenticate correctly, false for authentication failures.
+     * @exception MessagingException
+     */
+    protected boolean login() throws MessagingException
+    {
+        // if no username or password, fail this immediately.
+        // the base connect property should resolve a username/password combo for us and
+        // try again.
+        if (username == null || password == null) {
+            return false;
+        }
+
+        // are we permitted to use SASL mechanisms?
+        if (props.getBooleanProperty(MAIL_SASL_ENABLE, false)) {
+            // we might be enable for SASL, but the client and the server might
+            // not have any supported mechanisms in common.  Try again with another
+            // mechanism.
+
+            //We obtain only the SASL mechanisms to use
+            final List saslMechanisms = getSaslMechanisms();
+
+            if (saslMechanisms != null && saslMechanisms.contains(AUTHENTICATION_XOAUTH2)) {
+                if (processOauthAuthentication()) {
+                    return true;
+                }
+            }
+
+            if (processSaslAuthentication()) {
+                return true;
+            }
+        }
+
+        // see if we're allowed to try plain.
+        if (!props.getBooleanProperty(MAIL_PLAIN_DISABLE, false) && supportsMechanism(AUTHENTICATION_PLAIN)) {
+            return processPlainAuthentication();
+        }
+
+        // see if we're allowed to try login.
+        if (!props.getBooleanProperty(MAIL_LOGIN_DISABLE, false) && supportsMechanism(AUTHENTICATION_LOGIN)) {
+            // no authzid capability with this authentication method.
+            return processLoginAuthentication();
+        }
+
+        // the server can choose to disable the LOGIN command.  If not disabled, try
+        // using LOGIN rather than AUTHENTICATE.
+        if (!hasCapability(CAPABILITY_LOGIN_DISABLED)) {
+            return processLogin();
+        }
+
+        throw new MessagingException("No supported LOGIN methods enabled");
+    }
+
+    /**
+     * Process XOAUTH2-type authentication.
+     * @return Returns true if the server support a XOAUTH2 authentication mechanism and
+     *         accepted response challenges.
+     * @throws MessagingException
+     */
+    protected boolean processOauthAuthentication() throws MessagingException {
+        ClientAuthenticator authenticator = getOauthAuthenticator();
+        if (authenticator == null) {
+            return false;
+        }
+
+        // go process the login.
+        return processLogin(authenticator);
+    }
+
+    protected ClientAuthenticator getOauthAuthenticator() {
+        return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
+    }
+
+    /**
+     * Process SASL-type authentication.
+     *
+     * @return Returns true if the server support a SASL authentication mechanism and
+     * accepted reponse challenges.
+     * @exception MessagingException
+     */
+    protected boolean processSaslAuthentication() throws MessagingException {
+        // if unable to get an appropriate authenticator, just fail it.
+        ClientAuthenticator authenticator = getSaslAuthenticator();
+        if (authenticator == null) {
+            return false;
+        }
+
+        // go process the login.
+        return processLogin(authenticator);
+    }
+
+    protected ClientAuthenticator getSaslAuthenticator() {
+        return AuthenticatorFactory.getAuthenticator(props, selectSaslMechanisms(), serverHost, username, password, authid, realm);
+    }
+
+    /**
+     * Process SASL-type PLAIN authentication.
+     *
+     * @return Returns true if the login is accepted.
+     * @exception MessagingException
+     */
+    protected boolean processPlainAuthentication() throws MessagingException {
+        // go process the login.
+        return processLogin(new PlainAuthenticator(authid, username, password));
+    }
+
+
+    /**
+     * Process SASL-type LOGIN authentication.
+     *
+     * @return Returns true if the login is accepted.
+     * @exception MessagingException
+     */
+    protected boolean processLoginAuthentication() throws MessagingException {
+        // go process the login.
+        return processLogin(new LoginAuthenticator(username, password));
+    }
+
+
+    /**
+     * Process a LOGIN using the LOGIN command instead of AUTHENTICATE.
+     *
+     * @return true if the command succeeded, false for any authentication failures.
+     * @exception MessagingException
+     */
+    protected boolean processLogin() throws MessagingException {
+        // arguments are "LOGIN userid password"
+        IMAPCommand command = new IMAPCommand("LOGIN");
+        command.appendAtom(username);
+        command.appendAtom(password);
+
+        // go issue the command
+        try {
+            sendCommand(command);
+        } catch (CommandFailedException e) {
+            // we'll get a NO response for a rejected login
+            return false;
+        }
+        // seemed to work ok....
+        return true;
+    }
+
+
+    /**
+     * Process a login using the provided authenticator object.
+     *
+     * NB:  This method is synchronized because we have a multi-step process going on
+     * here.  No other commands should be sent to the server until we complete.
+     *
+     * @return Returns true if the server support a SASL authentication mechanism and
+     * accepted reponse challenges.
+     * @exception MessagingException
+     */
+    protected synchronized boolean processLogin(ClientAuthenticator authenticator) throws MessagingException {
+        if (debug) {
+            debugOut("Authenticating for user: " + username + " using " + authenticator.getMechanismName());
+        }
+
+        IMAPCommand command = new IMAPCommand("AUTHENTICATE");
+        // and tell the server which mechanism we're using.
+        command.appendAtom(authenticator.getMechanismName());
+        // send the command now
+
+        try {
+            IMAPTaggedResponse response = sendCommand(command);
+
+            // now process the challenge sequence.  We get a 235 response back when the server accepts the
+            // authentication, and a 334 indicates we have an additional challenge.
+            while (true) {
+                // this should be a continuation reply, if things are still good.
+                if (response.isContinuation()) {
+                    // we're passed back a challenge value, Base64 encoded.
+                    byte[] challenge = response.decodeChallengeResponse();
+
+                    // have the authenticator evaluate and send back the encoded response.
+                    response = sendLine(Base64.encode(authenticator.evaluateChallenge(challenge)));
+                }
+                else {
+                    // there are only two choices here, OK or a continuation.  OK means
+                    // we've passed muster and are in.
+                    return true;
+                }
+            }
+        } catch (CommandFailedException e ) {
+            // a failure at any point in this process will result in a "NO" response.
+            // That causes an exception to get thrown, so just fail the login
+            // if we get one.
+            return false;
+        }
+    }
+
+
+    /**
+     * Return the server host for this connection.
+     *
+     * @return The String name of the server host.
+     */
+    public String getHost() {
+        return serverHost;
+    }
+
+
+    /**
+     * Attach a handler for untagged responses to this connection.
+     *
+     * @param h      The new untagged response handler.
+     */
+    public synchronized void addResponseHandler(IMAPUntaggedResponseHandler h) {
+        responseHandlers.add(h);
+    }
+
+
+    /**
+     * Remove a response handler from the connection.
+     *
+     * @param h      The handler to remove.
+     */
+    public synchronized void removeResponseHandler(IMAPUntaggedResponseHandler h) {
+        responseHandlers.remove(h);
+    }
+
+
+    /**
+     * Add a response to the pending untagged response queue.
+     *
+     * @param response The response to add.
+     */
+    public synchronized void queuePendingResponse(IMAPUntaggedResponse response) {
+        queuedResponses.add(response);
+    }
+
+    /**
+     * Process any untagged responses in the queue.  This will clear out
+     * the queue, and send each response to the registered
+     * untagged response handlers.
+     */
+    public void processPendingResponses() throws MessagingException {
+        List pendingResponses = null;
+        List handlerList = null;
+
+        synchronized(this) {
+            if (queuedResponses.isEmpty()) {
+                return;
+            }
+            pendingResponses = queuedResponses;
+            queuedResponses = new LinkedList();
+            // get a copy of the response handlers so we can
+            // release the connection lock before broadcasting
+            handlerList = (List)responseHandlers.clone();
+        }
+
+        for (int i = 0; i < pendingResponses.size(); i++) {
+            IMAPUntaggedResponse response = (IMAPUntaggedResponse)pendingResponses.get(i);
+            for (int j = 0; j < handlerList.size(); j++) {
+                // broadcast to each handler.  If a handler returns true, then it
+                // handled whatever this message required and we should skip sending
+                // it to other handlers.
+                IMAPUntaggedResponseHandler h = (IMAPUntaggedResponseHandler)handlerList.get(j);
+                if (h.handleResponse(response)) {
+                    break;
+                }
+            }
+        }
+    }
+
+    /**
+     * Extract a single response from the pending queue that
+     * match a give keyword type.  All matching responses
+     * are removed from the pending queue.
+     *
+     * @param type   The string name of the keyword.
+     *
+     * @return A List of all matching queued responses.
+     */
+    public IMAPUntaggedResponse extractResponse(String type) {
+        Iterator i = queuedResponses.iterator();
+        while (i.hasNext()) {
+            IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
+            // if this is of the target type, move it to the response set.
+            if (response.isKeyword(type)) {
+                i.remove();
+                return response;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Extract all responses from the pending queue that
+     * match a give keyword type.  All matching responses
+     * are removed from the pending queue.
+     *
+     * @param type   The string name of the keyword.
+     *
+     * @return A List of all matching queued responses.
+     */
+    public List extractResponses(String type) {
+        List responses = new ArrayList();
+
+        Iterator i = queuedResponses.iterator();
+        while (i.hasNext()) {
+            IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
+            // if this is of the target type, move it to the response set.
+            if (response.isKeyword(type)) {
+                i.remove();
+                responses.add(response);
+            }
+        }
+        return responses;
+    }
+
+
+    /**
+     * Extract all responses from the pending queue that
+     * are "FETCH" responses for a given message number.  All matching responses
+     * are removed from the pending queue.
+     *
+     * @param type   The string name of the keyword.
+     *
+     * @return A List of all matching queued responses.
+     */
+    public List extractFetchResponses(int sequenceNumber) {
+        List responses = new ArrayList();
+
+        Iterator i = queuedResponses.iterator();
+        while (i.hasNext()) {
+            IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
+            // if this is of the target type, move it to the response set.
+            if (response.isKeyword("FETCH")) {
+                IMAPFetchResponse fetch = (IMAPFetchResponse)response;
+                // a response for the correct message number?
+                if (fetch.sequenceNumber == sequenceNumber) {
+                    // pluck these from the list and add to the response set.
+                    i.remove();
+                    responses.add(response);
+                }
+            }
+        }
+        return responses;
+    }
+
+    /**
+     * Extract a fetch response data item from the queued elements.
+     *
+     * @param sequenceNumber
+     *               The message number we're interested in.  Fetch responses for other messages
+     *               will be skipped.
+     * @param type   The type of body element we need. It is assumed that only one item for
+     *               the given message number will exist in the queue.  The located item will
+     *               be returned, and that fetch response will be removed from the pending queue.
+     *
+     * @return The target data item, or null if a match is not found.
+     */
+    protected IMAPFetchDataItem extractFetchDataItem(long sequenceNumber, int type)
+    {
+        Iterator i = queuedResponses.iterator();
+        while (i.hasNext()) {
+            IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
+            // if this is of the target type, move it to the response set.
+            if (response.isKeyword("FETCH")) {
+                IMAPFetchResponse fetch = (IMAPFetchResponse)response;
+                // a response for the correct message number?
+                if (fetch.sequenceNumber == sequenceNumber) {
+                    // does this response have the item we're looking for?
+                    IMAPFetchDataItem item = fetch.getDataItem(type);
+                    if (item != null) {
+                        // remove this from the pending queue and return the
+                        // located item
+                        i.remove();
+                        return item;
+                    }
+                }
+            }
+        }
+        // not located, sorry
+        return null;
+    }
+
+    /**
+     * Extract a all fetch responses that contain a given data item.
+     *
+     * @param type   The type of body element we need. It is assumed that only one item for
+     *               the given message number will exist in the queue.  The located item will
+     *               be returned, and that fetch response will be removed from the pending queue.
+     *
+     * @return A List of all matching Fetch responses.
+     */
+    protected List extractFetchDataItems(int type)
+    {
+        Iterator i = queuedResponses.iterator();
+        List items = new ArrayList();
+
+        while (i.hasNext()) {
+            IMAPUntaggedResponse response = (IMAPUntaggedResponse)i.next();
+            // if this is of the target type, move it to the response set.
+            if (response.isKeyword("FETCH")) {
+                IMAPFetchResponse fetch = (IMAPFetchResponse)response;
+                // does this response have the item we're looking for?
+                IMAPFetchDataItem item = fetch.getDataItem(type);
+                if (item != null) {
+                    // remove this from the pending queue and return the
+                    // located item
+                    i.remove();
+                    // we want the fetch response, not the data item, because
+                    // we're going to require the message sequence number information
+                    // too.
+                    items.add(fetch);
+                }
+            }
+        }
+        // return whatever we have.
+        return items;
+    }
+
+    /**
+     * Make sure we have the latest status information available.  We
+     * retreive this by sending a NOOP command to the server, and
+     * processing any untagged responses we get back.
+     */
+    public void updateMailboxStatus() throws MessagingException {
+        sendSimpleCommand("NOOP");
+    }
+
+
+    /**
+     * check to see if this connection is truely alive.
+     *
+     * @param timeout The timeout value to control how often we ping
+     *                the server to see if we're still good.
+     *
+     * @return true if the server is responding to requests, false for any
+     *         connection errors.  This will also update the folder status
+     *         by processing returned unsolicited messages.
+     */
+    public synchronized boolean isAlive(long timeout) {
+        long lastUsed = System.currentTimeMillis() - lastAccess;
+        if (lastUsed < timeout) {
+            return true;
+        }
+
+        try {
+            sendSimpleCommand("NOOP");
+            return true;
+        } catch (MessagingException e) {
+            // the NOOP command will throw a MessagingException if we get anything
+            // other than an OK response back from the server.
+        }
+        return false;
+    }
+
+
+    /**
+     * Issue a fetch command to retrieve the message ENVELOPE structure.
+     *
+     * @param sequenceNumber The sequence number of the message.
+     *
+     * @return The IMAPResponse item containing the ENVELOPE information.
+     */
+    public synchronized List fetchEnvelope(int sequenceNumber) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("FETCH");
+        command.appendInteger(sequenceNumber);
+        command.startList();
+        command.appendAtom("ENVELOPE INTERNALDATE RFC822.SIZE");
+        command.endList();
+
+        // we want all of the envelope information about the message, which involves multiple FETCH chunks.
+        sendCommand(command);
+        // these are fairly involved sets, so the caller needs to handle these.
+        // we just return all of the FETCH results matching the target message number.
+        return extractFetchResponses(sequenceNumber);
+    }
+
+    /**
+     * Issue a FETCH command to retrieve the message BODYSTRUCTURE structure.
+     *
+     * @param sequenceNumber The sequence number of the message.
+     *
+     * @return The IMAPBodyStructure item for the message.
+     *         All other untagged responses are queued for processing.
+     */
+    public synchronized IMAPBodyStructure fetchBodyStructure(int sequenceNumber) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("FETCH");
+        command.appendInteger(sequenceNumber);
+        command.startList();
+        command.appendAtom("BODYSTRUCTURE");
+        command.endList();
+
+        // we want all of the envelope information about the message, which involves multiple FETCH chunks.
+        sendCommand(command);
+        // locate the response from this
+        IMAPBodyStructure bodyStructure = (IMAPBodyStructure)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.BODYSTRUCTURE);
+
+        if (bodyStructure == null) {
+            throw new MessagingException("No BODYSTRUCTURE information received from IMAP server");
+        }
+        // and return the body structure directly.
+        return bodyStructure;
+    }
+
+
+    /**
+     * Issue a FETCH command to retrieve the message RFC822.HEADERS structure containing the message headers (using PEEK).
+     *
+     * @param sequenceNumber The sequence number of the message.
+     *
+     * @return The IMAPRFC822Headers item for the message.
+     *         All other untagged responses are queued for processing.
+     */
+    public synchronized InternetHeaders fetchHeaders(int sequenceNumber, String part) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("FETCH");
+        command.appendInteger(sequenceNumber);
+        command.startList();
+        command.appendAtom("BODY.PEEK");
+        command.appendBodySection(part, "HEADER");
+        command.endList();
+
+        // we want all of the envelope information about the message, which involves multiple FETCH chunks.
+        sendCommand(command);
+        IMAPInternetHeader header = (IMAPInternetHeader)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.HEADER);
+
+        if (header == null) {
+            throw new MessagingException("No HEADER information received from IMAP server");
+        }
+        // and return the body structure directly.
+        return header.headers;
+    }
+
+
+    /**
+     * Issue a FETCH command to retrieve the message text
+     *
+     * @param sequenceNumber The sequence number of the message.
+     *
+     * @return The IMAPMessageText item for the message.
+     *         All other untagged responses are queued for processing.
+     */
+    public synchronized IMAPMessageText fetchText(int sequenceNumber) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("FETCH");
+        command.appendInteger(sequenceNumber);
+        command.startList();
+        command.appendAtom("BODY.PEEK");
+        command.appendBodySection("TEXT");
+        command.endList();
+
+        // we want all of the envelope information about the message, which involves multiple FETCH chunks.
+        sendCommand(command);
+        IMAPMessageText text = (IMAPMessageText)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.TEXT);
+
+        if (text == null) {
+            throw new MessagingException("No TEXT information received from IMAP server");
+        }
+        // and return the body structure directly.
+        return text;
+    }
+
+
+    /**
+     * Issue a FETCH command to retrieve the message text
+     *
+     * @param sequenceNumber The sequence number of the message.
+     *
+     * @return The IMAPMessageText item for the message.
+     *         All other untagged responses are queued for processing.
+     */
+    public synchronized IMAPMessageText fetchBodyPartText(int sequenceNumber, String section) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("FETCH");
+        command.appendInteger(sequenceNumber);
+        command.startList();
+        command.appendAtom("BODY.PEEK");
+        command.appendBodySection(section, "TEXT");
+        command.endList();
+
+        // we want all of the envelope information about the message, which involves multiple FETCH chunks.
+        sendCommand(command);
+        IMAPMessageText text = (IMAPMessageText)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.TEXT);
+
+        if (text == null) {
+            throw new MessagingException("No TEXT information received from IMAP server");
+        }
+        // and return the body structure directly.
+        return text;
+    }
+
+
+    /**
+     * Issue a FETCH command to retrieve the entire message body in one shot.
+     * This may also be used to fetch an embedded message part as a unit.
+     *
+     * @param sequenceNumber
+     *                The sequence number of the message.
+     * @param section The section number to fetch.  If null, the entire body of the message
+     *                is retrieved.
+     *
+     * @return The IMAPBody item for the message.
+     *         All other untagged responses are queued for processing.
+     * @exception MessagingException
+     */
+    public synchronized IMAPBody fetchBody(int sequenceNumber, String section) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("FETCH");
+        command.appendInteger(sequenceNumber);
+        command.startList();
+        command.appendAtom("BODY.PEEK");
+        // no part name here, only the section identifier.  This will fetch
+        // the entire body, with all of the bits in place.
+        command.appendBodySection(section, null);
+        command.endList();
+
+        // we want all of the envelope information about the message, which involves multiple FETCH chunks.
+        sendCommand(command);
+        IMAPBody body = (IMAPBody)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.BODY);
+
+        if (body == null) {
+            throw new MessagingException("No BODY information received from IMAP server");
+        }
+        // and return the body structure directly.
+        return body;
+    }
+
+
+    /**
+     * Fetch the message content.  This sorts out which method should be used
+     * based on the server capability.
+     *
+     * @param sequenceNumber
+     *               The sequence number of the target message.
+     *
+     * @return The byte[] content information.
+     * @exception MessagingException
+     */
+    public byte[] fetchContent(int sequenceNumber) throws MessagingException {
+        // fetch the text item and return the data
+        IMAPMessageText text = fetchText(sequenceNumber);
+        return text.getContent();
+    }
+
+
+    /**
+     * Fetch the message content.  This sorts out which method should be used
+     * based on the server capability.
+     *
+     * @param sequenceNumber
+     *               The sequence number of the target message.
+     *
+     * @return The byte[] content information.
+     * @exception MessagingException
+     */
+    public byte[] fetchContent(int sequenceNumber, String section) throws MessagingException {
+        if (section == null) {
+            IMAPMessageText text = fetchText(sequenceNumber);
+            return text.getContent();
+        } else {
+            IMAPBody body = fetchBody(sequenceNumber, section);
+            return body.getContent();
+        }
+    }
+
+
+    /**
+     * Send an LIST command to the IMAP server, returning all LIST
+     * response information.
+     *
+     * @param mailbox The reference mailbox name sent on the command.
+     * @param pattern The match pattern used on the name.
+     *
+     * @return A List of all LIST response information sent back from the server.
+     */
+    public synchronized List list(String mailbox, String pattern) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("LIST");
+
+        // construct the command, encoding the tokens as required by the content.
+        command.appendEncodedString(mailbox);
+        command.appendEncodedString(pattern);
+
+        sendCommand(command);
+
+        // pull out the ones we're interested in
+        return extractResponses("LIST");
+    }
+
+
+    /**
+     * Send an LSUB command to the IMAP server, returning all LSUB
+     * response information.
+     *
+     * @param mailbox The reference mailbox name sent on the command.
+     * @param pattern The match pattern used on the name.
+     *
+     * @return A List of all LSUB response information sent back from the server.
+     */
+    public List listSubscribed(String mailbox, String pattern) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("LSUB");
+
+        // construct the command, encoding the tokens as required by the content.
+        command.appendEncodedString(mailbox);
+        command.appendEncodedString(pattern);
+
+        sendCommand(command);
+        // pull out the ones we're interested in
+        return extractResponses("LSUB");
+    }
+
+
+    /**
+     * Subscribe to a give mailbox.
+     *
+     * @param mailbox The desired mailbox name.
+     *
+     * @exception MessagingException
+     */
+    public void subscribe(String mailbox) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("SUBSCRIBE");
+        // add on the encoded mailbox name, as the appropriate token type.
+        command.appendEncodedString(mailbox);
+
+        // send this, and ignore the response.
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Unsubscribe from a mailbox.
+     *
+     * @param mailbox The mailbox to remove.
+     *
+     * @exception MessagingException
+     */
+    public void unsubscribe(String mailbox) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("UNSUBSCRIBE");
+        // add on the encoded mailbox name, as the appropriate token type.
+        command.appendEncodedString(mailbox);
+
+        // send this, and ignore the response.
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Create a mailbox.
+     *
+     * @param mailbox The desired new mailbox name (fully qualified);
+     *
+     * @exception MessagingException
+     */
+    public void createMailbox(String mailbox) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("CREATE");
+        // add on the encoded mailbox name, as the appropriate token type.
+        command.appendEncodedString(mailbox);
+
+        // send this, and ignore the response.
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Delete a mailbox.
+     *
+     * @param mailbox The target mailbox name (fully qualified);
+     *
+     * @exception MessagingException
+     */
+    public void deleteMailbox(String mailbox) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("DELETE");
+        // add on the encoded mailbox name, as the appropriate token type.
+        command.appendEncodedString(mailbox);
+
+        // send this, and ignore the response.
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Rename a mailbox.
+     *
+     * @param mailbox The target mailbox name (fully qualified);
+     *
+     * @exception MessagingException
+     */
+    public void renameMailbox(String oldName, String newName) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("RENAME");
+        // add on the encoded mailbox name, as the appropriate token type.
+        command.appendEncodedString(oldName);
+        command.appendEncodedString(newName);
+
+        // send this, and ignore the response.
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Retrieve a complete set of status items for a mailbox.
+     *
+     * @param mailbox The mailbox name.
+     *
+     * @return An IMAPMailboxStatus item filled in with the STATUS responses.
+     * @exception MessagingException
+     */
+    public synchronized IMAPMailboxStatus getMailboxStatus(String mailbox) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("STATUS");
+
+        // construct the command, encoding the tokens as required by the content.
+        command.appendEncodedString(mailbox);
+        // request all of the status items
+        command.append(" (MESSAGES RECENT UIDNEXT UIDVALIDITY UNSEEN)");
+
+        sendCommand(command);
+
+        // now harvest each of the respon
+        IMAPMailboxStatus status = new IMAPMailboxStatus();
+        status.mergeSizeResponses(extractResponses("EXISTS"));
+        status.mergeSizeResponses(extractResponses("RECENT"));
+        status.mergeOkResponses(extractResponses("UIDNEXT"));
+        status.mergeOkResponses(extractResponses("UIDVALIDITY"));
+        status.mergeOkResponses(extractResponses("UNSEEN"));
+        status.mergeStatus((IMAPStatusResponse)extractResponse("STATUS"));
+        status.mergeStatus((IMAPPermanentFlagsResponse)extractResponse("PERMANENTFLAGS"));
+
+        return status;
+    }
+
+
+    /**
+     * Select a mailbox, returning the accumulated status information
+     * about the mailbox returned with the response.
+     *
+     * @param mailbox  The desired mailbox name.
+     * @param readOnly The open mode.  If readOnly is true, the mailbox is opened
+     *                 using EXAMINE rather than SELECT.
+     *
+     * @return A status object containing the mailbox particulars.
+     * @exception MessagingException
+     */
+    public synchronized IMAPMailboxStatus openMailbox(String mailbox, boolean readOnly) throws MessagingException {
+        IMAPCommand command = new IMAPCommand();
+
+        // if readOnly is required, we use EXAMINE to switch to the mailbox rather than SELECT.
+        // This returns the same response information, but the mailbox will not accept update operations.
+        if (readOnly) {
+            command.appendAtom("EXAMINE");
+        }
+        else {
+            command.appendAtom("SELECT");
+        }
+
+        // construct the command, encoding the tokens as required by the content.
+        command.appendEncodedString(mailbox);
+
+        // issue the select
+        IMAPTaggedResponse response = sendCommand(command);
+
+        IMAPMailboxStatus status = new IMAPMailboxStatus();
+        // set the mode to the requested open mode.
+        status.mode = readOnly ? Folder.READ_ONLY : Folder.READ_WRITE;
+
+        // the server might disagree on the mode, so check to see if
+        // it's telling us READ-ONLY.
+        if (response.hasStatus("READ-ONLY")) {
+            status.mode = Folder.READ_ONLY;
+        }
+
+        // some of these are required, some are optional.
+        status.mergeFlags((IMAPFlagsResponse)extractResponse("FLAGS"));
+        status.mergeStatus((IMAPSizeResponse)extractResponse("EXISTS"));
+        status.mergeStatus((IMAPSizeResponse)extractResponse("RECENT"));
+        status.mergeStatus((IMAPOkResponse)extractResponse("UIDVALIDITY"));
+        status.mergeStatus((IMAPOkResponse)extractResponse("UNSEEN"));
+        status.mergeStatus((IMAPPermanentFlagsResponse)extractResponse("PERMANENTFLAGS"));
+        // mine the response for status information about the selected mailbox.
+        return status;
+    }
+
+
+    /**
+     * Tells the IMAP server to expunge messages marked for deletion.
+     * The server will send us an untagged EXPUNGE message back for
+     * each deleted message.  For explicit expunges we request, we'll
+     * grabbed the untagged responses here, rather than force them to
+     * be handled as pending responses.  The caller will handle the
+     * updates directly.
+     *
+     * @exception MessagingException
+     */
+    public synchronized List expungeMailbox() throws MessagingException {
+        // send the message, and make sure we got an OK response
+        sendCommand("EXPUNGE");
+        // extract all of the expunged responses and return.
+        return extractResponses("EXPUNGED");
+    }
+
+    public int[] searchMailbox(SearchTerm term) throws MessagingException {
+        return searchMailbox("ALL", term);
+    }
+
+    /**
+     * Send a search to the IMAP server using the specified
+     * messages selector and search term.  This figures out what
+     * to do with CHARSET on the SEARCH command.
+     *
+     * @param messages The list of messages (comma-separated numbers or "ALL").
+     * @param term     The desired search criteria
+     *
+     * @return Returns an int[] array of message numbers for all matched messages.
+     * @exception MessagingException
+     */
+    public int[] searchMailbox(String messages, SearchTerm term) throws MessagingException {
+        // don't use a charset by default, but we need to look at the data to see if we have a problem.
+        String charset = null;
+
+        if (IMAPCommand.checkSearchEncoding(term)) {
+            // not sure exactly how to decide what to use here.  Two immediate possibilities come to mind,
+            // UTF-8 or the MimeUtility.getDefaultJavaCharset() value.  Running a small test against the
+            // Sun impl shows them sending a CHARSET value of UTF-8, so that sounds like the winner.  I don't
+            // believe there's anything in the CAPABILITY response that would tell us what to use.
+            charset = "UTF-8";
+        }
+
+        return searchMailbox(messages, term, charset);
+    }
+
+    /**
+     * Send a search to the IMAP server using the specified
+     * messages selector and search term.
+     *
+     * @param messages The list of messages (comma-separated numbers or "ALL").
+     * @param charset  The charset specifier to send to the server.  If null, then
+     *                 the CHARSET keyword is omitted.
+     * @param term     The desired search criteria
+     *
+     * @return Returns an int[] array of message numbers for all matched messages.
+     * @exception MessagingException
+     */
+    public synchronized int[] searchMailbox(String messages, SearchTerm term, String charset) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("SEARCH");
+
+        // if we have an explicit charset to use, append that.
+        if (charset != null) {
+            command.appendAtom("CHARSET");
+            command.appendAtom(charset);
+        }
+
+        // now go through the process of translating the javamail SearchTerm objects into
+        // the IMAP command sequence.  The SearchTerm sequence may be a complex tree of comparison terms,
+        // so this is not a simple process.
+        command.appendSearchTerm(term, charset);
+        // need to append the message set
+        command.appendAtom(messages);
+
+        // now issue the composed command.
+        sendCommand(command);
+
+        // get the list of search responses
+        IMAPSearchResponse hits = (IMAPSearchResponse)extractResponse("SEARCH");
+        // and return the message hits
+        return hits.messageNumbers;
+    }
+
+
+    /**
+     * Append a message to a mailbox, given the direct message data.
+     *
+     * @param mailbox The target mailbox name.
+     * @param messageFlags
+     *                The initial flag set for the appended message.
+     * @param messageDate
+     *                The received date the message is created with,
+     * @param messageData
+     *                The RFC822 Message data stored on the server.
+     *
+     * @exception MessagingException
+     */
+    public void appendMessage(String mailbox, Date messageDate, Flags messageFlags, byte[] messageData) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("APPEND");
+
+        // the mailbox is encoded.
+        command.appendEncodedString(mailbox);
+
+        if (messageFlags != null) {
+            // the flags are pulled from an existing object.  We can set most flag values, but the servers
+            // reserve RECENT for themselves.  We need to force that one off.
+            messageFlags.remove(Flags.Flag.RECENT);
+            // and add the flag list to the commmand.
+            command.appendFlags(messageFlags);
+        }
+
+        if (messageDate != null) {
+            command.appendDate(messageDate);
+        }
+
+        // this gets appended as a literal.
+        command.appendLiteral(messageData);
+        // just send this as a simple command...we don't deal with the response other than to verifiy
+        // it was ok.
+        sendSimpleCommand(command);
+    }
+
+    /**
+     * Fetch the flag set for a given message sequence number.
+     *
+     * @param sequenceNumber
+     *               The message sequence number.
+     *
+     * @return The Flags defined for this message.
+     * @exception MessagingException
+     */
+    public synchronized Flags fetchFlags(int sequenceNumber) throws MessagingException {
+        // we want just the flag item here.
+        sendCommand("FETCH " + String.valueOf(sequenceNumber) + " (FLAGS)");
+        // get the return data item, and get the flags from within it
+        IMAPFlags flags = (IMAPFlags)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.FLAGS);
+        return flags.flags;
+    }
+
+
+    /**
+     * Set the flags for a range of messages.
+     *
+     * @param messageSet The set of message numbers.
+     * @param flags      The new flag settings.
+     * @param set        true if the flags should be set, false for a clear operation.
+     *
+     * @return A list containing all of the responses with the new flag values.
+     * @exception MessagingException
+     */
+    public synchronized List setFlags(String messageSet, Flags flags, boolean set) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("STORE");
+        command.appendAtom(messageSet);
+        // the command varies depending on whether this is a set or clear operation
+        if (set) {
+            command.appendAtom("+FLAGS");
+        }
+        else {
+            command.appendAtom("-FLAGS");
+        }
+
+        // append the flag set
+        command.appendFlags(flags);
+
+        // we want just the flag item here.
+        sendCommand(command);
+        // we should have a FETCH response for each of the updated messages.  Return this
+        // response, and update the message numbers.
+        return extractFetchDataItems(IMAPFetchDataItem.FLAGS);
+    }
+
+
+    /**
+     * Set the flags for a single message.
+     *
+     * @param sequenceNumber
+     *               The sequence number of target message.
+     * @param flags  The new flag settings.
+     * @param set    true if the flags should be set, false for a clear operation.
+     *
+     * @exception MessagingException
+     */
+    public synchronized Flags setFlags(int sequenceNumber, Flags flags, boolean set) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("STORE");
+        command.appendInteger(sequenceNumber);
+        // the command varies depending on whether this is a set or clear operation
+        if (set) {
+            command.appendAtom("+FLAGS");
+        }
+        else {
+            command.appendAtom("-FLAGS");
+        }
+
+        // append the flag set
+        command.appendFlags(flags);
+
+        // we want just the flag item here.
+        sendCommand(command);
+        // get the return data item, and get the flags from within it
+        IMAPFlags flagResponse = (IMAPFlags)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.FLAGS);
+        return flagResponse.flags;
+    }
+
+
+    /**
+     * Copy a range of messages to a target mailbox.
+     *
+     * @param messageSet The set of message numbers.
+     * @param target     The target mailbox name.
+     *
+     * @exception MessagingException
+     */
+    public void copyMessages(String messageSet, String target) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("COPY");
+        // the auth command initiates the handshaking.
+        command.appendAtom(messageSet);
+        // the mailbox is encoded.
+        command.appendEncodedString(target);
+        // just send this as a simple command...we don't deal with the response other than to verifiy
+        // it was ok.
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Fetch the message number for a give UID.
+     *
+     * @param uid    The target UID
+     *
+     * @return An IMAPUid object containing the mapping information.
+     */
+    public synchronized IMAPUid getSequenceNumberForUid(long uid) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("UID FETCH");
+        command.appendLong(uid);
+        command.appendAtom("(UID)");
+
+        // this situation is a little strange, so it deserves a little explanation.
+        // We need the message sequence number for this message from a UID value.
+        // we're going to send a UID FETCH command, requesting the UID value back.
+        // That seems strange, but the * nnnn FETCH response for the request will
+        // be tagged with the message sequence number.  THAT'S the information we
+        // really want, and it will be included in the IMAPUid object.
+
+        sendCommand(command);
+        // ok, now we need to search through these looking for a FETCH response with a UID element.
+        List responses = extractResponses("FETCH");
+
+        // we're looking for a fetch response with a UID data item with the UID information
+        // inside of it.
+        for (int i = 0; i < responses.size(); i++) {
+            IMAPFetchResponse response = (IMAPFetchResponse)responses.get(i);
+            IMAPUid item = (IMAPUid)response.getDataItem(IMAPFetchDataItem.UID);
+            // is this the response we're looking for?  The information we
+            // need is the message number returned with the response, which is
+            // also contained in the UID item.
+            if (item != null && item.uid == uid) {
+                return item;
+            }
+            // not one meant for us, add it back to the pending queue.
+            queuePendingResponse(response);
+        }
+        // didn't find this one
+        return null;
+    }
+
+
+    /**
+     * Fetch the message numbers for a consequetive range
+     * of UIDs.
+     *
+     * @param start  The start of the range.
+     * @param end    The end of the uid range.
+     *
+     * @return A list of UID objects containing the mappings.
+     */
+    public synchronized List getSequenceNumbersForUids(long start, long end) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("UID FETCH");
+        // send the request for the range "start:end" so we can fetch all of the info
+        // at once.
+        command.appendLong(start);
+        command.append(":");
+        // not the special range marker?  Just append the
+        // number.  The LASTUID value needs to be "*" on the command.
+        if (end != UIDFolder.LASTUID) {
+            command.appendLong(end);
+        }
+        else {
+            command.append("*");
+        }
+        command.appendAtom("(UID)");
+
+        // this situation is a little strange, so it deserves a little explanation.
+        // We need the message sequence number for this message from a UID value.
+        // we're going to send a UID FETCH command, requesting the UID value back.
+        // That seems strange, but the * nnnn FETCH response for the request will
+        // be tagged with the message sequence number.  THAT'S the information we
+        // really want, and it will be included in the IMAPUid object.
+
+        sendCommand(command);
+        // ok, now we need to search through these looking for a FETCH response with a UID element.
+        List responses = extractResponses("FETCH");
+
+        List uids = new ArrayList((int)(end - start + 1));
+
+        // we're looking for a fetch response with a UID data item with the UID information
+        // inside of it.
+        for (int i = 0; i < responses.size(); i++) {
+            IMAPFetchResponse response = (IMAPFetchResponse)responses.get(i);
+            IMAPUid item = (IMAPUid)response.getDataItem(IMAPFetchDataItem.UID);
+            // is this the response we're looking for?  The information we
+            // need is the message number returned with the response, which is
+            // also contained in the UID item.
+            if (item != null) {
+                uids.add(item);
+            }
+            else {
+                // not one meant for us, add it back to the pending queue.
+                queuePendingResponse(response);
+            }
+        }
+        // return the list of uids we located.
+        return uids;
+    }
+
+
+    /**
+     * Fetch the UID value for a target message number
+     *
+     * @param sequenceNumber
+     *               The target message number.
+     *
+     * @return An IMAPUid object containing the mapping information.
+     */
+    public synchronized IMAPUid getUidForSequenceNumber(int sequenceNumber) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("FETCH");
+        command.appendInteger(sequenceNumber);
+        command.appendAtom("(UID)");
+
+        // similar to the other fetches, but without the strange bit.  We're starting
+        // with the message number in this case.
+
+        sendCommand(command);
+
+        // ok, now we need to search through these looking for a FETCH response with a UID element.
+        return (IMAPUid)extractFetchDataItem(sequenceNumber, IMAPFetchDataItem.UID);
+    }
+
+
+    /**
+     * Retrieve the user name space info from the server.
+     *
+     * @return An IMAPNamespace response item with the information.  If the server
+     *         doesn't support the namespace extension, an empty one is returned.
+     */
+    public synchronized IMAPNamespaceResponse getNamespaces() throws MessagingException {
+        // if no namespace capability, then return an empty
+        // response, which will trigger the default behavior.
+        if (!hasCapability("NAMESPACE")) {
+            return new IMAPNamespaceResponse();
+        }
+        // no arguments on this command, so just send an hope it works.
+        sendCommand("NAMESPACE");
+
+        // this should be here, since it's a required response when the
+        // command worked.  Just extract, and return.
+        return (IMAPNamespaceResponse)extractResponse("NAMESPACE");
+    }
+
+
+    /**
+     * Prefetch message information based on the request profile.  We'll return
+     * all of the fetch information to the requesting Folder, which will sort
+     * out what goes where.
+     *
+     * @param messageSet The set of message numbers we need to fetch.
+     * @param profile    The profile of the required information.
+     *
+     * @return All FETCH responses resulting from the command.
+     * @exception MessagingException
+     */
+    public synchronized List fetch(String messageSet, FetchProfile profile) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("FETCH");
+        command.appendAtom(messageSet);
+        // this is the set of items to append
+        command.appendFetchProfile(profile);
+
+        // now send the fetch command, which will likely send back a lot of "FETCH" responses.
+        // Suck all of those reponses out of the queue and send them back for processing.
+        sendCommand(command);
+        // we can have a large number of messages here, so just grab all of the fetches
+        // we get back, and let the Folder sort out who gets what.
+        return extractResponses("FETCH");
+    }
+
+
+    /**
+     * Set the ACL rights for a mailbox.  This replaces
+     * any existing ACLs defined.
+     *
+     * @param mailbox The target mailbox.
+     * @param acl     The new ACL to be used for the mailbox.
+     *
+     * @exception MessagingException
+     */
+    public synchronized void setACLRights(String mailbox, ACL acl) throws MessagingException {
+        IMAPCommand command = new IMAPCommand("SETACL");
+        command.appendEncodedString(mailbox);
+
+        command.appendACL(acl);
+
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Add a set of ACL rights to a mailbox.
+     *
+     * @param mailbox The mailbox to alter.
+     * @param acl     The ACL to add.
+     *
+     * @exception MessagingException
+     */
+    public synchronized void addACLRights(String mailbox, ACL acl) throws MessagingException {
+        if (!hasCapability("ACL")) {
+            throw new MethodNotSupportedException("ACL not available from this IMAP server");
+        }
+        IMAPCommand command = new IMAPCommand("SETACL");
+        command.appendEncodedString(mailbox);
+
+        command.appendACL(acl, "+");
+
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Remove an ACL from a given mailbox.
+     *
+     * @param mailbox The mailbox to alter.
+     * @param acl     The particular ACL to revoke.
+     *
+     * @exception MessagingException
+     */
+    public synchronized void removeACLRights(String mailbox, ACL acl) throws MessagingException {
+        if (!hasCapability("ACL")) {
+            throw new MethodNotSupportedException("ACL not available from this IMAP server");
+        }
+        IMAPCommand command = new IMAPCommand("SETACL");
+        command.appendEncodedString(mailbox);
+
+        command.appendACL(acl, "-");
+
+        sendSimpleCommand(command);
+    }
+
+
+    /**
+     * Get the ACL rights assigned to a given mailbox.
+     *
+     * @param mailbox The target mailbox.
+     *
+     * @return The an array of ACL items describing the access
+     *         rights to the mailbox.
+     * @exception MessagingException
+     */
+    public synchronized ACL[] getACLRights(String mailbox) throws MessagingException {
+        if (!hasCapability("ACL")) {
+            throw new MethodNotSupportedException("ACL not available from this IMAP server");
+        }
+        IMAPCommand command = new IMAPCommand("GETACL");
+        command.appendEncodedString(mailbox);
+
+        // now send the GETACL command, which will return a single ACL untagged response.
+        sendCommand(command);
+        // there should be just a single ACL response back from this command.
+        IMAPACLResponse response = (IMAPACLResponse)extractResponse("ACL");
+        return response.acls;
+    }
+
+
+    /**
+     * Get the current user's ACL rights to a given mailbox.
+     *
+     * @param mailbox The target mailbox.
+     *
+     * @return The Rights associated with this mailbox.
+     * @exception MessagingException
+     */
+    public synchronized Rights getMyRights(String mailbox) throws MessagingException {
+        if (!hasCapability("ACL")) {
+            throw new MethodNotSupportedException("ACL not available from this IMAP server");
+        }
+        IMAPCommand command = new IMAPCommand("MYRIGHTS");
+        command.appendEncodedString(mailbox);
+
+        // now send the MYRIGHTS command, which will return a single MYRIGHTS untagged response.
+        sendCommand(command);
+        // there should be just a single MYRIGHTS response back from this command.
+        IMAPMyRightsResponse response = (IMAPMyRightsResponse)extractResponse("MYRIGHTS");
+        return response.rights;
+    }
+
+
+    /**
+     * List the ACL rights that a particular user has
+     * to a mailbox.
+     *
+     * @param mailbox The target mailbox.
+     * @param name    The user we're querying.
+     *
+     * @return An array of rights the use has to this mailbox.
+     * @exception MessagingException
+     */
+    public synchronized Rights[] listACLRights(String mailbox, String name) throws MessagingException {
+        if (!hasCapability("ACL")) {
+            throw new MethodNotSupportedException("ACL not available from this IMAP server");
+        }
+        IMAPCommand command = new IMAPCommand("LISTRIGHTS");
+        command.appendEncodedString(mailbox);
+        command.appendString(name);
+
+        // now send the GETACL command, which will return a single ACL untagged response.
+        sendCommand(command);
+        // there should be just a single ACL response back from this command.
+        IMAPListRightsResponse response = (IMAPListRightsResponse)extractResponse("LISTRIGHTS");
+        return response.rights;
+    }
+
+
+    /**
+     * Delete an ACL item for a given user name from
+     * a target mailbox.
+     *
+     * @param mailbox The mailbox we're altering.
+     * @param name    The user name.
+     *
+     * @exception MessagingException
+     */
+    public synchronized void deleteACL(String mailbox, String name) throws MessagingException {
+        if (!hasCapability("ACL")) {
+            throw new MethodNotSupportedException("ACL not available from this IMAP server");
+        }
+        IMAPCommand command = new IMAPCommand("DELETEACL");
+        command.appendEncodedString(mailbox);
+        command.appendString(name);
+
+        // just send the command.  No response to handle.
+        sendSimpleCommand(command);
+    }
+
+    /**
+     * Fetch the quota root information for a target mailbox.
+     *
+     * @param mailbox The mailbox of interest.
+     *
+     * @return An array of quotas describing all of the quota roots
+     *         that apply to the target mailbox.
+     * @exception MessagingException
+     */
+    public synchronized Quota[] fetchQuotaRoot(String mailbox) throws MessagingException {
+        if (!hasCapability("QUOTA")) {
+            throw new MethodNotSupportedException("QUOTA not available from this IMAP server");
+        }
+        IMAPCommand command = new IMAPCommand("GETQUOTAROOT");
+        command.appendEncodedString(mailbox);
+
+        // This will return a single QUOTAROOT response, plust a series of QUOTA responses for
+        // each root names in the first response.
+        sendCommand(command);
+        // we don't really need this, but pull it from the response queue anyway.
+        extractResponse("QUOTAROOT");
+
+        // now get the real meat of the matter
+        List responses = extractResponses("QUOTA");
+
+        // now copy all of the returned quota items into the response array.
+        Quota[] quotas = new Quota[responses.size()];
+        for (int i = 0; i < quotas.length; i++) {
+            IMAPQuotaResponse q = (IMAPQuotaResponse)responses.get(i);
+            quotas[i] = q.quota;
+        }
+
+        return quotas;
+    }
+
+    /**
+     * Fetch QUOTA information from a named QUOTE root.
+     *
+     * @param root   The target root name.
+     *
+     * @return An array of Quota items associated with that root name.
+     * @exception MessagingException
+     */
+    public synchronized Quota[] fetchQuota(String root) throws MessagingException {
+        if (!hasCapability("QUOTA")) {
+            throw new MethodNotSupportedException("QUOTA not available from this IMAP server");
+        }
+        IMAPCommand command = new IMAPCommand("GETQUOTA");
+        command.appendString(root);
+
+        // This will return a single QUOTAROOT response, plust a series of QUOTA responses for
+        // each root names in the first response.
+        sendCommand(command);
+
+        // now get the real meat of the matter
+        List responses = extractResponses("QUOTA");
+
+        // now copy all of the returned quota items into the response array.
+        Quota[] quotas = new Quota[responses.size()];
+        for (int i = 0; i < quotas.length; i++) {
+            IMAPQuotaResponse q = (IMAPQuotaResponse)responses.get(i);
+            quotas[i] = q.quota;
+        }
+
+        return quotas;
+    }
+
+    /**
+     * Set a Quota item for the currently accessed
+     * userid/folder resource.
+     *
+     * @param quota  The new QUOTA information.
+     *
+     * @exception MessagingException
+     */
+    public synchronized void setQuota(Quota quota) throws MessagingException {
+        if (!hasCapability("QUOTA")) {
+            throw new MethodNotSupportedException("QUOTA not available from this IMAP server");
+        }
+        IMAPCommand command = new IMAPCommand("GETQUOTA");
+        // this gets appended as a list of resource values
+        command.appendQuota(quota);
+
+        // This will return a single QUOTAROOT response, plust a series of QUOTA responses for
+        // each root names in the first response.
+        sendCommand(command);
+        // we don't really need this, but pull it from the response queue anyway.
+        extractResponses("QUOTA");
+    }
+
+
+    /**
+     * Test if this connection has a given capability.
+     *
+     * @param capability The capability name.
+     *
+     * @return true if this capability is in the list, false for a mismatch.
+     */
+    public boolean hasCapability(String capability) {
+        if (capabilities == null) {
+            return false;
+        }
+        return capabilities.containsKey(capability);
+    }
+
+    /**
+     * Tag this connection as having been closed by the
+     * server.  This will not be returned to the
+     * connection pool.
+     */
+    public void setClosed() {
+        closed = true;
+    }
+
+    /**
+     * Test if the connnection has been forcibly closed.
+     *
+     * @return True if the server disconnected the connection.
+     */
+    public boolean isClosed() {
+        return closed;
+    }
+}
+