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