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