| /** |
| * 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. |
| 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 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; |
| } |
| } |
| |