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