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