geronimo-scm mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From bsny...@apache.org
Subject svn commit: r381393 [2/5] - in /geronimo/specs/trunk/geronimo-spec-javamail/src: main/java/javax/mail/internet/ main/java/org/apache/geronimo/mail/util/ main/resources/META-INF/ test/java/javax/mail/internet/
Date Mon, 27 Feb 2006 17:38:09 GMT
Modified: geronimo/specs/trunk/geronimo-spec-javamail/src/main/java/javax/mail/internet/MimeMessage.java
URL: http://svn.apache.org/viewcvs/geronimo/specs/trunk/geronimo-spec-javamail/src/main/java/javax/mail/internet/MimeMessage.java?rev=381393&r1=381392&r2=381393&view=diff
==============================================================================
--- geronimo/specs/trunk/geronimo-spec-javamail/src/main/java/javax/mail/internet/MimeMessage.java (original)
+++ geronimo/specs/trunk/geronimo-spec-javamail/src/main/java/javax/mail/internet/MimeMessage.java Mon Feb 27 09:38:03 2006
@@ -17,20 +17,22 @@
 
 package javax.mail.internet;
 
+import java.io.BufferedInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.ObjectStreamException;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
-import java.io.ByteArrayInputStream;
-import java.io.BufferedInputStream;
-import java.io.ByteArrayOutputStream;
-import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Date;
 import java.util.Enumeration;
+import java.util.HashMap;
 import java.util.List;
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.util.Map;
+
 import javax.activation.DataHandler;
 import javax.mail.Address;
 import javax.mail.Flags;
@@ -38,12 +40,27 @@
 import javax.mail.Message;
 import javax.mail.MessagingException;
 import javax.mail.Multipart;
+import javax.mail.Part;
 import javax.mail.Session;
