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