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