+import javax.mail.internet.MimeMultipart;
+import javax.mail.internet.ParseException;
+import javax.mail.internet.HeaderTokenizer.Token;
+
+import org.apache.geronimo.mail.util.SessionUtil;
+import org.apache.geronimo.mail.util.ASCIIUtil;
 
 /**
  * @version $Rev$ $Date$
  */
 public class MimeMessage extends Message implements MimePart {
+	private static final String MIME_ADDRESS_STRICT = "mail.mime.address.strict";
+	private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
+	private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
+
+	private static final String MAIL_ALTERNATES = "mail.alternates";
+	private static final String MAIL_REPLYALLCC = "mail.replyallcc";
+
+
     /**
      * Extends {@link javax.mail.Message.RecipientType} to support addition recipient types.
      */
@@ -78,7 +95,7 @@
     /**
      * This message's content (unless sourced from a SharedInputStream).
      */
-    protected byte content[];
+    protected byte[] content;
     /**
      * If the data for this message was supplied by a {@link SharedInputStream}
      * then this is another such stream representing the content of this message;
@@ -127,7 +144,7 @@
      * @throws MessagingException if there is a problem reading or parsing the stream
      */
     public MimeMessage(Session session, InputStream in) throws MessagingException {
-        super(session);
+        this(session);
         parse(in);
     }
 
@@ -139,8 +156,34 @@
      */
     public MimeMessage(MimeMessage message) throws MessagingException {
         super(message.session);
-        // todo copy the message - how?
-        throw new UnsupportedOperationException();
+        // this is somewhat difficult to do.  There's a lot of data in both the superclass and this
+        // class that needs to undergo a "deep cloning" operation.  These operations don't really exist
+        // on the objects in question, so the only solution I can come up with is to serialize the
+        // message data of the source object using the write() method, then reparse the data in this
+        // object.  I've not found a lot of uses for this particular constructor, so perhaps that's not
+        // really all that bad of a solution.
+
+        // serialized this out to an in-memory stream.
+        ByteArrayOutputStream copy = new ByteArrayOutputStream();
+
+        try {
+            // write this out the stream.
+            message.writeTo(copy);
+            copy.close();
+            // I think this ends up creating a new array for the data, but I'm not aware of any more
+            // efficient options.
+            ByteArrayInputStream inData = new ByteArrayInputStream(copy.toByteArray());
+            // now reparse this message into this object.
+            inData.close();
+            parse (inData);
+            // writing out the source data requires saving it, so we should consider this one saved also.
+            saved = true;
+        } catch (IOException e) {
+            // I'm not sure ByteArrayInput/OutputStream actually throws IOExceptions or not, but the method
+            // signatures declare it, so we need to deal with it.  Turning it into a messaging exception
+            // should fit the bill.
+            throw new MessagingException("Error copying MimeMessage data", e);
+        }
     }
 
     /**
@@ -153,7 +196,7 @@
         super(folder, number);
         headers = new InternetHeaders();
         flags = new Flags();
-        modified = true;
+        saved = true;
     }
 
     /**
@@ -165,7 +208,7 @@
      * @throws MessagingException if there is a problem reading or parsing the stream
      */
     protected MimeMessage(Folder folder, InputStream in, int number) throws MessagingException {
-        super(folder, number);
+        this(folder, number);
         parse(in);
     }
 
@@ -179,10 +222,9 @@
      * @throws MessagingException if there is a problem reading or parsing the stream
      */
     protected MimeMessage(Folder folder, InternetHeaders headers, byte[] content, int number) throws MessagingException {
-        super(folder, number);
+        this(folder, number);
         this.headers = headers;
         this.content = content;
-        modified = true;
     }
 
     /**
@@ -193,7 +235,11 @@
      */
     protected void parse(InputStream in) throws MessagingException {
         in = new BufferedInputStream(in);
+        // create the headers first from the stream
         headers = new InternetHeaders(in);
+
+        // now we need to get the rest of the content as a byte array...this means reading from the current
+        // position in the stream until the end and writing it to an accumulator ByteArrayOutputStream.
         ByteArrayOutputStream baos = new ByteArrayOutputStream();
         try {
             byte buffer[] = new byte[1024];
@@ -204,10 +250,21 @@
         } catch (Exception e) {
             throw new MessagingException(e.toString(), e);
         }
+        // and finally extract the content as a byte array.
         content = baos.toByteArray();
     }
 
+    /**
+     * 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 {
+        // strict addressing controls this.
         boolean strict = isStrictAddressing();
         Address[] result = getHeaderAsAddresses("From", strict);
         if (result == null) {
@@ -216,6 +273,15 @@
         return result;
     }
 
+    /**
+     * Set the current message "From" recipient.  This replaces any
+     * existing "From" header.  If the address is null, the header is
+     * removed.
+     *
+     * @param address The new "From" target.
+     *
+     * @exception MessagingException
+     */
     public void setFrom(Address address) throws MessagingException {
         setHeader("From", address);
     }
@@ -226,9 +292,21 @@
      * @throws MessagingException if there was a problem setting the header
      */
     public void setFrom() throws MessagingException {
-        setFrom(InternetAddress.getLocalAddress(session));
+        InternetAddress address = InternetAddress.getLocalAddress(session);
+        // no local address resolvable?  This is an error.
+        if (address == null) {
+            throw new MessagingException("No local address defined");
+        }
+        setFrom(address);
     }
 
+    /**
+     * Add a set of addresses to the existing From header.
+     *
+     * @param addresses The list to add.
+     *
+     * @exception MessagingException
+     */
     public void addFrom(Address[] addresses) throws MessagingException {
         addHeader("From", addresses);
     }
@@ -240,24 +318,55 @@
      * @throws MessagingException if there was a problem parsing the header
      */
     public Address getSender() throws MessagingException {
-        InternetAddress[] addrs = getHeaderAsAddresses("Sender", isStrictAddressing());
+        Address[] addrs = getHeaderAsAddresses("Sender", isStrictAddressing());
         return addrs.length > 0 ? addrs[0] : null;
     }
 
     /**
-     * Set the "Sender" header.
+     * Set the "Sender" header.  If the address is null, this
+     * will remove the current sender header.
      *
      * @param address the new Sender address
-     * @throws MessagingException if there was a problem setting the header
+     *
+     * @throws MessagingException
+     *                if there was a problem setting the header
      */
     public void setSender(Address address) throws MessagingException {
         setHeader("Sender", address);
     }
 
+    /**
+     * 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 {
         return getHeaderAsAddresses(getHeaderForRecipientType(type), isStrictAddressing());
     }
 
+    /**
+     * Retrieve all of the recipients defined for this message.  This
+     * returns a merged array of all possible message recipients
+     * extracted from the headers.  The relevant header types are:
+     *
+     *
+     *    javax.mail.Message.RecipientType.TO
+     *    javax.mail.Message.RecipientType.CC
+     *    javax.mail.Message.RecipientType.BCC
+     *    javax.mail.internet.MimeMessage.RecipientType.NEWSGROUPS
+     *
+     * @return An array of all target message recipients.
+     * @exception MessagingException
+     */
     public Address[] getAllRecipients() throws MessagingException {
         List recipients = new ArrayList();
         addRecipientsToList(recipients, RecipientType.TO);
@@ -267,6 +376,15 @@
         return (Address[]) recipients.toArray(new Address[recipients.size()]);
     }
 
+    /**
+     * Utility routine to merge different recipient types into a
+     * single list.
+     *
+     * @param list   The accumulator list.
+     * @param type   The recipient type to extract.
+     *
+     * @exception MessagingException
+     */
     private void addRecipientsToList(List list, Message.RecipientType type) throws MessagingException {
         Address[] recipients = getHeaderAsAddresses(getHeaderForRecipientType(type), isStrictAddressing());
         if (recipients != null) {
@@ -274,56 +392,151 @@
         }
     }
 
+    /**
+     * Set a recipients list for a particular recipient type.  If the
+     * list is null, the corresponding header is removed.
+     *
+     * @param type      The type of recipient to set.
+     * @param addresses The list of addresses.
+     *
+     * @exception MessagingException
+     */
     public void setRecipients(Message.RecipientType type, Address[] addresses) throws MessagingException {
         setHeader(getHeaderForRecipientType(type), addresses);
     }
 
+    /**
+     * Set a recipient field to a string address (which may be a
+     * list or group type).
+     *
+     * If the address is null, the field is removed.
+     *
+     * @param type    The type of recipient to set.
+     * @param address The address string.
+     *
+     * @exception MessagingException
+     */
     public void setRecipients(Message.RecipientType type, String address) throws MessagingException {
-        setHeader(getHeaderForRecipientType(type), address);
+        setOrRemoveHeader(getHeaderForRecipientType(type), address);
     }
 
+
+    /**
+     * Add a list of addresses to a target recipient list.
+     *
+     * @param type    The target recipient type.
+     * @param address An array of addresses to add.
+     *
+     * @exception MessagingException
+     */
     public void addRecipients(Message.RecipientType type, Address[] address) throws MessagingException {
-        // TODO Implement method
-        throw new UnsupportedOperationException("Method not yet implemented");
+        addHeader(getHeaderForRecipientType(type), address);
     }
 
+    /**
+     * Add an address to a target recipient list by string name.
+     *
+     * @param type    The target header type.
+     * @param address The address to add.
+     *
+     * @exception MessagingException
+     */
     public void addRecipients(Message.RecipientType type, String address) throws MessagingException {
         addHeader(getHeaderForRecipientType(type), address);
     }
 
+    /**
+     * 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 {
-        return getHeaderAsAddresses("Reply-To", isStrictAddressing());
+         Address[] addresses = getHeaderAsAddresses("Reply-To", isStrictAddressing());
+         if (addresses == null) {
+             addresses = getFrom();
+         }
+         return addresses;
     }
 
+    /**
+     * Set the Reply-To field to the provided list of addresses.  If
+     * the address list is null, the header is removed.
+     *
+     * @param address The new field value.
+     *
+     * @exception MessagingException
+     */
     public void setReplyTo(Address[] address) throws MessagingException {
         setHeader("Reply-To", address);
     }
 
+    /**
+     * 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 {
         String subject = getSingleHeader("Subject");
         if (subject == null) {
             return null;
         } else {
             try {
-                return MimeUtility.decodeText(subject);
+                // this needs to be unfolded before decodeing.
+                return MimeUtility.decodeText(ASCIIUtil.unfold(subject));
             } catch (UnsupportedEncodingException e) {
-                return subject;
+                // ignored.
             }
         }
+
+        return subject;
     }
 
+    /**
+     * Set the value for the "Subject" header.  If the subject
+     * contains non US-ASCII characters, it is encoded in RFC 2047
+     * fashion.
+     *
+     * If the subject value is null, the Subject field is removed.
+     *
+     * @param subject The new subject value.
+     *
+     * @exception MessagingException
+     */
     public void setSubject(String subject) throws MessagingException {
-        setHeader("Subject", subject);
+        // just set this using the default character set.
+        setSubject(subject, null);
     }
 
     public void setSubject(String subject, String charset) throws MessagingException {
-        try {
-            setHeader("Subject", MimeUtility.encodeText(subject, charset, null));
-        } catch (UnsupportedEncodingException e) {
-            throw new MessagingException(e.getMessage(), e);
+        // standard null removal (yada, yada, yada....)
+        if (subject == null) {
+            removeHeader("Subject");
+        }
+        else {
+            try {
+                String s = ASCIIUtil.fold(9, MimeUtility.encodeText(subject, charset, null));
+                // encode this, and then fold to fit the line lengths.
+                setHeader("Subject", ASCIIUtil.fold(9, MimeUtility.encodeText(subject, charset, null)));
+            } catch (UnsupportedEncodingException e) {
+                throw new MessagingException("Encoding error", e);
+            }
         }
     }
 
+    /**
+     * 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 {
         String value = getSingleHeader("Date");
         if (value == null) {
@@ -331,34 +544,81 @@
         }
         try {
             return dateFormat.parse(value);
-        } catch (ParseException e) {
+        } catch (java.text.ParseException e) {
             return null;
         }
     }
 
+    /**
+     * Set the message sent date.  This updates the "Date" header.
+     * If the provided date is null, the header is removed.
+     *
+     * @param sent   The new sent date value.
+     *
+     * @exception MessagingException
+     */
     public void setSentDate(Date sent) throws MessagingException {
-        if (sent == null) {
-            removeHeader("Date");
-        } else {
-            setHeader("Date", dateFormat.format(sent));
-        }
+        setOrRemoveHeader("Date", dateFormat.format(sent));
     }
 
+    /**
+     * Get the message received date.  The Sun implementation is
+     * documented as always returning null, so this one does too.
+     *
+     * @return Always returns null.
+     * @exception MessagingException
+     */
     public Date getReceivedDate() throws MessagingException {
         return null;
     }
 
+    /**
+     * Return the content size of this message.  This is obtained
+     * either from the size of the content field (if available) or
+     * from the contentStream, IFF the contentStream returns a positive
+     * size.  Returns -1 if the size is not available.
+     *
+     * @return Size of the content in bytes.
+     * @exception MessagingException
+     */
     public int getSize() throws MessagingException {
         if (content != null) {
             return content.length;
         }
+        if (contentStream != null) {
+            try {
+                int size = contentStream.available();
+                if (size > 0) {
+                    return size;
+                }
+            } catch (IOException e) {
+                // ignore
+            }
+        }
         return -1;
     }
 
+    /**
+     * Retrieve the line count for the current message.  Returns
+     * -1 if the count cannot be determined.
+     *
+     * The Sun implementation always returns -1, so this version
+     * does too.
+     *
+     * @return The content line count (always -1 in this implementation).
+     * @exception MessagingException
+     */
     public int getLineCount() throws MessagingException {
         return -1;
     }
 
+    /**
+     * 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 {
         String value = getSingleHeader("Content-Type");
         if (value == null) {
@@ -367,28 +627,108 @@
         return value;
     }
 
+
+    /**
+     * 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 {
         return new ContentType(getContentType()).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 {
-        return getSingleHeader("Content-Disposition");
+        String disp = getSingleHeader("Content-Disposition");
+        if (disp != null) {
+            return new ContentDisposition(disp).getDisposition();
+        }
+        return null;
     }
 
+
+    /**
+     * Set a new dispostion value for the "Content-Disposition" field.
+     * If the new value is null, the header is removed.
+     *
+     * @param disposition
+     *               The new disposition value.
+     *
+     * @exception MessagingException
+     */
     public void setDisposition(String disposition) throws MessagingException {
-        setHeader("Content-Disposition", disposition);
+        if (disposition == null) {
+            removeHeader("Content-Disposition");
+        }
+        else {
+            // the disposition has parameters, which we'll attempt to preserve in any existing header.
+            String currentHeader = getSingleHeader("Content-Disposition");
+            if (currentHeader != null) {
+                ContentDisposition content = new ContentDisposition(currentHeader);
+                content.setDisposition(disposition);
+                setHeader("Content-Disposition", content.toString());
+            }
+            else {
+                // set using the raw string.
+                setHeader("Content-Disposition", disposition);
+            }
+        }
     }
 
+    /**
+     * 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 {
-        return getSingleHeader("Content-Transfer-Encoding");
+        // this might require some parsing to sort out.
+        String encoding = getSingleHeader("Content-Transfer-Encoding");
+        if (encoding != null) {
+            // we need to parse this into ATOMs and other constituent parts.  We want the first
+            // ATOM token on the string.
+            HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME);
+
+            Token token = tokenizer.next();
+            while (token.getType() != Token.EOF) {
+                // if this is an ATOM type, return it.
+                if (token.getType() == Token.ATOM) {
+                    return token.getValue();
+                }
+            }
+            // not ATOMs found, just return the entire header value....somebody might be able to make sense of
+            // this.
+            return encoding;
+        }
+        // no header, nothing to return.
+        return null;
     }
 
+    /**
+     * 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 {
         return getSingleHeader("Content-ID");
     }
 
     public void setContentID(String cid) throws MessagingException {
-        setHeader("Content-ID", cid);
+        setOrRemoveHeader("Content-ID", cid);
     }
 
     public String getContentMD5() throws MessagingException {
@@ -396,20 +736,39 @@
     }
 
     public void setContentMD5(String md5) throws MessagingException {
-        setHeader("Content-MD5", md5);
+        setOrRemoveHeader("Content-MD5", md5);
     }
 
     public String getDescription() throws MessagingException {
-        return getSingleHeader("Content-Description");
+        String description = getSingleHeader("Content-Description");
+        if (description != null) {
+            try {
+                // this could be both folded and encoded.  Return this to usable form.
+                return MimeUtility.decodeText(ASCIIUtil.unfold(description));
+            } catch (UnsupportedEncodingException e) {
+                // ignore
+            }
+        }
+        // return the raw version for any errors.
+        return description;
     }
 
     public void setDescription(String description) throws MessagingException {
-        setHeader("Content-Description", description);
+        setDescription(description, null);
     }
 
     public void setDescription(String description, String charset) throws MessagingException {
-        // todo encoding
-        setHeader("Content-Description", description);
+        if (description == null) {
+            removeHeader("Content-Description");
+        }
+        else {
+            try {
+                setHeader("Content-Description", ASCIIUtil.fold(21, MimeUtility.encodeText(description, charset, null)));
+            } catch (UnsupportedEncodingException e) {
+                throw new MessagingException(e.getMessage(), e);
+            }
+        }
+
     }
 
     public String[] getContentLanguage() throws MessagingException {
@@ -417,7 +776,7 @@
     }
 
     public void setContentLanguage(String[] languages) throws MessagingException {
-        if (languages == null || languages.length == 0) {
+        if (languages == null) {
             removeHeader("Content-Language");
         } else if (languages.length == 1) {
             setHeader("Content-Language", languages[0]);
@@ -436,13 +795,61 @@
     }
 
     public String getFileName() throws MessagingException {
-        // TODO Implement method
-        throw new UnsupportedOperationException("Method not yet implemented");
+        // see if there is a disposition.  If there is, parse off the filename parameter.
+        String disposition = getDisposition();
+        String filename = null;
+
+        if (disposition != null) {
+            filename = new ContentDisposition(disposition).getParameter("filename");
+        }
+
+        // if there's no filename on the disposition, there might be a name parameter on a
+        // Content-Type header.
+        if (filename == null) {
+            String type = getContentType();
+            if (type != null) {
+                try {
+                    filename = new ContentType(type).getParameter("name");
+                } catch (ParseException e) {
+                }
+            }
+        }
+        // if we have a name, we might need to decode this if an additional property is set.
+        if (filename != null && SessionUtil.getBooleanProperty(session, MIME_DECODEFILENAME, false)) {
+            try {
+                filename = MimeUtility.decodeText(filename);
+            } catch (UnsupportedEncodingException e) {
+                throw new MessagingException("Unable to decode filename", e);
+            }
+        }
+
+        return filename;
     }
 
+
     public void setFileName(String name) throws MessagingException {
-        // TODO Implement method
-        throw new UnsupportedOperationException("Method not yet implemented");
+        // there's an optional session property that requests file name encoding...we need to process this before
+        // setting the value.
+        if (name != null && SessionUtil.getBooleanProperty(session, MIME_ENCODEFILENAME, false)) {
+            try {
+                name = MimeUtility.encodeText(name);
+            } catch (UnsupportedEncodingException e) {
+                throw new MessagingException("Unable to encode filename", e);
+            }
+        }
+
+        // get the disposition string.
+        String disposition = getDisposition();
+        // if not there, then this is an attachment.
+        if (disposition == null) {
+            disposition = Part.ATTACHMENT;
+        }
+        // now create a disposition object and set the parameter.
+        ContentDisposition contentDisposition = new ContentDisposition(disposition);
+        contentDisposition.setParameter("filename", name);
+
+        // serialize this back out and reset.
+        setDisposition(contentDisposition.toString());
     }
 
     public InputStream getInputStream() throws MessagingException, IOException {
@@ -450,6 +857,10 @@
     }
 
     protected InputStream getContentStream() throws MessagingException {
+        if (contentStream != null) {
+            return contentStream;
+        }
+
         if (content != null) {
             return new ByteArrayInputStream(content);
         } else {
@@ -481,11 +892,21 @@
     }
 
     public void setText(String text) throws MessagingException {
-        setText(text, MimeUtility.getDefaultJavaCharset());
+        setText(text, null);
     }
 
     public void setText(String text, String charset) throws MessagingException {
-        setContent(text, "text/plain; charset=" + charset);
+        // we need to sort out the character set if one is not provided.
+        if (charset == null) {
+            // if we have non us-ascii characters here, we need to adjust this.
+            if (!ASCIIUtil.isAscii(text)) {
+                charset = MimeUtility.getDefaultMIMECharset();
+            }
+            else {
+                charset = "us-ascii";
+            }
+        }
+        setContent(text, "text/plain; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME));
     }
 
     public void setContent(Multipart part) throws MessagingException {
@@ -494,48 +915,292 @@
     }
 
     public Message reply(boolean replyToAll) throws MessagingException {
-        // TODO Implement method
-        throw new UnsupportedOperationException("Method not yet implemented");
+        // create a new message in this session.
+        MimeMessage reply = new MimeMessage(session);
+
+        // get the header and add the "Re:" bit, if necessary.
+        String newSubject = getSubject();
+        if (newSubject != null) {
+            // check to see if it already begins with "Re: " (in any case).
+            // Add one on if we don't have it yet.
+            if (!newSubject.regionMatches(true, 0, "Re: ", 0, 4)) {
+                newSubject = "Re: " + newSubject;
+            }
+            reply.setSubject(newSubject);
+        }
+
+        Address[] toRecipients = getReplyTo();
+
+        // set the target recipients the replyTo value
+        reply.setRecipients(Message.RecipientType.TO, getReplyTo());
+
+        // need to reply to everybody?  More things to add.
+        if (replyToAll) {
+            // when replying, we want to remove "duplicates" in the final list.
+
+            HashMap masterList = new HashMap();
+
+            // reply to all implies add the local sender.  Add this to the list if resolveable.
+            InternetAddress localMail = InternetAddress.getLocalAddress(session);
+            if (localMail != null) {
+                masterList.put(localMail.getAddress(), localMail);
+            }
+            // see if we have some local aliases to deal with.
+            String alternates = session.getProperty(MAIL_ALTERNATES);
+            if (alternates != null) {
+                // parse this string list and merge with our set.
+                Address[] alternateList = InternetAddress.parse(alternates, false);
+                mergeAddressList(masterList, alternateList);
+            }
+
+            // the master list now contains an a list of addresses we will exclude from
+            // the addresses.  From this point on, we're going to prune any additional addresses
+            // against this list, AND add any new addresses to the list
+
+            // now merge in the main recipients, and merge in the other recipents as well
+            Address[] toList = pruneAddresses(masterList, getRecipients(Message.RecipientType.TO));
+            if (toList.length != 0) {
+                // now check to see what sort of reply we've been asked to send.
+                // if replying to all as a CC, then we need to add to the CC list, otherwise they are
+                // TO recipients.
+                if (SessionUtil.getBooleanProperty(session, MAIL_REPLYALLCC, false)) {
+                    reply.addRecipients(Message.RecipientType.CC, toList);
+                }
+                else {
+                    reply.addRecipients(Message.RecipientType.TO, toList);
+                }
+            }
+            // and repeat for the CC list.
+            toList = pruneAddresses(masterList, getRecipients(Message.RecipientType.CC));
+            if (toList.length != 0) {
+                reply.addRecipients(Message.RecipientType.CC, toList);
+            }
+
+            // a news group list is separate from the normal addresses.  We just take these recepients
+            // asis without trying to prune duplicates.
+            toList = getRecipients(RecipientType.NEWSGROUPS);
+            if (toList != null && toList.length != 0) {
+                reply.addRecipients(RecipientType.NEWSGROUPS, toList);
+            }
+        }
+
+        // this is a bit of a pain.  We can't set the flags here by specifying the system flag, we need to
+        // construct a flag item instance inorder to set it.
+
+        // this is an answered email.
+        setFlags(new Flags(Flags.Flag.ANSWERED), true);
+        // all done, return the constructed Message object.
+        return reply;
+    }
+
+
+    /**
+     * Merge a set of addresses into a master accumulator list, eliminating
+     * duplicates.
+     *
+     * @param master The set of addresses we've accumulated so far.
+     * @param list   The list of addresses to merge in.
+     */
+    private void mergeAddressList(Map master, Address[] list) {
+        // make sure we have a list.
+        if (list == null) {
+            return;
+        }
+        for (int i = 0; i < list.length; i++) {
+            InternetAddress address = (InternetAddress)list[i];
+
+            // if not in the master list already, add it now.
+            if (!master.containsKey(address.getAddress())) {
+                master.put(address.getAddress(), address);
+            }
+        }
+    }
+
+
+    /**
+     * Prune a list of addresses against our master address list,
+     * returning the "new" addresses.  The master list will be
+     * updated with this new set of addresses.
+     *
+     * @param master The master address list of addresses we've seen before.
+     * @param list   The new list of addresses to prune.
+     *
+     * @return An array of addresses pruned of any duplicate addresses.
+     */
+    private Address[] pruneAddresses(Map master, Address[] list) {
+        // return an empy array if we don't get an input list.
+        if (list == null) {
+            return new Address[0];
+        }
+
+        // optimistically assume there are no addresses to eliminate (common).
+        ArrayList prunedList = new ArrayList(list.length);
+        for (int i = 0; i < list.length; i++) {
+            InternetAddress address = (InternetAddress)list[i];
+
+            // if not in the master list, this is a new one.  Add to both the master list and
+            // the pruned list.
+            if (!master.containsKey(address.getAddress())) {
+                master.put(address.getAddress(), address);
+                prunedList.add(address);
+            }
+        }
+        // convert back to list form.
+        return (Address[])prunedList.toArray(new Address[0]);
     }
 
+
+    /**
+     * Write the message out to a stream in RFC 822 format.
+     *
+     * @param out    The target output stream.
+     *
+     * @exception MessagingException
+     * @exception IOException
+     */
     public void writeTo(OutputStream out) throws MessagingException, IOException {
         writeTo(out, null);
     }
 
+    /**
+     * Write the message out to a target output stream, excluding the
+     * specified message headers.
+     *
+     * @param out    The target output stream.
+     * @param ignoreHeaders
+     *               An array of header types to ignore.  This can be null, which means
+     *               write out all headers.
+     *
+     * @exception MessagingException
+     * @exception IOException
+     */
     public void writeTo(OutputStream out, String[] ignoreHeaders) throws MessagingException, IOException {
         if (!saved) {
             saveChanges();
         }
+        // write out the headers first
         headers.writeTo(out, ignoreHeaders);
-        out.write(13);
-        out.write(10);
+        // add the separater between the headers and the data portion.
+        out.write('\r');
+        out.write('\n');
         if (modified) {
             dh.writeTo(MimeUtility.encode(out, getEncoding()));
         } else {
-            out.write(content);
+            // if we have content directly, we can write this out now.
+            if (content != null) {
+                out.write(content);
+            }
+            else {
+                // see if we can get a content stream for this message.  We might have had one
+                // explicitly set, or a subclass might override the get method to provide one.
+                InputStream in = getContentStream();
+
+                byte[] buffer = new byte[8192];
+                int length = in.read(buffer);
+                // copy the data stream-to-stream.
+                while (length > 0) {
+                    out.write(buffer, 0, length);
+                    length = in.read(buffer);
+                }
+                in.close();
+            }
         }
+        // flush any data we wrote out, but do not close the stream.  That's the caller's duty.
+        out.flush();
     }
 
+
+    /**
+     * Retrieve all headers that match a given name.
+     *
+     * @param name   The target name.
+     *
+     * @return The set of headers that match the given name.  These headers
+     *         will be the decoded() header values if these are RFC 2047
+     *         encoded.
+     * @exception MessagingException
+     */
     public String[] getHeader(String name) throws MessagingException {
         return headers.getHeader(name);
     }
 
+    /**
+     * Get all headers that match a particular name, as a single string.
+     * Individual headers are separated by the provided delimiter.   If
+     * the delimiter is null, only the first header is returned.
+     *
+     * @param name      The source header name.
+     * @param delimiter The delimiter string to be used between headers.  If null, only
+     *                  the first is returned.
+     *
+     * @return The headers concatenated as a single string.
+     * @exception MessagingException
+     */
     public String getHeader(String name, String delimiter) throws MessagingException {
         return headers.getHeader(name, delimiter);
     }
 
+    /**
+     * Set a new value for a named header.
+     *
+     * @param name   The name of the target header.
+     * @param value  The new value for the header.
+     *
+     * @exception MessagingException
+     */
     public void setHeader(String name, String value) throws MessagingException {
         headers.setHeader(name, value);
     }
 
+    /**
+     * Conditionally set or remove a named header.  If the new value
+     * is null, the header is removed.
+     *
+     * @param name   The header name.
+     * @param value  The new header value.  A null value causes the header to be
+     *               removed.
+     *
+     * @exception MessagingException
+     */
+    private void setOrRemoveHeader(String name, String value) throws MessagingException {
+        if (value == null) {
+            headers.removeHeader(name);
+        }
+        else {
+            headers.setHeader(name, value);
+        }
+    }
+
+    /**
+     * Add a new value to an existing header.  The added value is
+     * created as an additional header of the same type and value.
+     *
+     * @param name   The name of the target header.
+     * @param value  The removed header.
+     *
+     * @exception MessagingException
+     */
     public void addHeader(String name, String value) throws MessagingException {
         headers.addHeader(name, value);
     }
 
+    /**
+     * Remove a header with the given name.
+     *
+     * @param name   The name of the removed header.
+     *
+     * @exception MessagingException
+     */
     public void removeHeader(String name) throws MessagingException {
         headers.removeHeader(name);
     }
 
+    /**
+     * Retrieve the complete list of message headers, as an enumeration.
+     *
+     * @return An Enumeration of the message headers.
+     * @exception MessagingException
+     */
     public Enumeration getAllHeaders() throws MessagingException {
         return headers.getAllHeaders();
     }
@@ -572,49 +1237,173 @@
         return flags.contains(flag);
     }
 
-    public synchronized void setFlags(Flags flags, boolean set) throws MessagingException {
-        // TODO Implement method
-        throw new UnsupportedOperationException("Method not yet implemented");
+    /**
+     * 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 {
+        if (set) {
+            flags.add(flag);
+        }
+        else {
+            flags.remove(flag);
+        }
     }
 
+    /**
+     * Saves any changes on this message.  When called, the modified
+     * and saved flags are set to true and updateHeaders() is called
+     * to force updates.
+     *
+     * @exception MessagingException
+     */
     public void saveChanges() throws MessagingException {
+        modified = true;
+        saved = true;
+        // update message headers from the content.
         updateHeaders();
     }
 
+    /**
+     * Update the internet headers so that they make sense.  This
+     * will attempt to make sense of the message content type
+     * given the state of the content.
+     *
+     * @exception MessagingException
+     */
     protected void updateHeaders() throws MessagingException {
+        DataHandler handler = getDataHandler();
+
+        try {
+            // figure out the content type.  If not set, we'll need to figure this out.
+            String type = getContentType();
+            // parse this content type out so we can do matches/compares.
+            ContentType content = new ContentType(type);
+            // is this a multipart content?
+            if (content.match("multipart/*")) {
+                // the content is suppose to be a MimeMultipart.  Ping it to update it's headers as well.
+                try {
+                    MimeMultipart part = (MimeMultipart)handler.getContent();
+                    part.updateHeaders();
+                } catch (ClassCastException e) {
+                    throw new MessagingException("Message content is not MimeMultipart", e);
+                }
+            }
+            else if (!content.match("message/rfc822")) {
+                // simple part, we need to update the header type information
+                // if no encoding is set yet, figure this out from the data handler content.
+                if (getHeader("Content-Transfer-Encoding") == null) {
+                    setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
+                }
+
+                // is a content type header set?  Check the property to see if we need to set this.
+                if (getSingleHeader("Content-Type") == null) {
+                    if (SessionUtil.getBooleanProperty(session, "MIME_MAIL_SETDEFAULTTEXTCHARSET", true)) {
+                        // is this a text type?  Figure out the encoding and make sure it is set.
+                        if (content.match("text/*")) {
+                            // the charset should be specified as a parameter on the MIME type.  If not there,
+                            // try to figure one out.
+                            if (content.getParameter("charset") == null) {
+
+                                String encoding = getEncoding();
+                                // if we're sending this as 7-bit ASCII, our character set need to be
+                                // compatible.
+                                if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
+                                    content.setParameter("charset", "us-ascii");
+                                }
+                                else {
+                                    // get the global default.
+                                    content.setParameter("charset", MimeUtility.getDefaultMIMECharset());
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            // if we don't have a content type header, then create one.
+            if (getSingleHeader("Content-Type") == null) {
+                // get the disposition header, and if it is there, copy the filename parameter into the
+                // name parameter of the type.
+                String disp = getSingleHeader("Content-Disposition");
+                if (disp != null) {
+                    // parse up the string value of the disposition
+                    ContentDisposition disposition = new ContentDisposition(disp);
+                    // now check for a filename value
+                    String filename = disposition.getParameter("filename");
+                    // copy and rename the parameter, if it exists.
+                    if (filename != null) {
+                        content.setParameter("name", filename);
+                    }
+                }
+                // set the header with the updated content type information.
+                setHeader("Content-Type", content.toString());
+            }
+
+        } catch (IOException e) {
+            throw new MessagingException("Error updating message headers", e);
+        }
     }
 
+
     protected InternetHeaders createInternetHeaders(InputStream in) throws MessagingException {
-        // TODO Implement method
-        throw new UnsupportedOperationException("Method not yet implemented");
+        // internet headers has a constructor for just this purpose
+        return new InternetHeaders(in);
     }
 
-    private InternetAddress[] getHeaderAsAddresses(String header, boolean strict) throws MessagingException {
+    private Address[] getHeaderAsAddresses(String header, boolean strict) throws MessagingException {
         return headers.getHeaderAsAddresses(header, strict);
     }
 
+    /**
+     * Check to see if we require strict addressing on parsing
+     * internet headers.
+     *
+     * @return The current value of the "mail.mime.address.strict" session
+     *         property, or true, if the property is not set.
+     */
     private boolean isStrictAddressing() {
-        String property = session.getProperty("mail.mime.address.strict");
-        return property == null ? true : Boolean.valueOf(property).booleanValue();
+        return SessionUtil.getBooleanProperty(session, MIME_ADDRESS_STRICT, true);
     }
 
-    private void setHeader(String header, Address address) {
+    /**
+     * Set a named header to the value of an address field.
+     *
+     * @param header  The header name.
+     * @param address The address value.  If the address is null, the header is removed.
+     *
+     * @exception MessagingException
+     */
+    private void setHeader(String header, Address address) throws MessagingException {
         if (address == null) {
-            headers.removeHeader(header);
-        } else {
-            headers.setHeader(header, address.toString());
+            removeHeader(header);
+        }
+        else {
+            setHeader(header, address.toString());
         }
     }
 
+    /**
+     * Set a header to a list of addresses.
+     *
+     * @param header    The header name.
+     * @param addresses An array of addresses to set the header to.  If null, the
+     *                  header is removed.
+     */
     private void setHeader(String header, Address[] addresses) {
         if (addresses == null) {
             headers.removeHeader(header);
-        } else {
+        }
+        else {
             headers.setHeader(header, addresses);
         }
     }
 
-    private void addHeader(String header, Address[] addresses) {
+    private void addHeader(String header, Address[] addresses) throws MessagingException {
         headers.addHeader(header, InternetAddress.toString(addresses));
     }
 
@@ -632,6 +1421,16 @@
         }
     }
 
+    /**
+     * Utility routine to get a header as a single string value
+     * rather than an array of headers.
+     *
+     * @param name   The name of the header.
+     *
+     * @return The single string header value.  If multiple headers exist,
+     *         the additional ones are ignored.
+     * @exception MessagingException
+     */
     private String getSingleHeader(String name) throws MessagingException {
         String[] values = getHeader(name);
         if (values == null || values.length == 0) {



Mime
View raw message