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