001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one
003     * or more contributor license agreements.  See the NOTICE file
004     * distributed with this work for additional information
005     * regarding copyright ownership.  The ASF licenses this file
006     * to you under the Apache License, Version 2.0 (the
007     * "License"); you may not use this file except in compliance
008     * with the License.  You may obtain a copy of the License at
009     *
010     *  http://www.apache.org/licenses/LICENSE-2.0
011     *
012     * Unless required by applicable law or agreed to in writing,
013     * software distributed under the License is distributed on an
014     * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     * KIND, either express or implied.  See the License for the
016     * specific language governing permissions and limitations
017     * under the License.
018     */
019    
020    package javax.mail.internet;
021    
022    import java.io.BufferedInputStream;
023    import java.io.ByteArrayInputStream;
024    import java.io.ByteArrayOutputStream;
025    import java.io.IOException;
026    import java.io.InputStream;
027    import java.io.ObjectStreamException;
028    import java.io.OutputStream;
029    import java.io.UnsupportedEncodingException;
030    import java.util.ArrayList;
031    import java.util.Arrays;
032    import java.util.Date;
033    import java.util.Enumeration;
034    import java.util.HashMap;
035    import java.util.List;
036    import java.util.Map;
037    
038    import javax.activation.DataHandler;
039    import javax.mail.Address;
040    import javax.mail.Flags;
041    import javax.mail.Folder;
042    import javax.mail.Message;
043    import javax.mail.MessagingException;
044    import javax.mail.Multipart;
045    import javax.mail.Part;
046    import javax.mail.Session;
047    import javax.mail.internet.HeaderTokenizer.Token;
048    
049    import org.apache.geronimo.mail.util.ASCIIUtil;
050    import org.apache.geronimo.mail.util.SessionUtil;
051    
052    /**
053     * @version $Rev: 582780 $ $Date: 2007-10-08 07:17:15 -0400 (Mon, 08 Oct 2007) $
054     */
055    public class MimeMessage extends Message implements MimePart {
056            private static final String MIME_ADDRESS_STRICT = "mail.mime.address.strict";
057            private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename";
058            private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename";
059    
060            private static final String MAIL_ALTERNATES = "mail.alternates";
061            private static final String MAIL_REPLYALLCC = "mail.replyallcc";
062    
063        // static used to ensure message ID uniqueness
064        private static int messageID = 0;
065    
066    
067        /**
068         * Extends {@link javax.mail.Message.RecipientType} to support addition recipient types.
069         */
070        public static class RecipientType extends Message.RecipientType {
071            /**
072             * Recipient type for Usenet news.
073             */
074            public static final RecipientType NEWSGROUPS = new RecipientType("Newsgroups");
075    
076            protected RecipientType(String type) {
077                super(type);
078            }
079    
080            /**
081             * Ensure the singleton is returned.
082             *
083             * @return resolved object
084             */
085            protected Object readResolve() throws ObjectStreamException {
086                if (this.type.equals("Newsgroups")) {
087                    return NEWSGROUPS;
088                } else {
089                    return super.readResolve();
090                }
091            }
092        }
093    
094        /**
095         * The {@link DataHandler} for this Message's content.
096         */
097        protected DataHandler dh;
098        /**
099         * 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    }