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: 702800 $ $Date: 2008-10-08 06:38:14 -0400 (Wed, 08 Oct 2008) $
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            // 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                OutputStream encoderStream = MimeUtility.encode(out, getEncoding());
1166                dh.writeTo(encoderStream);
1167                encoderStream.flush();
1168            } else {
1169                // if we have content directly, we can write this out now.
1170                if (content != null) {
1171                    out.write(content);
1172                }
1173                else {
1174                    // see if we can get a content stream for this message.  We might have had one
1175                    // explicitly set, or a subclass might override the get method to provide one.
1176                    InputStream in = getContentStream();
1177    
1178                    byte[] buffer = new byte[8192];
1179                    int length = in.read(buffer);
1180                    // copy the data stream-to-stream.
1181                    while (length > 0) {
1182                        out.write(buffer, 0, length);
1183                        length = in.read(buffer);
1184                    }
1185                    in.close();
1186                }
1187            }
1188    
1189            // flush any data we wrote out, but do not close the stream.  That's the caller's duty.
1190            out.flush();
1191        }
1192    
1193    
1194        /**
1195         * Retrieve all headers that match a given name.
1196         *
1197         * @param name   The target name.
1198         *
1199         * @return The set of headers that match the given name.  These headers
1200         *         will be the decoded() header values if these are RFC 2047
1201         *         encoded.
1202         * @exception MessagingException
1203         */
1204        public String[] getHeader(String name) throws MessagingException {
1205            return headers.getHeader(name);
1206        }
1207    
1208        /**
1209         * Get all headers that match a particular name, as a single string.
1210         * Individual headers are separated by the provided delimiter.   If
1211         * the delimiter is null, only the first header is returned.
1212         *
1213         * @param name      The source header name.
1214         * @param delimiter The delimiter string to be used between headers.  If null, only
1215         *                  the first is returned.
1216         *
1217         * @return The headers concatenated as a single string.
1218         * @exception MessagingException
1219         */
1220        public String getHeader(String name, String delimiter) throws MessagingException {
1221            return headers.getHeader(name, delimiter);
1222        }
1223    
1224        /**
1225         * Set a new value for a named header.
1226         *
1227         * @param name   The name of the target header.
1228         * @param value  The new value for the header.
1229         *
1230         * @exception MessagingException
1231         */
1232        public void setHeader(String name, String value) throws MessagingException {
1233            headers.setHeader(name, value);
1234        }
1235    
1236        /**
1237         * Conditionally set or remove a named header.  If the new value
1238         * is null, the header is removed.
1239         *
1240         * @param name   The header name.
1241         * @param value  The new header value.  A null value causes the header to be
1242         *               removed.
1243         *
1244         * @exception MessagingException
1245         */
1246        private void setOrRemoveHeader(String name, String value) throws MessagingException {
1247            if (value == null) {
1248                headers.removeHeader(name);
1249            }
1250            else {
1251                headers.setHeader(name, value);
1252            }
1253        }
1254    
1255        /**
1256         * Add a new value to an existing header.  The added value is
1257         * created as an additional header of the same type and value.
1258         *
1259         * @param name   The name of the target header.
1260         * @param value  The removed header.
1261         *
1262         * @exception MessagingException
1263         */
1264        public void addHeader(String name, String value) throws MessagingException {
1265            headers.addHeader(name, value);
1266        }
1267    
1268        /**
1269         * Remove a header with the given name.
1270         *
1271         * @param name   The name of the removed header.
1272         *
1273         * @exception MessagingException
1274         */
1275        public void removeHeader(String name) throws MessagingException {
1276            headers.removeHeader(name);
1277        }
1278    
1279        /**
1280         * Retrieve the complete list of message headers, as an enumeration.
1281         *
1282         * @return An Enumeration of the message headers.
1283         * @exception MessagingException
1284         */
1285        public Enumeration getAllHeaders() throws MessagingException {
1286            return headers.getAllHeaders();
1287        }
1288    
1289        public Enumeration getMatchingHeaders(String[] names) throws MessagingException {
1290            return headers.getMatchingHeaders(names);
1291        }
1292    
1293        public Enumeration getNonMatchingHeaders(String[] names) throws MessagingException {
1294            return headers.getNonMatchingHeaders(names);
1295        }
1296    
1297        public void addHeaderLine(String line) throws MessagingException {
1298            headers.addHeaderLine(line);
1299        }
1300    
1301        public Enumeration getAllHeaderLines() throws MessagingException {
1302            return headers.getAllHeaderLines();
1303        }
1304    
1305        public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException {
1306            return headers.getMatchingHeaderLines(names);
1307        }
1308    
1309        public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException {
1310            return headers.getNonMatchingHeaderLines(names);
1311        }
1312    
1313    
1314        /**
1315         * Return a copy the flags associated with this message.
1316         *
1317         * @return a copy of the flags for this message
1318         * @throws MessagingException if there was a problem accessing the Store
1319         */
1320        public synchronized Flags getFlags() throws MessagingException {
1321            return (Flags) flags.clone();
1322        }
1323    
1324    
1325        /**
1326         * Check whether the supplied flag is set.
1327         * The default implementation checks the flags returned by {@link #getFlags()}.
1328         *
1329         * @param flag the flags to check for
1330         * @return true if the flags is set
1331         * @throws MessagingException if there was a problem accessing the Store
1332         */
1333        public synchronized boolean isSet(Flags.Flag flag) throws MessagingException {
1334            return flags.contains(flag);
1335        }
1336    
1337        /**
1338         * Set or clear a flag value.
1339         *
1340         * @param flags  The set of flags to effect.
1341         * @param set    The value to set the flag to (true or false).
1342         *
1343         * @exception MessagingException
1344         */
1345        public synchronized void setFlags(Flags flag, boolean set) throws MessagingException {
1346            if (set) {
1347                flags.add(flag);
1348            }
1349            else {
1350                flags.remove(flag);
1351            }
1352        }
1353    
1354        /**
1355         * Saves any changes on this message.  When called, the modified
1356         * and saved flags are set to true and updateHeaders() is called
1357         * to force updates.
1358         *
1359         * @exception MessagingException
1360         */
1361        public void saveChanges() throws MessagingException {
1362            // setting modified invalidates the current content.
1363            modified = true;
1364            saved = true;
1365            // update message headers from the content.
1366            updateHeaders();
1367        }
1368    
1369        /**
1370         * Update the internet headers so that they make sense.  This
1371         * will attempt to make sense of the message content type
1372         * given the state of the content.
1373         *
1374         * @exception MessagingException
1375         */
1376        protected void updateHeaders() throws MessagingException {
1377    
1378            DataHandler handler = getDataHandler();
1379    
1380            try {
1381                // figure out the content type.  If not set, we'll need to figure this out.
1382                String type = dh.getContentType();
1383                // we might need to reconcile the content type and our explicitly set type
1384                String explicitType = getSingleHeader("Content-Type"); 
1385                // parse this content type out so we can do matches/compares.
1386                ContentType content = new ContentType(type);
1387    
1388                // is this a multipart content?
1389                if (content.match("multipart/*")) {
1390                    // the content is suppose to be a MimeMultipart.  Ping it to update it's headers as well.
1391                    try {
1392                        MimeMultipart part = (MimeMultipart)handler.getContent();
1393                        part.updateHeaders();
1394                    } catch (ClassCastException e) {
1395                        throw new MessagingException("Message content is not MimeMultipart", e);
1396                    }
1397                }
1398                else if (!content.match("message/rfc822")) {
1399                    // simple part, we need to update the header type information
1400                    // if no encoding is set yet, figure this out from the data handler content.
1401                    if (getSingleHeader("Content-Transfer-Encoding") == null) {
1402                        setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
1403                    }
1404    
1405                    // is a content type header set?  Check the property to see if we need to set this.
1406                    if (explicitType == null) {
1407                        if (SessionUtil.getBooleanProperty(session, "MIME_MAIL_SETDEFAULTTEXTCHARSET", true)) {
1408                            // is this a text type?  Figure out the encoding and make sure it is set.
1409                            if (content.match("text/*")) {
1410                                // the charset should be specified as a parameter on the MIME type.  If not there,
1411                                // try to figure one out.
1412                                if (content.getParameter("charset") == null) {
1413    
1414                                    String encoding = getEncoding();
1415                                    // if we're sending this as 7-bit ASCII, our character set need to be
1416                                    // compatible.
1417                                    if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
1418                                        content.setParameter("charset", "us-ascii");
1419                                    }
1420                                    else {
1421                                        // get the global default.
1422                                        content.setParameter("charset", MimeUtility.getDefaultMIMECharset());
1423                                    }
1424                                    // replace the original type string 
1425                                    type = content.toString(); 
1426                                }
1427                            }
1428                        }
1429                    }
1430                }
1431    
1432                // if we don't have a content type header, then create one.
1433                if (explicitType == null) {
1434                    // get the disposition header, and if it is there, copy the filename parameter into the
1435                    // name parameter of the type.
1436                    String disp = getSingleHeader("Content-Disposition");
1437                    if (disp != null) {
1438                        // parse up the string value of the disposition
1439                        ContentDisposition disposition = new ContentDisposition(disp);
1440                        // now check for a filename value
1441                        String filename = disposition.getParameter("filename");
1442                        // copy and rename the parameter, if it exists.
1443                        if (filename != null) {
1444                            content.setParameter("name", filename);
1445                            // set the header with the updated content type information.
1446                            type = content.toString();
1447                        }
1448                    }
1449                    // if no header has been set, then copy our current type string (which may 
1450                    // have been modified above) 
1451                    setHeader("Content-Type", type); 
1452                }
1453    
1454                // make sure we set the MIME version
1455                setHeader("MIME-Version", "1.0");
1456                // new javamail 1.4 requirement.
1457                updateMessageID();
1458    
1459            } catch (IOException e) {
1460                throw new MessagingException("Error updating message headers", e);
1461            }
1462        }
1463    
1464    
1465        /**
1466         * Create a new set of internet headers from the 
1467         * InputStream
1468         * 
1469         * @param in     The header source.
1470         * 
1471         * @return A new InternetHeaders object containing the 
1472         *         appropriate headers.
1473         * @exception MessagingException
1474         */
1475        protected InternetHeaders createInternetHeaders(InputStream in) throws MessagingException {
1476            // internet headers has a constructor for just this purpose
1477            return new InternetHeaders(in);
1478        }
1479    
1480        /**
1481         * Convert a header into an array of NewsAddress items.
1482         *
1483         * @param header The name of the source header.
1484         *
1485         * @return The parsed array of addresses.
1486         * @exception MessagingException
1487         */
1488        private Address[] getHeaderAsNewsAddresses(String header) throws MessagingException {
1489            // NB:  We're using getHeader() here to allow subclasses an opportunity to perform lazy loading
1490            // of the headers.
1491            String mergedHeader = getHeader(header, ",");
1492            if (mergedHeader != null) {
1493                return NewsAddress.parse(mergedHeader);
1494            }
1495            return null;
1496        }
1497    
1498        private Address[] getHeaderAsInternetAddresses(String header, boolean strict) throws MessagingException {
1499            // NB:  We're using getHeader() here to allow subclasses an opportunity to perform lazy loading
1500            // of the headers.
1501            String mergedHeader = getHeader(header, ",");
1502    
1503            if (mergedHeader != null) {
1504                return InternetAddress.parseHeader(mergedHeader, strict);
1505            }
1506            return null;
1507        }
1508    
1509        /**
1510         * Check to see if we require strict addressing on parsing
1511         * internet headers.
1512         *
1513         * @return The current value of the "mail.mime.address.strict" session
1514         *         property, or true, if the property is not set.
1515         */
1516        private boolean isStrictAddressing() {
1517            return SessionUtil.getBooleanProperty(session, MIME_ADDRESS_STRICT, true);
1518        }
1519    
1520        /**
1521         * Set a named header to the value of an address field.
1522         *
1523         * @param header  The header name.
1524         * @param address The address value.  If the address is null, the header is removed.
1525         *
1526         * @exception MessagingException
1527         */
1528        private void setHeader(String header, Address address) throws MessagingException {
1529            if (address == null) {
1530                removeHeader(header);
1531            }
1532            else {
1533                setHeader(header, address.toString());
1534            }
1535        }
1536    
1537        /**
1538         * Set a header to a list of addresses.
1539         *
1540         * @param header    The header name.
1541         * @param addresses An array of addresses to set the header to.  If null, the
1542         *                  header is removed.
1543         */
1544        private void setHeader(String header, Address[] addresses) {
1545            if (addresses == null) {
1546                headers.removeHeader(header);
1547            }
1548            else {
1549                headers.setHeader(header, addresses);
1550            }
1551        }
1552    
1553        private void addHeader(String header, Address[] addresses) throws MessagingException {
1554            headers.addHeader(header, InternetAddress.toString(addresses));
1555        }
1556    
1557        private String getHeaderForRecipientType(Message.RecipientType type) throws MessagingException {
1558            if (RecipientType.TO == type) {
1559                return "To";
1560            } else if (RecipientType.CC == type) {
1561                return "Cc";
1562            } else if (RecipientType.BCC == type) {
1563                return "Bcc";
1564            } else if (RecipientType.NEWSGROUPS == type) {
1565                return "Newsgroups";
1566            } else {
1567                throw new MessagingException("Unsupported recipient type: " + type.toString());
1568            }
1569        }
1570    
1571        /**
1572         * Utility routine to get a header as a single string value
1573         * rather than an array of headers.
1574         *
1575         * @param name   The name of the header.
1576         *
1577         * @return The single string header value.  If multiple headers exist,
1578         *         the additional ones are ignored.
1579         * @exception MessagingException
1580         */
1581        private String getSingleHeader(String name) throws MessagingException {
1582            String[] values = getHeader(name);
1583            if (values == null || values.length == 0) {
1584                return null;
1585            } else {
1586                return values[0];
1587            }
1588        }
1589    
1590        /**
1591         * Update the message identifier after headers have been updated.
1592         *
1593         * The default message id is composed of the following items:
1594         *
1595         * 1)  A newly created object's hash code.
1596         * 2)  A uniqueness counter
1597         * 3)  The current time in milliseconds
1598         * 4)  The string JavaMail
1599         * 5)  The user's local address as returned by InternetAddress.getLocalAddress().
1600         *
1601         * @exception MessagingException
1602         */
1603        protected void updateMessageID() throws MessagingException {
1604            StringBuffer id = new StringBuffer();
1605    
1606            id.append('<');
1607            id.append(new Object().hashCode());
1608            id.append('.');
1609            id.append(messageID++);
1610            id.append(System.currentTimeMillis());
1611            id.append('.');
1612            id.append("JavaMail.");
1613    
1614            // get the local address and apply a suitable default.
1615    
1616            InternetAddress localAddress = InternetAddress.getLocalAddress(session);
1617            if (localAddress != null) {
1618                id.append(localAddress.getAddress());
1619            }
1620            else {
1621                id.append("javamailuser@localhost");
1622            }
1623            id.append('>');
1624    
1625            setHeader("Message-ID", id.toString());
1626        }
1627    
1628        /**
1629         * Method used to create a new MimeMessage instance.  This method
1630         * is used whenever the MimeMessage class needs to create a new
1631         * Message instance (e.g, reply()).  This method allows subclasses
1632         * to override the class of message that gets created or set
1633         * default values, if needed.
1634         *
1635         * @param session The session associated with this message.
1636         *
1637         * @return A newly create MimeMessage instance.
1638         * @throws javax.mail.MessagingException if the MimeMessage could not be created
1639         */
1640        protected MimeMessage createMimeMessage(Session session) throws javax.mail.MessagingException {
1641            return new MimeMessage(session);
1642        }
1643    
1644    }