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