View Javadoc

1   /**
2    *
3    * Copyright 2003-2004 The Apache Software Foundation
4    *
5    *  Licensed under the Apache License, Version 2.0 (the "License");
6    *  you may not use this file except in compliance with the License.
7    *  You may obtain a copy of the License at
8    *
9    *     http://www.apache.org/licenses/LICENSE-2.0
10   *
11   *  Unless required by applicable law or agreed to in writing, software
12   *  distributed under the License is distributed on an "AS IS" BASIS,
13   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   *  See the License for the specific language governing permissions and
15   *  limitations under the License.
16   */
17  
18  package javax.mail.internet;
19  
20  import java.io.BufferedInputStream;
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.ObjectStreamException;
26  import java.io.OutputStream;
27  import java.io.UnsupportedEncodingException;
28  import java.util.ArrayList;
29  import java.util.Arrays;
30  import java.util.Date;
31  import java.util.Enumeration;
32  import java.util.HashMap;
33  import java.util.List;
34  import java.util.Map;
35  
36  import javax.activation.DataHandler;
37  import javax.mail.Address;
38  import javax.mail.Flags;
39  import javax.mail.Folder;
40  import javax.mail.Message;
41  import javax.mail.MessagingException;
42  import javax.mail.Multipart;
43  import javax.mail.Part;
44  import javax.mail.Session;
45  import javax.mail.internet.HeaderTokenizer.Token;
46  
47  import org.apache.geronimo.mail.util.ASCIIUtil;
48  import org.apache.geronimo.mail.util.SessionUtil;
49  
50  /**
51   * @version $Rev: 398618 $ $Date: 2006-05-01 08:18:57 -0700 (Mon, 01 May 2006) $
52   */
53  public class MimeMessage extends Message implements MimePart {
54  	private static final String MIME_ADDRESS_STRICT = "mail.mime.address.strict";
55  	private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
56  	private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
57  
58  	private static final String MAIL_ALTERNATES = "mail.alternates";
59  	private static final String MAIL_REPLYALLCC = "mail.replyallcc";
60  
61  
62      /**
63       * Extends {@link javax.mail.Message.RecipientType} to support addition recipient types.
64       */
65      public static class RecipientType extends Message.RecipientType {
66          /**
67           * Recipient type for Usenet news.
68           */
69          public static final RecipientType NEWSGROUPS = new RecipientType("Newsgroups");
70  
71          protected RecipientType(String type) {
72              super(type);
73          }
74  
75          /**
76           * Ensure the singleton is returned.
77           *
78           * @return resolved object
79           */
80          protected Object readResolve() throws ObjectStreamException {
81              if (this.type.equals("Newsgroups")) {
82                  return NEWSGROUPS;
83              } else {
84                  return super.readResolve();
85              }
86          }
87      }
88  
89      /**
90       * The {@link DataHandler} for this Message's content.
91       */
92      protected DataHandler dh;
93      /**
94       * This message's content (unless sourced from a SharedInputStream).
95       */
96      protected byte[] content;
97      /**
98       * If the data for this message was supplied by a {@link SharedInputStream}
99       * then this is another such stream representing the content of this message;
100      * if this field is non-null, then {@link #content} will be null.
101      */
102     protected InputStream contentStream;
103     /**
104      * This message's headers.
105      */
106     protected InternetHeaders headers;
107     /**
108      * This message's flags.
109      */
110     protected Flags flags;
111     /**
112      * Flag indicating that the message has been modified; set to true when
113      * an empty message is created or when {@link #saveChanges()} is called.
114      */
115     protected boolean modified;
116     /**
117      * Flag indicating that the message has been saved.
118      */
119     protected boolean saved;
120 
121     private final MailDateFormat dateFormat = new MailDateFormat();
122 
123     /**
124      * Create a new MimeMessage.
125      * An empty message is created, with empty {@link #headers} and empty {@link #flags}.
126      * The {@link #modified} flag is set.
127      *
128      * @param session the session for this message
129      */
130     public MimeMessage(Session session) {
131         super(session);
132         headers = new InternetHeaders();
133         flags = new Flags();
134         // empty messages are modified, because the content is not there, and require saving before use.
135         modified = true;
136         saved = false;
137     }
138 
139     /**
140      * Create a MimeMessage by reading an parsing the data from the supplied stream.
141      *
142      * @param session the session for this message
143      * @param in      the stream to load from
144      * @throws MessagingException if there is a problem reading or parsing the stream
145      */
146     public MimeMessage(Session session, InputStream in) throws MessagingException {
147         this(session);
148         parse(in);
149         // this message is complete, so marked as unmodified.
150         modified = false;
151         // and no saving required
152         saved = true;
153     }
154 
155     /**
156      * Copy a MimeMessage.
157      *
158      * @param message the message to copy
159      * @throws MessagingException is there was a problem copying the message
160      */
161     public MimeMessage(MimeMessage message) throws MessagingException {
162         super(message.session);
163         // this is somewhat difficult to do.  There's a lot of data in both the superclass and this
164         // class that needs to undergo a "deep cloning" operation.  These operations don't really exist
165         // on the objects in question, so the only solution I can come up with is to serialize the
166         // message data of the source object using the write() method, then reparse the data in this
167         // object.  I've not found a lot of uses for this particular constructor, so perhaps that's not
168         // really all that bad of a solution.
169 
170         // serialized this out to an in-memory stream.
171         ByteArrayOutputStream copy = new ByteArrayOutputStream();
172 
173         try {
174             // write this out the stream.
175             message.writeTo(copy);
176             copy.close();
177             // I think this ends up creating a new array for the data, but I'm not aware of any more
178             // efficient options.
179             ByteArrayInputStream inData = new ByteArrayInputStream(copy.toByteArray());
180             // now reparse this message into this object.
181             inData.close();
182             parse (inData);
183             // writing out the source data requires saving it, so we should consider this one saved also.
184             saved = true;
185             // this message is complete, so marked as unmodified.
186             modified = false;
187         } catch (IOException e) {
188             // I'm not sure ByteArrayInput/OutputStream actually throws IOExceptions or not, but the method
189             // signatures declare it, so we need to deal with it.  Turning it into a messaging exception
190             // should fit the bill.
191             throw new MessagingException("Error copying MimeMessage data", e);
192         }
193     }
194 
195     /**
196      * Create an new MimeMessage in the supplied {@link Folder} and message number.
197      *
198      * @param folder the Folder that contains the new message
199      * @param number the message number of the new message
200      */
201     protected MimeMessage(Folder folder, int number) {
202         super(folder, number);
203         headers = new InternetHeaders();
204         flags = new Flags();
205         // saving primarly involves updates to the message header.  Since we're taking the header info
206         // from a message store in this context, we mark the message as saved.
207         saved = true;
208         // we've not filled in the content yet, so this needs to be marked as modified
209         modified = true;
210     }
211 
212     /**
213      * Create a MimeMessage by reading an parsing the data from the supplied stream.
214      *
215      * @param folder the folder for this message
216      * @param in     the stream to load from
217      * @param number the message number of the new message
218      * @throws MessagingException if there is a problem reading or parsing the stream
219      */
220     protected MimeMessage(Folder folder, InputStream in, int number) throws MessagingException {
221         this(folder, number);
222         parse(in);
223         // this message is complete, so marked as unmodified.
224         modified = false;
225         // and no saving required
226         saved = true;
227     }
228 
229 
230     /**
231      * Create a MimeMessage with the supplied headers and content.
232      *
233      * @param folder  the folder for this message
234      * @param headers the headers for the new message
235      * @param content the content of the new message
236      * @param number  the message number of the new message
237      * @throws MessagingException if there is a problem reading or parsing the stream
238      */
239     protected MimeMessage(Folder folder, InternetHeaders headers, byte[] content, int number) throws MessagingException {
240         this(folder, number);
241         this.headers = headers;
242         this.content = content;
243         // this message is complete, so marked as unmodified.
244         modified = false;
245     }
246 
247     /**
248      * Parse the supplied stream and initialize {@link #headers} and {@link #content} appropriately.
249      *
250      * @param in the stream to read
251      * @throws MessagingException if there was a problem parsing the stream
252      */
253     protected void parse(InputStream in) throws MessagingException {
254         in = new BufferedInputStream(in);
255         // create the headers first from the stream
256         headers = new InternetHeaders(in);
257 
258         // now we need to get the rest of the content as a byte array...this means reading from the current
259         // position in the stream until the end and writing it to an accumulator ByteArrayOutputStream.
260         ByteArrayOutputStream baos = new ByteArrayOutputStream();
261         try {
262             byte buffer[] = new byte[1024];
263             int count;
264             while ((count = in.read(buffer, 0, 1024)) != -1) {
265                 baos.write(buffer, 0, count);
266             }
267         } catch (Exception e) {
268             throw new MessagingException(e.toString(), e);
269         }
270         // and finally extract the content as a byte array.
271         content = baos.toByteArray();
272     }
273 
274     /**
275      * Get the message "From" addresses.  This looks first at the
276      * "From" headers, and no "From" header is found, the "Sender"
277      * header is checked.  Returns null if not found.
278      *
279      * @return An array of addresses identifying the message from target.  Returns
280      *         null if this is not resolveable from the headers.
281      * @exception MessagingException
282      */
283     public Address[] getFrom() throws MessagingException {
284         // strict addressing controls this.
285         boolean strict = isStrictAddressing();
286         Address[] result = getHeaderAsInternetAddresses("From", strict);
287         if (result == null) {
288             result = getHeaderAsInternetAddresses("Sender", strict);
289         }
290         return result;
291     }
292 
293     /**
294      * Set the current message "From" recipient.  This replaces any
295      * existing "From" header.  If the address is null, the header is
296      * removed.
297      *
298      * @param address The new "From" target.
299      *
300      * @exception MessagingException
301      */
302     public void setFrom(Address address) throws MessagingException {
303         setHeader("From", address);
304     }
305 
306     /**
307      * Set the "From" header using the value returned by {@link InternetAddress#getLocalAddress(javax.mail.Session)}.
308      *
309      * @throws MessagingException if there was a problem setting the header
310      */
311     public void setFrom() throws MessagingException {
312         InternetAddress address = InternetAddress.getLocalAddress(session);
313         // no local address resolvable?  This is an error.
314         if (address == null) {
315             throw new MessagingException("No local address defined");
316         }
317         setFrom(address);
318     }
319 
320     /**
321      * Add a set of addresses to the existing From header.
322      *
323      * @param addresses The list to add.
324      *
325      * @exception MessagingException
326      */
327     public void addFrom(Address[] addresses) throws MessagingException {
328         addHeader("From", addresses);
329     }
330 
331     /**
332      * Return the "Sender" header as an address.
333      *
334      * @return the "Sender" header as an address, or null if not present
335      * @throws MessagingException if there was a problem parsing the header
336      */
337     public Address getSender() throws MessagingException {
338         Address[] addrs = getHeaderAsInternetAddresses("Sender", isStrictAddressing());
339         return addrs.length > 0 ? addrs[0] : null;
340     }
341 
342     /**
343      * Set the "Sender" header.  If the address is null, this
344      * will remove the current sender header.
345      *
346      * @param address the new Sender address
347      *
348      * @throws MessagingException
349      *                if there was a problem setting the header
350      */
351     public void setSender(Address address) throws MessagingException {
352         setHeader("Sender", address);
353     }
354 
355     /**
356      * Gets the recipients by type.  Returns null if there are no
357      * headers of the specified type.  Acceptable RecipientTypes are:
358      *
359      *   javax.mail.Message.RecipientType.TO
360      *   javax.mail.Message.RecipientType.CC
361      *   javax.mail.Message.RecipientType.BCC
362      *   javax.mail.internet.MimeMessage.RecipientType.NEWSGROUPS
363      *
364      * @param type   The message RecipientType identifier.
365      *
366      * @return The array of addresses for the specified recipient types.
367      * @exception MessagingException
368      */
369     public Address[] getRecipients(Message.RecipientType type) throws MessagingException {
370         // is this a NEWSGROUP request?  We need to handle this as a special case here, because
371         // this needs to return NewsAddress instances instead of InternetAddress items.
372         if (type == RecipientType.NEWSGROUPS) {
373             return getHeaderAsNewsAddresses(getHeaderForRecipientType(type));
374         }
375         // the other types are all internet addresses.
376         return getHeaderAsInternetAddresses(getHeaderForRecipientType(type), isStrictAddressing());
377     }
378 
379     /**
380      * Retrieve all of the recipients defined for this message.  This
381      * returns a merged array of all possible message recipients
382      * extracted from the headers.  The relevant header types are:
383      *
384      *
385      *    javax.mail.Message.RecipientType.TO
386      *    javax.mail.Message.RecipientType.CC
387      *    javax.mail.Message.RecipientType.BCC
388      *    javax.mail.internet.MimeMessage.RecipientType.NEWSGROUPS
389      *
390      * @return An array of all target message recipients.
391      * @exception MessagingException
392      */
393     public Address[] getAllRecipients() throws MessagingException {
394         List recipients = new ArrayList();
395         addRecipientsToList(recipients, RecipientType.TO);
396         addRecipientsToList(recipients, RecipientType.CC);
397         addRecipientsToList(recipients, RecipientType.BCC);
398         addRecipientsToList(recipients, RecipientType.NEWSGROUPS);
399         return (Address[]) recipients.toArray(new Address[recipients.size()]);
400     }
401 
402     /**
403      * Utility routine to merge different recipient types into a
404      * single list.
405      *
406      * @param list   The accumulator list.
407      * @param type   The recipient type to extract.
408      *
409      * @exception MessagingException
410      */
411     private void addRecipientsToList(List list, Message.RecipientType type) throws MessagingException {
412 
413         Address[] recipients;
414         if (type == RecipientType.NEWSGROUPS) {
415             recipients = getHeaderAsNewsAddresses(getHeaderForRecipientType(type));
416         }
417         else {
418             recipients = getHeaderAsInternetAddresses(getHeaderForRecipientType(type), isStrictAddressing());
419         }
420         if (recipients != null) {
421             list.addAll(Arrays.asList(recipients));
422         }
423     }
424 
425     /**
426      * Set a recipients list for a particular recipient type.  If the
427      * list is null, the corresponding header is removed.
428      *
429      * @param type      The type of recipient to set.
430      * @param addresses The list of addresses.
431      *
432      * @exception MessagingException
433      */
434     public void setRecipients(Message.RecipientType type, Address[] addresses) throws MessagingException {
435         setHeader(getHeaderForRecipientType(type), addresses);
436     }
437 
438     /**
439      * Set a recipient field to a string address (which may be a
440      * list or group type).
441      *
442      * If the address is null, the field is removed.
443      *
444      * @param type    The type of recipient to set.
445      * @param address The address string.
446      *
447      * @exception MessagingException
448      */
449     public void setRecipients(Message.RecipientType type, String address) throws MessagingException {
450         setOrRemoveHeader(getHeaderForRecipientType(type), address);
451     }
452 
453 
454     /**
455      * Add a list of addresses to a target recipient list.
456      *
457      * @param type    The target recipient type.
458      * @param address An array of addresses to add.
459      *
460      * @exception MessagingException
461      */
462     public void addRecipients(Message.RecipientType type, Address[] address) throws MessagingException {
463         addHeader(getHeaderForRecipientType(type), address);
464     }
465 
466     /**
467      * Add an address to a target recipient list by string name.
468      *
469      * @param type    The target header type.
470      * @param address The address to add.
471      *
472      * @exception MessagingException
473      */
474     public void addRecipients(Message.RecipientType type, String address) throws MessagingException {
475         addHeader(getHeaderForRecipientType(type), address);
476     }
477 
478     /**
479      * Get the ReplyTo address information.  The headers are parsed
480      * using the "mail.mime.address.strict" setting.  If the "Reply-To" header does
481      * not have any addresses, then the value of the "From" field is used.
482      *
483      * @return An array of addresses obtained from parsing the header.
484      * @exception MessagingException
485      */
486     public Address[] getReplyTo() throws MessagingException {
487          Address[] addresses = getHeaderAsInternetAddresses("Reply-To", isStrictAddressing());
488          if (addresses == null) {
489              addresses = getFrom();
490          }
491          return addresses;
492     }
493 
494     /**
495      * Set the Reply-To field to the provided list of addresses.  If
496      * the address list is null, the header is removed.
497      *
498      * @param address The new field value.
499      *
500      * @exception MessagingException
501      */
502     public void setReplyTo(Address[] address) throws MessagingException {
503         setHeader("Reply-To", address);
504     }
505 
506     /**
507      * Returns the value of the "Subject" header.  If the subject
508      * is encoded as an RFC 2047 value, the value is decoded before
509      * return.  If decoding fails, the raw string value is
510      * returned.
511      *
512      * @return The String value of the subject field.
513      * @exception MessagingException
514      */
515     public String getSubject() throws MessagingException {
516         String subject = getSingleHeader("Subject");
517         if (subject == null) {
518             return null;
519         } else {
520             try {
521                 // this needs to be unfolded before decodeing.
522                 return MimeUtility.decodeText(ASCIIUtil.unfold(subject));
523             } catch (UnsupportedEncodingException e) {
524                 // ignored.
525             }
526         }
527 
528         return subject;
529     }
530 
531     /**
532      * Set the value for the "Subject" header.  If the subject
533      * contains non US-ASCII characters, it is encoded in RFC 2047
534      * fashion.
535      *
536      * If the subject value is null, the Subject field is removed.
537      *
538      * @param subject The new subject value.
539      *
540      * @exception MessagingException
541      */
542     public void setSubject(String subject) throws MessagingException {
543         // just set this using the default character set.
544         setSubject(subject, null);
545     }
546 
547     public void setSubject(String subject, String charset) throws MessagingException {
548         // standard null removal (yada, yada, yada....)
549         if (subject == null) {
550             removeHeader("Subject");
551         }
552         else {
553             try {
554                 String s = ASCIIUtil.fold(9, MimeUtility.encodeText(subject, charset, null));
555                 // encode this, and then fold to fit the line lengths.
556                 setHeader("Subject", ASCIIUtil.fold(9, MimeUtility.encodeText(subject, charset, null)));
557             } catch (UnsupportedEncodingException e) {
558                 throw new MessagingException("Encoding error", e);
559             }
560         }
561     }
562 
563     /**
564      * Get the value of the "Date" header field.  Returns null if
565      * if the field is absent or the date is not in a parseable format.
566      *
567      * @return A Date object parsed according to RFC 822.
568      * @exception MessagingException
569      */
570     public Date getSentDate() throws MessagingException {
571         String value = getSingleHeader("Date");
572         if (value == null) {
573             return null;
574         }
575         try {
576             return dateFormat.parse(value);
577         } catch (java.text.ParseException e) {
578             return null;
579         }
580     }
581 
582     /**
583      * Set the message sent date.  This updates the "Date" header.
584      * If the provided date is null, the header is removed.
585      *
586      * @param sent   The new sent date value.
587      *
588      * @exception MessagingException
589      */
590     public void setSentDate(Date sent) throws MessagingException {
591         setOrRemoveHeader("Date", dateFormat.format(sent));
592     }
593 
594     /**
595      * Get the message received date.  The Sun implementation is
596      * documented as always returning null, so this one does too.
597      *
598      * @return Always returns null.
599      * @exception MessagingException
600      */
601     public Date getReceivedDate() throws MessagingException {
602         return null;
603     }
604 
605     /**
606      * Return the content size of this message.  This is obtained
607      * either from the size of the content field (if available) or
608      * from the contentStream, IFF the contentStream returns a positive
609      * size.  Returns -1 if the size is not available.
610      *
611      * @return Size of the content in bytes.
612      * @exception MessagingException
613      */
614     public int getSize() throws MessagingException {
615         if (content != null) {
616             return content.length;
617         }
618         if (contentStream != null) {
619             try {
620                 int size = contentStream.available();
621                 if (size > 0) {
622                     return size;
623                 }
624             } catch (IOException e) {
625                 // ignore
626             }
627         }
628         return -1;
629     }
630 
631     /**
632      * Retrieve the line count for the current message.  Returns
633      * -1 if the count cannot be determined.
634      *
635      * The Sun implementation always returns -1, so this version
636      * does too.
637      *
638      * @return The content line count (always -1 in this implementation).
639      * @exception MessagingException
640      */
641     public int getLineCount() throws MessagingException {
642         return -1;
643     }
644 
645     /**
646      * Returns the current content type (defined in the "Content-Type"
647      * header.  If not available, "text/plain" is the default.
648      *
649      * @return The String name of the message content type.
650      * @exception MessagingException
651      */
652     public String getContentType() throws MessagingException {
653         String value = getSingleHeader("Content-Type");
654         if (value == null) {
655             value = "text/plain";
656         }
657         return value;
658     }
659 
660 
661     /**
662      * Tests to see if this message has a mime-type match with the
663      * given type name.
664      *
665      * @param type   The tested type name.
666      *
667      * @return If this is a type match on the primary and secondare portion of the types.
668      * @exception MessagingException
669      */
670     public boolean isMimeType(String type) throws MessagingException {
671         return new ContentType(getContentType()).match(type);
672     }
673 
674     /**
675      * Retrieve the message "Content-Disposition" header field.
676      * This value represents how the part should be represented to
677      * the user.
678      *
679      * @return The string value of the Content-Disposition field.
680      * @exception MessagingException
681      */
682     public String getDisposition() throws MessagingException {
683         String disp = getSingleHeader("Content-Disposition");
684         if (disp != null) {
685             return new ContentDisposition(disp).getDisposition();
686         }
687         return null;
688     }
689 
690 
691     /**
692      * Set a new dispostion value for the "Content-Disposition" field.
693      * If the new value is null, the header is removed.
694      *
695      * @param disposition
696      *               The new disposition value.
697      *
698      * @exception MessagingException
699      */
700     public void setDisposition(String disposition) throws MessagingException {
701         if (disposition == null) {
702             removeHeader("Content-Disposition");
703         }
704         else {
705             // the disposition has parameters, which we'll attempt to preserve in any existing header.
706             String currentHeader = getSingleHeader("Content-Disposition");
707             if (currentHeader != null) {
708                 ContentDisposition content = new ContentDisposition(currentHeader);
709                 content.setDisposition(disposition);
710                 setHeader("Content-Disposition", content.toString());
711             }
712             else {
713                 // set using the raw string.
714                 setHeader("Content-Disposition", disposition);
715             }
716         }
717     }
718 
719     /**
720      * Decode the Content-Transfer-Encoding header to determine
721      * the transfer encoding type.
722      *
723      * @return The string name of the required encoding.
724      * @exception MessagingException
725      */
726     public String getEncoding() throws MessagingException {
727         // this might require some parsing to sort out.
728         String encoding = getSingleHeader("Content-Transfer-Encoding");
729         if (encoding != null) {
730             // we need to parse this into ATOMs and other constituent parts.  We want the first
731             // ATOM token on the string.
732             HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME);
733 
734             Token token = tokenizer.next();
735             while (token.getType() != Token.EOF) {
736                 // if this is an ATOM type, return it.
737                 if (token.getType() == Token.ATOM) {
738                     return token.getValue();
739                 }
740             }
741             // not ATOMs found, just return the entire header value....somebody might be able to make sense of
742             // this.
743             return encoding;
744         }
745         // no header, nothing to return.
746         return null;
747     }
748 
749     /**
750      * Retrieve the value of the "Content-ID" header.  Returns null
751      * if the header does not exist.
752      *
753      * @return The current header value or null.
754      * @exception MessagingException
755      */
756     public String getContentID() throws MessagingException {
757         return getSingleHeader("Content-ID");
758     }
759 
760     public void setContentID(String cid) throws MessagingException {
761         setOrRemoveHeader("Content-ID", cid);
762     }
763 
764     public String getContentMD5() throws MessagingException {
765         return getSingleHeader("Content-MD5");
766     }
767 
768     public void setContentMD5(String md5) throws MessagingException {
769         setOrRemoveHeader("Content-MD5", md5);
770     }
771 
772     public String getDescription() throws MessagingException {
773         String description = getSingleHeader("Content-Description");
774         if (description != null) {
775             try {
776                 // this could be both folded and encoded.  Return this to usable form.
777                 return MimeUtility.decodeText(ASCIIUtil.unfold(description));
778             } catch (UnsupportedEncodingException e) {
779                 // ignore
780             }
781         }
782         // return the raw version for any errors.
783         return description;
784     }
785 
786     public void setDescription(String description) throws MessagingException {
787         setDescription(description, null);
788     }
789 
790     public void setDescription(String description, String charset) throws MessagingException {
791         if (description == null) {
792             removeHeader("Content-Description");
793         }
794         else {
795             try {
796                 setHeader("Content-Description", ASCIIUtil.fold(21, MimeUtility.encodeText(description, charset, null)));
797             } catch (UnsupportedEncodingException e) {
798                 throw new MessagingException(e.getMessage(), e);
799             }
800         }
801 
802     }
803 
804     public String[] getContentLanguage() throws MessagingException {
805         return getHeader("Content-Language");
806     }
807 
808     public void setContentLanguage(String[] languages) throws MessagingException {
809         if (languages == null) {
810             removeHeader("Content-Language");
811         } else if (languages.length == 1) {
812             setHeader("Content-Language", languages[0]);
813         } else {
814             StringBuffer buf = new StringBuffer(languages.length * 20);
815             buf.append(languages[0]);
816             for (int i = 1; i < languages.length; i++) {
817                 buf.append(',').append(languages[i]);
818             }
819             setHeader("Content-Language", buf.toString());
820         }
821     }
822 
823     public String getMessageID() throws MessagingException {
824         return getSingleHeader("Message-ID");
825     }
826 
827     public String getFileName() throws MessagingException {
828         // see if there is a disposition.  If there is, parse off the filename parameter.
829         String disposition = getDisposition();
830         String filename = null;
831 
832         if (disposition != null) {
833             filename = new ContentDisposition(disposition).getParameter("filename");
834         }
835 
836         // if there's no filename on the disposition, there might be a name parameter on a
837         // Content-Type header.
838         if (filename == null) {
839             String type = getContentType();
840             if (type != null) {
841                 try {
842                     filename = new ContentType(type).getParameter("name");
843                 } catch (ParseException e) {
844                 }
845             }
846         }
847         // if we have a name, we might need to decode this if an additional property is set.
848         if (filename != null && SessionUtil.getBooleanProperty(session, MIME_DECODEFILENAME, false)) {
849             try {
850                 filename = MimeUtility.decodeText(filename);
851             } catch (UnsupportedEncodingException e) {
852                 throw new MessagingException("Unable to decode filename", e);
853             }
854         }
855 
856         return filename;
857     }
858 
859 
860     public void setFileName(String name) throws MessagingException {
861         // there's an optional session property that requests file name encoding...we need to process this before
862         // setting the value.
863         if (name != null && SessionUtil.getBooleanProperty(session, MIME_ENCODEFILENAME, false)) {
864             try {
865                 name = MimeUtility.encodeText(name);
866             } catch (UnsupportedEncodingException e) {
867                 throw new MessagingException("Unable to encode filename", e);
868             }
869         }
870 
871         // get the disposition string.
872         String disposition = getDisposition();
873         // if not there, then this is an attachment.
874         if (disposition == null) {
875             disposition = Part.ATTACHMENT;
876         }
877         // now create a disposition object and set the parameter.
878         ContentDisposition contentDisposition = new ContentDisposition(disposition);
879         contentDisposition.setParameter("filename", name);
880 
881         // serialize this back out and reset.
882         setDisposition(contentDisposition.toString());
883     }
884 
885     public InputStream getInputStream() throws MessagingException, IOException {
886         return getDataHandler().getInputStream();
887     }
888 
889     protected InputStream getContentStream() throws MessagingException {
890         if (contentStream != null) {
891             return contentStream;
892         }
893 
894         if (content != null) {
895             return new ByteArrayInputStream(content);
896         } else {
897             throw new MessagingException("No content");
898         }
899     }
900 
901     public InputStream getRawInputStream() throws MessagingException {
902         return getContentStream();
903     }
904 
905     public synchronized DataHandler getDataHandler() throws MessagingException {
906         if (dh == null) {
907             dh = new DataHandler(new MimePartDataSource(this));
908         }
909         return dh;
910     }
911 
912     public Object getContent() throws MessagingException, IOException {
913         return getDataHandler().getContent();
914     }
915 
916     public void setDataHandler(DataHandler handler) throws MessagingException {
917         dh = handler;
918         // if we have a handler override, then we need to invalidate any content
919         // headers that define the types.  This information will be derived from the
920         // data heander unless subsequently overridden.
921         removeHeader("Content-Type");
922         removeHeader("Content-Transfer-Encoding");
923     }
924 
925     public void setContent(Object content, String type) throws MessagingException {
926         setDataHandler(new DataHandler(content, type));
927     }
928 
929     public void setText(String text) throws MessagingException {
930         setText(text, null);
931     }
932 
933     public void setText(String text, String charset) throws MessagingException {
934         // we need to sort out the character set if one is not provided.
935         if (charset == null) {
936             // if we have non us-ascii characters here, we need to adjust this.
937             if (!ASCIIUtil.isAscii(text)) {
938                 charset = MimeUtility.getDefaultMIMECharset();
939             }
940             else {
941                 charset = "us-ascii";
942             }
943         }
944         setContent(text, "text/plain; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME));
945     }
946 
947     public void setContent(Multipart part) throws MessagingException {
948         setDataHandler(new DataHandler(part, part.getContentType()));
949         part.setParent(this);
950     }
951 
952     public Message reply(boolean replyToAll) throws MessagingException {
953         // create a new message in this session.
954         MimeMessage reply = new MimeMessage(session);
955 
956         // get the header and add the "Re:" bit, if necessary.
957         String newSubject = getSubject();
958         if (newSubject != null) {
959             // check to see if it already begins with "Re: " (in any case).
960             // Add one on if we don't have it yet.
961             if (!newSubject.regionMatches(true, 0, "Re: ", 0, 4)) {
962                 newSubject = "Re: " + newSubject;
963             }
964             reply.setSubject(newSubject);
965         }
966 
967         Address[] toRecipients = getReplyTo();
968 
969         // set the target recipients the replyTo value
970         reply.setRecipients(Message.RecipientType.TO, getReplyTo());
971 
972         // need to reply to everybody?  More things to add.
973         if (replyToAll) {
974             // when replying, we want to remove "duplicates" in the final list.
975 
976             HashMap masterList = new HashMap();
977 
978             // reply to all implies add the local sender.  Add this to the list if resolveable.
979             InternetAddress localMail = InternetAddress.getLocalAddress(session);
980             if (localMail != null) {
981                 masterList.put(localMail.getAddress(), localMail);
982             }
983             // see if we have some local aliases to deal with.
984             String alternates = session.getProperty(MAIL_ALTERNATES);
985             if (alternates != null) {
986                 // parse this string list and merge with our set.
987                 Address[] alternateList = InternetAddress.parse(alternates, false);
988                 mergeAddressList(masterList, alternateList);
989             }
990 
991             // the master list now contains an a list of addresses we will exclude from
992             // the addresses.  From this point on, we're going to prune any additional addresses
993             // against this list, AND add any new addresses to the list
994 
995             // now merge in the main recipients, and merge in the other recipents as well
996             Address[] toList = pruneAddresses(masterList, getRecipients(Message.RecipientType.TO));
997             if (toList.length != 0) {
998                 // now check to see what sort of reply we've been asked to send.
999                 // if replying to all as a CC, then we need to add to the CC list, otherwise they are
1000                 // TO recipients.
1001                 if (SessionUtil.getBooleanProperty(session, MAIL_REPLYALLCC, false)) {
1002                     reply.addRecipients(Message.RecipientType.CC, toList);
1003                 }
1004                 else {
1005                     reply.addRecipients(Message.RecipientType.TO, toList);
1006                 }
1007             }
1008             // and repeat for the CC list.
1009             toList = pruneAddresses(masterList, getRecipients(Message.RecipientType.CC));
1010             if (toList.length != 0) {
1011                 reply.addRecipients(Message.RecipientType.CC, toList);
1012             }
1013 
1014             // a news group list is separate from the normal addresses.  We just take these recepients
1015             // asis without trying to prune duplicates.
1016             toList = getRecipients(RecipientType.NEWSGROUPS);
1017             if (toList != null && toList.length != 0) {
1018                 reply.addRecipients(RecipientType.NEWSGROUPS, toList);
1019             }
1020         }
1021 
1022         // this is a bit of a pain.  We can't set the flags here by specifying the system flag, we need to
1023         // construct a flag item instance inorder to set it.
1024 
1025         // this is an answered email.
1026         setFlags(new Flags(Flags.Flag.ANSWERED), true);
1027         // all done, return the constructed Message object.
1028         return reply;
1029     }
1030 
1031 
1032     /**
1033      * Merge a set of addresses into a master accumulator list, eliminating
1034      * duplicates.
1035      *
1036      * @param master The set of addresses we've accumulated so far.
1037      * @param list   The list of addresses to merge in.
1038      */
1039     private void mergeAddressList(Map master, Address[] list) {
1040         // make sure we have a list.
1041         if (list == null) {
1042             return;
1043         }
1044         for (int i = 0; i < list.length; i++) {
1045             InternetAddress address = (InternetAddress)list[i];
1046 
1047             // if not in the master list already, add it now.
1048             if (!master.containsKey(address.getAddress())) {
1049                 master.put(address.getAddress(), address);
1050             }
1051         }
1052     }
1053 
1054 
1055     /**
1056      * Prune a list of addresses against our master address list,
1057      * returning the "new" addresses.  The master list will be
1058      * updated with this new set of addresses.
1059      *
1060      * @param master The master address list of addresses we've seen before.
1061      * @param list   The new list of addresses to prune.
1062      *
1063      * @return An array of addresses pruned of any duplicate addresses.
1064      */
1065     private Address[] pruneAddresses(Map master, Address[] list) {
1066         // return an empy array if we don't get an input list.
1067         if (list == null) {
1068             return new Address[0];
1069         }
1070 
1071         // optimistically assume there are no addresses to eliminate (common).
1072         ArrayList prunedList = new ArrayList(list.length);
1073         for (int i = 0; i < list.length; i++) {
1074             InternetAddress address = (InternetAddress)list[i];
1075 
1076             // if not in the master list, this is a new one.  Add to both the master list and
1077             // the pruned list.
1078             if (!master.containsKey(address.getAddress())) {
1079                 master.put(address.getAddress(), address);
1080                 prunedList.add(address);
1081             }
1082         }
1083         // convert back to list form.
1084         return (Address[])prunedList.toArray(new Address[0]);
1085     }
1086 
1087 
1088     /**
1089      * Write the message out to a stream in RFC 822 format.
1090      *
1091      * @param out    The target output stream.
1092      *
1093      * @exception MessagingException
1094      * @exception IOException
1095      */
1096     public void writeTo(OutputStream out) throws MessagingException, IOException {
1097         writeTo(out, null);
1098     }
1099 
1100     /**
1101      * Write the message out to a target output stream, excluding the
1102      * specified message headers.
1103      *
1104      * @param out    The target output stream.
1105      * @param ignoreHeaders
1106      *               An array of header types to ignore.  This can be null, which means
1107      *               write out all headers.
1108      *
1109      * @exception MessagingException
1110      * @exception IOException
1111      */
1112     public void writeTo(OutputStream out, String[] ignoreHeaders) throws MessagingException, IOException {
1113         // make sure everything is saved before we write
1114         if (!saved) {
1115             saveChanges();
1116         }
1117 
1118         // write out the headers first
1119         headers.writeTo(out, ignoreHeaders);
1120         // add the separater between the headers and the data portion.
1121         out.write('\r');
1122         out.write('\n');
1123 
1124         // if the modfied flag, we don't have current content, so the data handler needs to
1125         // take care of writing this data out.
1126         if (modified) {
1127             dh.writeTo(MimeUtility.encode(out, getEncoding()));
1128         } else {
1129             // if we have content directly, we can write this out now.
1130             if (content != null) {
1131                 out.write(content);
1132             }
1133             else {
1134                 // see if we can get a content stream for this message.  We might have had one
1135                 // explicitly set, or a subclass might override the get method to provide one.
1136                 InputStream in = getContentStream();
1137 
1138                 byte[] buffer = new byte[8192];
1139                 int length = in.read(buffer);
1140                 // copy the data stream-to-stream.
1141                 while (length > 0) {
1142                     out.write(buffer, 0, length);
1143                     length = in.read(buffer);
1144                 }
1145                 in.close();
1146             }
1147         }
1148 
1149         // flush any data we wrote out, but do not close the stream.  That's the caller's duty.
1150         out.flush();
1151     }
1152 
1153 
1154     /**
1155      * Retrieve all headers that match a given name.
1156      *
1157      * @param name   The target name.
1158      *
1159      * @return The set of headers that match the given name.  These headers
1160      *         will be the decoded() header values if these are RFC 2047
1161      *         encoded.
1162      * @exception MessagingException
1163      */
1164     public String[] getHeader(String name) throws MessagingException {
1165         return headers.getHeader(name);
1166     }
1167 
1168     /**
1169      * Get all headers that match a particular name, as a single string.
1170      * Individual headers are separated by the provided delimiter.   If
1171      * the delimiter is null, only the first header is returned.
1172      *
1173      * @param name      The source header name.
1174      * @param delimiter The delimiter string to be used between headers.  If null, only
1175      *                  the first is returned.
1176      *
1177      * @return The headers concatenated as a single string.
1178      * @exception MessagingException
1179      */
1180     public String getHeader(String name, String delimiter) throws MessagingException {
1181         return headers.getHeader(name, delimiter);
1182     }
1183 
1184     /**
1185      * Set a new value for a named header.
1186      *
1187      * @param name   The name of the target header.
1188      * @param value  The new value for the header.
1189      *
1190      * @exception MessagingException
1191      */
1192     public void setHeader(String name, String value) throws MessagingException {
1193         headers.setHeader(name, value);
1194     }
1195 
1196     /**
1197      * Conditionally set or remove a named header.  If the new value
1198      * is null, the header is removed.
1199      *
1200      * @param name   The header name.
1201      * @param value  The new header value.  A null value causes the header to be
1202      *               removed.
1203      *
1204      * @exception MessagingException
1205      */
1206     private void setOrRemoveHeader(String name, String value) throws MessagingException {
1207         if (value == null) {
1208             headers.removeHeader(name);
1209         }
1210         else {
1211             headers.setHeader(name, value);
1212         }
1213     }
1214 
1215     /**
1216      * Add a new value to an existing header.  The added value is
1217      * created as an additional header of the same type and value.
1218      *
1219      * @param name   The name of the target header.
1220      * @param value  The removed header.
1221      *
1222      * @exception MessagingException
1223      */
1224     public void addHeader(String name, String value) throws MessagingException {
1225         headers.addHeader(name, value);
1226     }
1227 
1228     /**
1229      * Remove a header with the given name.
1230      *
1231      * @param name   The name of the removed header.
1232      *
1233      * @exception MessagingException
1234      */
1235     public void removeHeader(String name) throws MessagingException {
1236         headers.removeHeader(name);
1237     }
1238 
1239     /**
1240      * Retrieve the complete list of message headers, as an enumeration.
1241      *
1242      * @return An Enumeration of the message headers.
1243      * @exception MessagingException
1244      */
1245     public Enumeration getAllHeaders() throws MessagingException {
1246         return headers.getAllHeaders();
1247     }
1248 
1249     public Enumeration getMatchingHeaders(String[] names) throws MessagingException {
1250         return headers.getMatchingHeaders(names);
1251     }
1252 
1253     public Enumeration getNonMatchingHeaders(String[] names) throws MessagingException {
1254         return headers.getNonMatchingHeaders(names);
1255     }
1256 
1257     public void addHeaderLine(String line) throws MessagingException {
1258         headers.addHeaderLine(line);
1259     }
1260 
1261     public Enumeration getAllHeaderLines() throws MessagingException {
1262         return headers.getAllHeaderLines();
1263     }
1264 
1265     public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
1266         return headers.getMatchingHeaderLines(names);
1267     }
1268 
1269     public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
1270         return headers.getNonMatchingHeaderLines(names);
1271     }
1272 
1273     public synchronized Flags getFlags() throws MessagingException {
1274         return (Flags) flags.clone();
1275     }
1276 
1277     public synchronized boolean isSet(Flags.Flag flag) throws MessagingException {
1278         return flags.contains(flag);
1279     }
1280 
1281     /**
1282      * Set or clear a flag value.
1283      *
1284      * @param flags  The set of flags to effect.
1285      * @param set    The value to set the flag to (true or false).
1286      *
1287      * @exception MessagingException
1288      */
1289     public synchronized void setFlags(Flags flag, boolean set) throws MessagingException {
1290         if (set) {
1291             flags.add(flag);
1292         }
1293         else {
1294             flags.remove(flag);
1295         }
1296     }
1297 
1298     /**
1299      * Saves any changes on this message.  When called, the modified
1300      * and saved flags are set to true and updateHeaders() is called
1301      * to force updates.
1302      *
1303      * @exception MessagingException
1304      */
1305     public void saveChanges() throws MessagingException {
1306         // setting modified invalidates the current content.
1307         modified = true;
1308         saved = true;
1309         // update message headers from the content.
1310         updateHeaders();
1311     }
1312 
1313     /**
1314      * Update the internet headers so that they make sense.  This
1315      * will attempt to make sense of the message content type
1316      * given the state of the content.
1317      *
1318      * @exception MessagingException
1319      */
1320     protected void updateHeaders() throws MessagingException {
1321 
1322         // make sure we set the MIME version
1323         setHeader("MIME-Version", "1.0");
1324 
1325         DataHandler handler = getDataHandler();
1326 
1327         try {
1328             // figure out the content type.  If not set, we'll need to figure this out.
1329             String type = dh.getContentType();
1330             // parse this content type out so we can do matches/compares.
1331             ContentType content = new ContentType(type);
1332 
1333             // is this a multipart content?
1334             if (content.match("multipart/*")) {
1335                 // the content is suppose to be a MimeMultipart.  Ping it to update it's headers as well.
1336                 try {
1337                     MimeMultipart part = (MimeMultipart)handler.getContent();
1338                     part.updateHeaders();
1339                 } catch (ClassCastException e) {
1340                     throw new MessagingException("Message content is not MimeMultipart", e);
1341                 }
1342             }
1343             else if (!content.match("message/rfc822")) {
1344                 // simple part, we need to update the header type information
1345                 // if no encoding is set yet, figure this out from the data handler content.
1346                 if (getSingleHeader("Content-Transfer-Encoding") == null) {
1347                     setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
1348                 }
1349 
1350                 // is a content type header set?  Check the property to see if we need to set this.
1351                 if (getSingleHeader("Content-Type") == null) {
1352                     if (SessionUtil.getBooleanProperty(session, "MIME_MAIL_SETDEFAULTTEXTCHARSET", true)) {
1353                         // is this a text type?  Figure out the encoding and make sure it is set.
1354                         if (content.match("text/*")) {
1355                             // the charset should be specified as a parameter on the MIME type.  If not there,
1356                             // try to figure one out.
1357                             if (content.getParameter("charset") == null) {
1358 
1359                                 String encoding = getEncoding();
1360                                 // if we're sending this as 7-bit ASCII, our character set need to be
1361                                 // compatible.
1362                                 if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
1363                                     content.setParameter("charset", "us-ascii");
1364                                 }
1365                                 else {
1366                                     // get the global default.
1367                                     content.setParameter("charset", MimeUtility.getDefaultMIMECharset());
1368                                 }
1369                             }
1370                         }
1371                     }
1372                 }
1373             }
1374 
1375             // if we don't have a content type header, then create one.
1376             if (getSingleHeader("Content-Type") == null) {
1377                 // get the disposition header, and if it is there, copy the filename parameter into the
1378                 // name parameter of the type.
1379                 String disp = getSingleHeader("Content-Disposition");
1380                 if (disp != null) {
1381                     // parse up the string value of the disposition
1382                     ContentDisposition disposition = new ContentDisposition(disp);
1383                     // now check for a filename value
1384                     String filename = disposition.getParameter("filename");
1385                     // copy and rename the parameter, if it exists.
1386                     if (filename != null) {
1387                         content.setParameter("name", filename);
1388                     }
1389                 }
1390                 // set the header with the updated content type information.
1391                 setHeader("Content-Type", content.toString());
1392             }
1393 
1394         } catch (IOException e) {
1395             throw new MessagingException("Error updating message headers", e);
1396         }
1397     }
1398 
1399 
1400     protected InternetHeaders createInternetHeaders(InputStream in) throws MessagingException {
1401         // internet headers has a constructor for just this purpose
1402         return new InternetHeaders(in);
1403     }
1404 
1405     /**
1406      * Convert a header into an array of NewsAddress items.
1407      *
1408      * @param header The name of the source header.
1409      *
1410      * @return The parsed array of addresses.
1411      * @exception MessagingException
1412      */
1413     private Address[] getHeaderAsNewsAddresses(String header) throws MessagingException {
1414         // NB:  We're using getHeader() here to allow subclasses an opportunity to perform lazy loading
1415         // of the headers.
1416         String mergedHeader = getHeader(header, ",");
1417         if (mergedHeader != null) {
1418             return NewsAddress.parse(mergedHeader);
1419         }
1420         return null;
1421     }
1422 
1423     private Address[] getHeaderAsInternetAddresses(String header, boolean strict) throws MessagingException {
1424         // NB:  We're using getHeader() here to allow subclasses an opportunity to perform lazy loading
1425         // of the headers.
1426         String mergedHeader = getHeader(header, ",");
1427         if (mergedHeader != null) {
1428             return InternetAddress.parseHeader(mergedHeader, strict);
1429         }
1430         return null;
1431     }
1432 
1433     /**
1434      * Check to see if we require strict addressing on parsing
1435      * internet headers.
1436      *
1437      * @return The current value of the "mail.mime.address.strict" session
1438      *         property, or true, if the property is not set.
1439      */
1440     private boolean isStrictAddressing() {
1441         return SessionUtil.getBooleanProperty(session, MIME_ADDRESS_STRICT, true);
1442     }
1443 
1444     /**
1445      * Set a named header to the value of an address field.
1446      *
1447      * @param header  The header name.
1448      * @param address The address value.  If the address is null, the header is removed.
1449      *
1450      * @exception MessagingException
1451      */
1452     private void setHeader(String header, Address address) throws MessagingException {
1453         if (address == null) {
1454             removeHeader(header);
1455         }
1456         else {
1457             setHeader(header, address.toString());
1458         }
1459     }
1460 
1461     /**
1462      * Set a header to a list of addresses.
1463      *
1464      * @param header    The header name.
1465      * @param addresses An array of addresses to set the header to.  If null, the
1466      *                  header is removed.
1467      */
1468     private void setHeader(String header, Address[] addresses) {
1469         if (addresses == null) {
1470             headers.removeHeader(header);
1471         }
1472         else {
1473             headers.setHeader(header, addresses);
1474         }
1475     }
1476 
1477     private void addHeader(String header, Address[] addresses) throws MessagingException {
1478         headers.addHeader(header, InternetAddress.toString(addresses));
1479     }
1480 
1481     private String getHeaderForRecipientType(Message.RecipientType type) throws MessagingException {
1482         if (RecipientType.TO == type) {
1483             return "To";
1484         } else if (RecipientType.CC == type) {
1485             return "Cc";
1486         } else if (RecipientType.BCC == type) {
1487             return "Bcc";
1488         } else if (RecipientType.NEWSGROUPS == type) {
1489             return "Newsgroups";
1490         } else {
1491             throw new MessagingException("Unsupported recipient type: " + type.toString());
1492         }
1493     }
1494 
1495     /**
1496      * Utility routine to get a header as a single string value
1497      * rather than an array of headers.
1498      *
1499      * @param name   The name of the header.
1500      *
1501      * @return The single string header value.  If multiple headers exist,
1502      *         the additional ones are ignored.
1503      * @exception MessagingException
1504      */
1505     private String getSingleHeader(String name) throws MessagingException {
1506         String[] values = getHeader(name);
1507         if (values == null || values.length == 0) {
1508             return null;
1509         } else {
1510             return values[0];
1511         }
1512     }
1513 }