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;
+ }
+}
+