blob: 44cfb459819258e819419f7f470932cf7453fd0d [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.geronimo.javamail.store.imap;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import javax.activation.DataHandler;
import javax.mail.Address;
import javax.mail.FetchProfile;
import javax.mail.Flags;
import javax.mail.Folder;
import javax.mail.Header;
import javax.mail.IllegalWriteException;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.MessageRemovedException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.UIDFolder;
import javax.mail.event.MessageChangedEvent;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.InternetHeaders;
import javax.mail.internet.MailDateFormat;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeUtility;
import org.apache.geronimo.javamail.store.imap.connection.IMAPBody;
import org.apache.geronimo.javamail.store.imap.connection.IMAPBodyStructure;
import org.apache.geronimo.javamail.store.imap.connection.IMAPConnection;
import org.apache.geronimo.javamail.store.imap.connection.IMAPEnvelope;
import org.apache.geronimo.javamail.store.imap.connection.IMAPFetchDataItem;
import org.apache.geronimo.javamail.store.imap.connection.IMAPFetchResponse;
import org.apache.geronimo.javamail.store.imap.connection.IMAPInternalDate;
import org.apache.geronimo.javamail.store.imap.connection.IMAPInternetHeader;
import org.apache.geronimo.javamail.store.imap.connection.IMAPMessageSize;
import org.apache.geronimo.javamail.store.imap.connection.IMAPUid;
import org.apache.geronimo.javamail.util.MailConnection;
/**
* IMAP implementation of javax.mail.internet.MimeMessage
*
* Only the most basic information is given and
* Message objects created here is a light-weight reference to the actual Message
* As per the JavaMail spec items from the actual message will get filled up on demand
*
* If some other items are obtained from the server as a result of one call, then the other
* details are also processed and filled in. For ex if RETR is called then header information
* will also be processed in addition to the content
*
* @version $Rev: 946314 $ $Date: 2010-05-19 14:01:55 -0400 (Wed, 19 May 2010) $
*/
public class IMAPMessage extends MimeMessage {
private static final byte[] CRLF = new byte[]{'\r', '\n'};
// the Store we're stored in (which manages the connection and other stuff).
protected IMAPStore store;
// the IMAP server sequence number (potentially updated during the life of this message object).
protected int sequenceNumber;
// the IMAP uid value;
protected long uid = -1;
// the section identifier. This is only really used for nested messages. The toplevel version
// will be null, and each nested message will set the appropriate part identifier
protected String section;
// the loaded message envelope (delayed until needed)
protected IMAPEnvelope envelope;
// the body structure information (also lazy loaded).
protected IMAPBodyStructure bodyStructure;
// the IMAP INTERNALDATE value.
protected Date receivedDate;
// the size item, which is maintained separately from the body structure
// as it can be retrieved without getting the body structure
protected int size;
// turned on once we've requested the entire header set.
protected boolean allHeadersRetrieved = false;
// singleton date formatter for this class.
static protected MailDateFormat dateFormat = new MailDateFormat();
/**
* Contruct an IMAPMessage instance.
*
* @param folder The hosting folder for the message.
* @param store The Store owning the article (and folder).
* @param msgnum The article message number. This is assigned by the Folder, and is unique
* for each message in the folder. The message numbers are only valid
* as long as the Folder is open.
* @param sequenceNumber The IMAP server manages messages by sequence number, which is subject to
* change whenever messages are expunged. This is the server retrieval number
* of the message, which needs to be synchronized with status updates
* sent from the server.
*
* @exception MessagingException
*/
IMAPMessage(IMAPFolder folder, IMAPStore store, int msgnum, int sequenceNumber) {
super(folder, msgnum);
this.sequenceNumber = sequenceNumber;
this.store = store;
// The default constructor creates an empty Flags item. We need to clear this out so we
// know if the flags need to be fetched from the server when requested.
flags = null;
// make sure this is a totally fresh set of headers. We'll fill things in as we retrieve them.
headers = new InternetHeaders();
}
/**
* Override for the Message class setExpunged() method to allow
* us to do additional cleanup for expunged messages.
*
* @param value The new expunge setting.
*/
public void setExpunged(boolean value) {
// super class handles most of the details
super.setExpunged(value);
// if we're now expunged, this removes us from the server message sequencing scheme, so
// we need to invalidate the sequence number.
if (isExpunged()) {
sequenceNumber = -1;
}
}
/**
* Return a copy the flags associated with this message.
*
* @return a copy of the flags for this message
* @throws MessagingException if there was a problem accessing the Store
*/
public synchronized Flags getFlags() throws MessagingException {
// load the flags, if needed
loadFlags();
return super.getFlags();
}
/**
* Check whether the supplied flag is set.
* The default implementation checks the flags returned by {@link #getFlags()}.
*
* @param flag the flags to check for
* @return true if the flags is set
* @throws MessagingException if there was a problem accessing the Store
*/
public synchronized boolean isSet(Flags.Flag flag) throws MessagingException {
// load the flags, if needed
loadFlags();
return super.isSet(flag);
}
/**
* Set or clear a flag value.
*
* @param flags The set of flags to effect.
* @param set The value to set the flag to (true or false).
*
* @exception MessagingException
*/
public synchronized void setFlags(Flags flag, boolean set) throws MessagingException {
// make sure this is in a valid state.
checkValidity();
// we need to ensure that we're the only ones with access to the folder's
// message cache any time we need to talk to the server. This needs to be
// held until after we release the connection so that any pending EXPUNGE
// untagged responses are processed before the next time the folder connection is
// used.
synchronized (folder) {
IMAPConnection connection = getConnection();
try {
// set the flags for this item and update the
// internal state with the new values returned from the
// server.
flags = connection.setFlags(sequenceNumber, flag, set);
} finally {
releaseConnection(connection);
}
}
}
/**
* Return an InputStream instance for accessing the
* message content.
*
* @return An InputStream instance for accessing the content
* (body) of the message.
* @exception MessagingException
* @see javax.mail.internet.MimeMessage#getContentStream()
*/
protected InputStream getContentStream() throws MessagingException {
// no content loaded yet?
if (content == null) {
// make sure we're still valid
checkValidity();
// make sure the content is fully loaded
loadContent();
}
// allow the super class to handle creating it from the loaded content.
return super.getContentStream();
}
/**
* Write out the byte data to the provided output stream.
*
* @param out The target stream.
*
* @exception IOException
* @exception MessagingException
*/
public void writeTo(OutputStream out) throws IOException, MessagingException {
// no content loaded yet?
if (content == null) {
// make sure we're still valid
checkValidity();
// make sure the content is fully loaded
loadContent();
}
loadHeaders();
Enumeration e = headers.getAllHeaderLines();
while(e.hasMoreElements()) {
String line = (String)e.nextElement();
out.write(line.getBytes("ISO8859-1"));
out.write(CRLF);
}
out.write(CRLF);
out.write(CRLF);
out.write(content);
}
/******************************************************************
* Following is a set of methods that deal with information in the
* envelope. These methods ensure the enveloper is loaded and
* retrieve the information.
********************************************************************/
/**
* Get the message "From" addresses. This looks first at the
* "From" headers, and no "From" header is found, the "Sender"
* header is checked. Returns null if not found.
*
* @return An array of addresses identifying the message from target. Returns
* null if this is not resolveable from the headers.
* @exception MessagingException
*/
public Address[] getFrom() throws MessagingException {
// make sure we've retrieved the envelope information.
loadEnvelope();
// make sure we return a copy of the array so this can't be changed.
Address[] addresses = envelope.from;
if (addresses == null) {
return null;
}
return (Address[])addresses.clone();
}
/**
* Return the "Sender" header as an address.
*
* @return the "Sender" header as an address, or null if not present
* @throws MessagingException if there was a problem parsing the header
*/
public Address getSender() throws MessagingException {
// make sure we've retrieved the envelope information.
loadEnvelope();
// make sure we return a copy of the array so this can't be changed.
Address[] addresses = envelope.sender;
if (addresses == null) {
return null;
}
// There's only a single sender, despite IMAP potentially returning a list
return addresses[0];
}
/**
* Gets the recipients by type. Returns null if there are no
* headers of the specified type. Acceptable RecipientTypes are:
*
* javax.mail.Message.RecipientType.TO
* javax.mail.Message.RecipientType.CC
* javax.mail.Message.RecipientType.BCC
* javax.mail.internet.MimeMessage.RecipientType.NEWSGROUPS
*
* @param type The message RecipientType identifier.
*
* @return The array of addresses for the specified recipient types.
* @exception MessagingException
*/
public Address[] getRecipients(Message.RecipientType type) throws MessagingException {
// make sure we've retrieved the envelope information.
loadEnvelope();
Address[] addresses = null;
if (type == Message.RecipientType.TO) {
addresses = envelope.to;
}
else if (type == Message.RecipientType.CC) {
addresses = envelope.cc;
}
else if (type == Message.RecipientType.BCC) {
addresses = envelope.bcc;
}
else {
// this could be a newsgroup type, which will tickle the message headers.
return super.getRecipients(type);
}
// make sure we return a copy of the array so this can't be changed.
if (addresses == null) {
return null;
}
return (Address[])addresses.clone();
}
/**
* Get the ReplyTo address information. The headers are parsed
* using the "mail.mime.address.strict" setting. If the "Reply-To" header does
* not have any addresses, then the value of the "From" field is used.
*
* @return An array of addresses obtained from parsing the header.
* @exception MessagingException
*/
public Address[] getReplyTo() throws MessagingException {
// make sure we've retrieved the envelope information.
loadEnvelope();
// make sure we return a copy of the array so this can't be changed.
Address[] addresses = envelope.replyTo;
if (addresses == null) {
return null;
}
return (Address[])addresses.clone();
}
/**
* Returns the value of the "Subject" header. If the subject
* is encoded as an RFC 2047 value, the value is decoded before
* return. If decoding fails, the raw string value is
* returned.
*
* @return The String value of the subject field.
* @exception MessagingException
*/
public String getSubject() throws MessagingException {
// make sure we've retrieved the envelope information.
loadEnvelope();
if (envelope.subject == null) {
return null;
}
// the subject could be encoded. If there is a decoding error,
// return the raw subject string.
try {
return MimeUtility.decodeText(envelope.subject);
} catch (UnsupportedEncodingException e) {
return envelope.subject;
}
}
/**
* Get the value of the "Date" header field. Returns null if
* if the field is absent or the date is not in a parseable format.
*
* @return A Date object parsed according to RFC 822.
* @exception MessagingException
*/
public Date getSentDate() throws MessagingException {
// make sure we've retrieved the envelope information.
loadEnvelope();
// just return that directly
return envelope.date;
}
/**
* Get the message received date.
*
* @return Always returns the formatted INTERNALDATE, if available.
* @exception MessagingException
*/
public Date getReceivedDate() throws MessagingException {
loadEnvelope();
return receivedDate;
}
/**
* Retrieve the size of the message content. The content will
* be retrieved from the server, if necessary.
*
* @return The size of the content.
* @exception MessagingException
*/
public int getSize() throws MessagingException {
// make sure we've retrieved the envelope information. We load the
// size when we retrieve that.
loadEnvelope();
return size;
}
/**
* Get a line count for the IMAP message. This is potentially
* stored in the Lines article header. If not there, we return
* a default of -1.
*
* @return The header line count estimate, or -1 if not retrieveable.
* @exception MessagingException
*/
public int getLineCount() throws MessagingException {
loadBodyStructure();
return bodyStructure.lines;
}
/**
* Return the IMAP in reply to information (retrieved with the
* ENVELOPE).
*
* @return The in reply to String value, if available.
* @exception MessagingException
*/
public String getInReplyTo() throws MessagingException {
loadEnvelope();
return envelope.inReplyTo;
}
/**
* Returns the current content type (defined in the "Content-Type"
* header. If not available, "text/plain" is the default.
*
* @return The String name of the message content type.
* @exception MessagingException
*/
public String getContentType() throws MessagingException {
loadBodyStructure();
return bodyStructure.mimeType.toString();
}
/**
* Tests to see if this message has a mime-type match with the
* given type name.
*
* @param type The tested type name.
*
* @return If this is a type match on the primary and secondare portion of the types.
* @exception MessagingException
*/
public boolean isMimeType(String type) throws MessagingException {
loadBodyStructure();
return bodyStructure.mimeType.match(type);
}
/**
* Retrieve the message "Content-Disposition" header field.
* This value represents how the part should be represented to
* the user.
*
* @return The string value of the Content-Disposition field.
* @exception MessagingException
*/
public String getDisposition() throws MessagingException {
loadBodyStructure();
if (bodyStructure.disposition != null) {
return bodyStructure.disposition.getDisposition();
}
return null;
}
/**
* Decode the Content-Transfer-Encoding header to determine
* the transfer encoding type.
*
* @return The string name of the required encoding.
* @exception MessagingException
*/
public String getEncoding() throws MessagingException {
loadBodyStructure();
return bodyStructure.transferEncoding;
}
/**
* Retrieve the value of the "Content-ID" header. Returns null
* if the header does not exist.
*
* @return The current header value or null.
* @exception MessagingException
*/
public String getContentID() throws MessagingException {
loadBodyStructure();
return bodyStructure.contentID;
}
public String getContentMD5() throws MessagingException {
loadBodyStructure();
return bodyStructure.md5Hash;
}
public String getDescription() throws MessagingException {
loadBodyStructure();
if (bodyStructure.contentDescription == null) {
return null;
}
// the subject could be encoded. If there is a decoding error,
// return the raw subject string.
try {
return MimeUtility.decodeText(bodyStructure.contentDescription);
} catch (UnsupportedEncodingException e) {
return bodyStructure.contentDescription;
}
}
/**
* Return the content languages associated with this
* message.
*
* @return
* @exception MessagingException
*/
public String[] getContentLanguage() throws MessagingException {
loadBodyStructure();
if (!bodyStructure.languages.isEmpty()) {
return (String[])bodyStructure.languages.toArray(new String[bodyStructure.languages.size()]);
}
return null;
}
public String getMessageID() throws MessagingException {
loadEnvelope();
return envelope.messageID;
}
public void setFrom(Address address) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void addFrom(Address[] address) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setSender(Address address) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setRecipients(Message.RecipientType type, Address[] addresses) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setRecipients(Message.RecipientType type, String address) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void addRecipients(Message.RecipientType type, Address[] address) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setReplyTo(Address[] address) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setSubject(String subject) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setSubject(String subject, String charset) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setSentDate(Date sent) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setDisposition(String disposition) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setContentID(String cid) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setContentMD5(String md5) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setDescription(String description) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setDescription(String description, String charset) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setContentLanguage(String[] languages) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
/******************************************************************
* Following is a set of methods that deal with headers
* These methods are just overrides on the superclass methods to
* allow lazy loading of the header information.
********************************************************************/
public String[] getHeader(String name) throws MessagingException {
loadHeaders();
return headers.getHeader(name);
}
public String getHeader(String name, String delimiter) throws MessagingException {
loadHeaders();
return headers.getHeader(name, delimiter);
}
public Enumeration getAllHeaders() throws MessagingException {
loadHeaders();
return headers.getAllHeaders();
}
public Enumeration getMatchingHeaders(String[] names) throws MessagingException {
loadHeaders();
return headers.getMatchingHeaders(names);
}
public Enumeration getNonMatchingHeaders(String[] names) throws MessagingException {
loadHeaders();
return headers.getNonMatchingHeaders(names);
}
public Enumeration getAllHeaderLines() throws MessagingException {
loadHeaders();
return headers.getAllHeaderLines();
}
public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
loadHeaders();
return headers.getMatchingHeaderLines(names);
}
public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
loadHeaders();
return headers.getNonMatchingHeaderLines(names);
}
// the following are overrides for header modification methods. These messages are read only,
// so the headers cannot be modified.
public void addHeader(String name, String value) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void setHeader(String name, String value) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void removeHeader(String name) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
public void addHeaderLine(String line) throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
/**
* We cannot modify these messages
*/
public void saveChanges() throws MessagingException {
throw new IllegalWriteException("IMAP messages are read-only");
}
/**
* Utility method for synchronizing IMAP envelope information and
* the message headers.
*
* @param header The target header name.
* @param addresses The update addresses.
*/
protected void updateHeader(String header, InternetAddress[] addresses) throws MessagingException {
if (addresses != null) {
headers.addHeader(header, InternetAddress.toString(addresses));
}
}
/**
* Utility method for synchronizing IMAP envelope information and
* the message headers.
*
* @param header The target header name.
* @param address The update address.
*/
protected void updateHeader(String header, Address address) throws MessagingException {
if (address != null) {
headers.setHeader(header, address.toString());
}
}
/**
* Utility method for synchronizing IMAP envelope information and
* the message headers.
*
* @param header The target header name.
* @param value The update value.
*/
protected void updateHeader(String header, String value) throws MessagingException {
if (value != null) {
headers.setHeader(header, value);
}
}
/**
* Create the DataHandler object for this message.
*
* @return The DataHandler object that processes the content set for this
* message.
* @exception MessagingException
*/
public synchronized DataHandler getDataHandler() throws MessagingException {
// check the validity and make sure we have the body structure information.
checkValidity();
loadBodyStructure();
if (dh == null) {
// are we working with a multipart message here?
if (bodyStructure.isMultipart()) {
dh = new DataHandler(new IMAPMultipartDataSource(this, this, section, bodyStructure));
return dh;
}
else if (bodyStructure.isAttachedMessage()) {
dh = new DataHandler(new IMAPAttachedMessage(this, section, bodyStructure.nestedEnvelope, bodyStructure.nestedBody),
bodyStructure.mimeType.toString());
return dh;
}
}
// single part messages get handled the normal way.
return super.getDataHandler();
}
public void setDataHandler(DataHandler content) throws MessagingException {
throw new IllegalWriteException("IMAP body parts are read-only");
}
/**
* Update the message headers from an input stream.
*
* @param in The InputStream source for the header information.
*
* @exception MessagingException
*/
public void updateHeaders(InputStream in) throws MessagingException {
// wrap a stream around the reply data and read as headers.
headers = new InternetHeaders(in);
allHeadersRetrieved = true;
}
/**
* Load the flag set for this message from the server.
*
* @exception MessagingeException
*/
public void loadFlags() throws MessagingException {
// make sure this is in a valid state.
checkValidity();
// if the flags are already loaded, nothing to do
if (flags != null) {
return;
}
// we need to ensure that we're the only ones with access to the folder's
// message cache any time we need to talk to the server. This needs to be
// held until after we release the connection so that any pending EXPUNGE
// untagged responses are processed before the next time the folder connection is
// used.
synchronized (folder) {
IMAPConnection connection = getConnection();
try {
// fetch the flags for this item.
flags = connection.fetchFlags(sequenceNumber);
} finally {
releaseConnection(connection);
}
}
}
/**
* Retrieve the message raw message headers from the IMAP server, synchronizing with the existing header set.
*
* @exception MessagingException
*/
protected synchronized void loadHeaders() throws MessagingException {
// don't retrieve if already loaded.
if (allHeadersRetrieved) {
return;
}
// make sure this is in a valid state.
checkValidity();
// we need to ensure that we're the only ones with access to the folder's
// message cache any time we need to talk to the server. This needs to be
// held until after we release the connection so that any pending EXPUNGE
// untagged responses are processed before the next time the folder connection is
// used.
synchronized (folder) {
IMAPConnection connection = getConnection();
try {
// get the headers and set
headers = connection.fetchHeaders(sequenceNumber, section);
// we have the entire header set, not just a subset.
allHeadersRetrieved = true;
} finally {
releaseConnection(connection);
}
}
}
/**
* Retrieve the message envelope from the IMAP server, synchronizing the headers with the
* information.
*
* @exception MessagingException
*/
protected synchronized void loadEnvelope() throws MessagingException {
// don't retrieve if already loaded.
if (envelope != null) {
return;
}
// make sure this is in a valid state.
checkValidity();
// we need to ensure that we're the only ones with access to the folder's
// message cache any time we need to talk to the server. This needs to be
// held until after we release the connection so that any pending EXPUNGE
// untagged responses are processed before the next time the folder connection is
// used.
synchronized (folder) {
IMAPConnection connection = getConnection();
try {
// fetch the envelope information for this
List fetches = connection.fetchEnvelope(sequenceNumber);
// now process all of the fetch responses before releasing the folder lock.
// it's possible that an unsolicited update on another thread might try to
// make an update, causing a potential deadlock.
for (int i = 0; i < fetches.size(); i++) {
// get the returned data items from each of the fetch responses
// and process.
IMAPFetchResponse fetch = (IMAPFetchResponse)fetches.get(i);
// update the internal info
updateMessageInformation(fetch);
}
} finally {
releaseConnection(connection);
}
}
}
/**
* Retrieve the message envelope from the IMAP server, synchronizing the headers with the
* information.
*
* @exception MessagingException
*/
protected synchronized void updateEnvelope(IMAPEnvelope envelope) throws MessagingException {
// set the envelope item
this.envelope = envelope;
// copy header type information from the envelope into the headers.
updateHeader("From", envelope.from);
if (envelope.sender != null) {
// we can only have a single sender, even though the envelope theoretically supports more.
updateHeader("Sender", envelope.sender[0]);
}
updateHeader("To", envelope.to);
updateHeader("Cc", envelope.cc);
updateHeader("Bcc", envelope.bcc);
updateHeader("Reply-To", envelope.replyTo);
// NB: This is already in encoded form, if needed.
updateHeader("Subject", envelope.subject);
updateHeader("Message-ID", envelope.messageID);
}
/**
* Retrieve the BODYSTRUCTURE information from the IMAP server.
*
* @exception MessagingException
*/
protected synchronized void loadBodyStructure() throws MessagingException {
// don't retrieve if already loaded.
if (bodyStructure != null) {
return;
}
// make sure this is in a valid state.
checkValidity();
// we need to ensure that we're the only ones with access to the folder's
// message cache any time we need to talk to the server. This needs to be
// held until after we release the connection so that any pending EXPUNGE
// untagged responses are processed before the next time the folder connection is
// used.
synchronized (folder) {
IMAPConnection connection = getConnection();
try {
// fetch the envelope information for this
bodyStructure = connection.fetchBodyStructure(sequenceNumber);
// go update all of the information
} finally {
releaseConnection(connection);
}
// update this before we release the folder lock so we can avoid
// deadlock.
updateBodyStructure(bodyStructure);
}
}
/**
* Update the BODYSTRUCTURE information from the IMAP server.
*
* @exception MessagingException
*/
protected synchronized void updateBodyStructure(IMAPBodyStructure structure) throws MessagingException {
// save the reference.
bodyStructure = structure;
// now update various headers with the information from the body structure
// now update header information with the body structure data.
if (bodyStructure.lines != -1) {
updateHeader("Lines", Integer.toString(bodyStructure.lines));
}
// languages are a little more complicated
if (bodyStructure.languages != null) {
// this is a duplicate of what happens in the super class, but
// the superclass methods call setHeader(), which we override and
// throw an exception for. We need to set the headers ourselves.
if (bodyStructure.languages.size() == 1) {
updateHeader("Content-Language", (String)bodyStructure.languages.get(0));
}
else {
StringBuffer buf = new StringBuffer(bodyStructure.languages.size() * 20);
buf.append(bodyStructure.languages.get(0));
for (int i = 1; i < bodyStructure.languages.size(); i++) {
buf.append(',').append(bodyStructure.languages.get(i));
}
updateHeader("Content-Language", buf.toString());
}
}
updateHeader("Content-Type", bodyStructure.mimeType.toString());
if (bodyStructure.disposition != null) {
updateHeader("Content-Disposition", bodyStructure.disposition.toString());
}
updateHeader("Content-Transfer-Encoding", bodyStructure.transferEncoding);
updateHeader("Content-ID", bodyStructure.contentID);
// NB: This is already in encoded form, if needed.
updateHeader("Content-Description", bodyStructure.contentDescription);
}
/**
* Load the message content into the Message object.
*
* @exception MessagingException
*/
protected void loadContent() throws MessagingException {
// if we've loaded this already, just return
if (content != null) {
return;
}
// we need to ensure that we're the only ones with access to the folder's
// message cache any time we need to talk to the server. This needs to be
// held until after we release the connection so that any pending EXPUNGE
// untagged responses are processed before the next time the folder connection is
// used.
synchronized (folder) {
IMAPConnection connection = getConnection();
try {
// load the content from the server.
content = connection.fetchContent(getSequenceNumber(), section);
} finally {
releaseConnection(connection);
}
}
}
/**
* Retrieve the sequence number assigned to this message.
*
* @return The messages assigned sequence number. This maps back to the server's assigned number for
* this message.
*/
int getSequenceNumber() {
return sequenceNumber;
}
/**
* Set the sequence number for the message. This
* is updated whenever messages get expunged from
* the folder.
*
* @param s The new sequence number.
*/
void setSequenceNumber(int s) {
sequenceNumber = s;
}
/**
* Retrieve the message UID value.
*
* @return The assigned UID value, if we have the information.
*/
long getUID() {
return uid;
}
/**
* Set the message UID value.
*
* @param uid The new UID value.
*/
void setUID(long uid) {
this.uid = uid;
}
/**
* get the current connection pool attached to the folder. We need
* to do this dynamically, to A) ensure we're only accessing an
* currently open folder, and B) to make sure we're using the
* correct connection attached to the folder.
*
* @return A connection attached to the hosting folder.
*/
protected IMAPConnection getConnection() throws MessagingException {
// the folder owns everything.
return ((IMAPFolder)folder).getMessageConnection();
}
/**
* Release the connection back to the Folder after performing an operation
* that requires a connection.
*
* @param connection The previously acquired connection.
*/
protected void releaseConnection(IMAPConnection connection) throws MessagingException {
// the folder owns everything.
((IMAPFolder)folder).releaseMessageConnection(connection);
}
/**
* Check the validity of the current message. This ensures that
* A) the folder is currently open, B) that the message has not
* been expunged (after getting the latest status from the server).
*
* @exception MessagingException
*/
protected void checkValidity() throws MessagingException {
checkValidity(false);
}
/**
* Check the validity of the current message. This ensures that
* A) the folder is currently open, B) that the message has not
* been expunged (after getting the latest status from the server).
*
* @exception MessagingException
*/
protected void checkValidity(boolean update) throws MessagingException {
// we need to ensure that we're the only ones with access to the folder's
// message cache any time we need to talk to the server. This needs to be
// held until after we release the connection so that any pending EXPUNGE
// untagged responses are processed before the next time the folder connection is
// used.
if (update) {
synchronized (folder) {
// have the connection update the folder status. This might result in this message
// changing its state to expunged. It might also result in an exception if the
// folder has been closed.
IMAPConnection connection = getConnection();
try {
connection.updateMailboxStatus();
} finally {
// this will force any expunged messages to be processed before we release
// the lock.
releaseConnection(connection);
}
}
}
// now see if we've been expunged, this is a bad op on the message.
if (isExpunged()) {
throw new MessageRemovedException("Illegal opertion on a deleted message");
}
}
/**
* Evaluate whether this message requires any of the information
* in a FetchProfile to be fetched from the server. If the messages
* already contains the information in the profile, it returns false.
* This allows IMAPFolder to optimize fetch() requests to just the
* messages that are missing any of the requested information.
*
* NOTE: If any of the items in the profile are missing, then this
* message will be updated with ALL of the items.
*
* @param profile The FetchProfile indicating the information that should be prefetched.
*
* @return true if any of the profile information requires fetching. false if this
* message already contains the given information.
*/
protected boolean evaluateFetch(FetchProfile profile) {
// the fetch profile can contain a number of different item types. Validate
// whether we need any of these and return true on the first mismatch.
// the UID is a common fetch request, put it first.
if (profile.contains(UIDFolder.FetchProfileItem.UID) && uid == -1) {
return true;
}
if (profile.contains(FetchProfile.Item.ENVELOPE) && envelope == null) {
return true;
}
if (profile.contains(FetchProfile.Item.FLAGS) && flags == null) {
return true;
}
if (profile.contains(FetchProfile.Item.CONTENT_INFO) && bodyStructure == null) {
return true;
}
// The following profile items are our implementation of items that the
// Sun IMAPFolder implementation supports.
if (profile.contains(IMAPFolder.FetchProfileItem.HEADERS) && !allHeadersRetrieved) {
return true;
}
if (profile.contains(IMAPFolder.FetchProfileItem.SIZE) && bodyStructure.bodySize < 0) {
return true;
}
// last bit after checking each of the information types is to see if
// particular headers have been requested and whether those are on the
// set we do have loaded.
String [] requestedHeaders = profile.getHeaderNames();
// ok, any missing header in the list is enough to force us to request the
// information.
for (int i = 0; i < requestedHeaders.length; i++) {
if (headers.getHeader(requestedHeaders[i]) == null) {
return true;
}
}
// this message, at least, does not need anything fetched.
return false;
}
/**
* Update a message instance with information retrieved via an IMAP FETCH
* command. The command response for this message may contain multiple pieces
* that we need to process.
*
* @param response The response line, which may contain multiple data items.
*
* @exception MessagingException
*/
void updateMessageInformation(IMAPFetchResponse response) throws MessagingException {
// get the list of data items associated with this response. We can have
// a large number of items returned in a single update.
List items = response.getDataItems();
for (int i = 0; i < items.size(); i++) {
IMAPFetchDataItem item = (IMAPFetchDataItem)items.get(i);
switch (item.getType()) {
// if the envelope has been requested, we'll end up with all of these items.
case IMAPFetchDataItem.ENVELOPE:
// update the envelope and map the envelope items into the headers.
updateEnvelope((IMAPEnvelope)item);
break;
case IMAPFetchDataItem.INTERNALDATE:
receivedDate = ((IMAPInternalDate)item).getDate();;
break;
case IMAPFetchDataItem.SIZE:
size = ((IMAPMessageSize)item).size;
break;
case IMAPFetchDataItem.UID:
uid = ((IMAPUid)item).uid;
// make sure the folder knows about the UID update.
((IMAPFolder)folder).addToUidCache(new Long(uid), this);
break;
case IMAPFetchDataItem.BODYSTRUCTURE:
updateBodyStructure((IMAPBodyStructure)item);
break;
// a partial or full header update
case IMAPFetchDataItem.HEADER:
{
// if we've fetched the complete set, then replace what we have
IMAPInternetHeader h = (IMAPInternetHeader)item;
if (h.isComplete()) {
// we've got a complete header set now.
this.headers = h.headers;
allHeadersRetrieved = true;
}
else {
// need to merge the requested headers in with
// our existing set. We need to be careful, since we
// don't want to add duplicates.
mergeHeaders(h.headers);
}
}
default:
}
}
}
/**
* Merge a subset of the requested headers with our existing partial set.
* The new set will contain all headers requested from the server, plus
* any of our existing headers that were not included in the retrieved set.
*
* @param newHeaders The retrieved set of headers.
*/
protected synchronized void mergeHeaders(InternetHeaders newHeaders) {
// This is sort of tricky to manage. The input headers object is a fresh set
// retrieved from the server, but it's a subset of the headers. Our existing set
// might not be complete, but it may contain duplicates of information in the
// retrieved set, plus headers that are not in the retrieved set. To keep from
// adding duplicates, we'll only add headers that are not in the retrieved set to
// that set.
// start by running through the list of headers
Enumeration e = headers.getAllHeaders();
while (e.hasMoreElements()) {
Header header = (Header)e.nextElement();
// if there are no headers with this name in the new set, then
// we can add this. Note that to add the header, we need to
// retrieve all instances by this name and add them as a unit.
// When we hit one of the duplicates again with the enumeration,
// we'll skip it then because the merge target will have everything.
if (newHeaders.getHeader(header.getName()) == null) {
// get all occurrences of this name and stuff them into the
// new list
String name = header.getName();
String[] a = headers.getHeader(name);
for (int i = 0; i < a.length; i++) {
newHeaders.addHeader(name, a[i]);
}
}
}
// and replace the current header set
headers = newHeaders;
}
}