View Javadoc

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