1 /* 2 * Copyright (c) 1997, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 /* 27 * @(#)MimeBodyPart.java 1.52 03/02/12 28 */ 29 30 31 32 package com.sun.xml.internal.messaging.saaj.packaging.mime.internet; 33 34 35 import com.sun.xml.internal.messaging.saaj.packaging.mime.MessagingException; 36 import com.sun.xml.internal.messaging.saaj.packaging.mime.util.OutputUtil; 37 import com.sun.xml.internal.messaging.saaj.util.ByteOutputStream; 38 import com.sun.xml.internal.messaging.saaj.util.FinalArrayList; 39 40 import java.util.logging.Level; 41 import java.util.logging.Logger; 42 import javax.activation.DataHandler; 43 import java.io.BufferedInputStream; 44 import java.io.ByteArrayInputStream; 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.io.OutputStream; 48 import java.io.UnsupportedEncodingException; 49 import java.util.List; 50 import javax.activation.DataSource; 51 import com.sun.xml.internal.org.jvnet.mimepull.MIMEPart; 52 53 /** 54 * This class represents a MIME body part. 55 * MimeBodyParts are contained in <code>MimeMultipart</code> 56 * objects. <p> 57 * 58 * MimeBodyPart uses the <code>InternetHeaders</code> class to parse 59 * and store the headers of that body part. <p> 60 * 61 * <hr><strong>A note on RFC 822 and MIME headers</strong><p> 62 * 63 * RFC 822 header fields <strong>must</strong> contain only 64 * US-ASCII characters. MIME allows non ASCII characters to be present 65 * in certain portions of certain headers, by encoding those characters. 66 * RFC 2047 specifies the rules for doing this. The MimeUtility 67 * class provided in this package can be used to to achieve this. 68 * Callers of the <code>setHeader</code>, <code>addHeader</code>, and 69 * <code>addHeaderLine</code> methods are responsible for enforcing 70 * the MIME requirements for the specified headers. In addition, these 71 * header fields must be folded (wrapped) before being sent if they 72 * exceed the line length limitation for the transport (1000 bytes for 73 * SMTP). Received headers may have been folded. The application is 74 * responsible for folding and unfolding headers as appropriate. <p> 75 * 76 * @author John Mani 77 * @author Bill Shannon 78 * @see MimeUtility 79 */ 80 81 public final class MimeBodyPart { 82 83 /** 84 * This part should be presented as an attachment. 85 * @see #getDisposition 86 * @see #setDisposition 87 */ 88 public static final String ATTACHMENT = "attachment"; 89 90 /** 91 * This part should be presented inline. 92 * @see #getDisposition 93 * @see #setDisposition 94 */ 95 public static final String INLINE = "inline"; 96 97 98 // Paranoia: 99 // allow this last minute change to be disabled if it causes problems 100 private static boolean setDefaultTextCharset = true; 101 102 static { 103 try { 104 String s = System.getProperty("mail.mime.setdefaulttextcharset"); 105 // default to true 106 setDefaultTextCharset = s == null || !s.equalsIgnoreCase("false"); 107 } catch (SecurityException sex) { 108 // ignore it 109 } 110 } 111 112 /* 113 Data is represented in one of three forms. 114 Either we have a DataHandler, or byte[] as the raw content image, or the contentStream. 115 It's OK to have more than one of them, provided that they are identical. 116 */ 117 118 /** 119 * The DataHandler object representing this MimeBodyPart's content. 120 */ 121 private DataHandler dh; 122 123 /** 124 * Byte array that holds the bytes of the content of this MimeBodyPart. 125 * Used in a pair with {@link #contentLength} to denote a regision of a buffer 126 * as a valid data. 127 */ 128 private byte[] content; 129 private int contentLength; 130 private int start = 0; 131 132 /** 133 * If the data for this body part was supplied by an 134 * InputStream that implements the SharedInputStream interface, 135 * <code>contentStream</code> is another such stream representing 136 * the content of this body part. In this case, <code>content</code> 137 * will be null. 138 * 139 * @since JavaMail 1.2 140 */ 141 private InputStream contentStream; 142 143 144 145 /** 146 * The InternetHeaders object that stores all the headers 147 * of this body part. 148 */ 149 private final InternetHeaders headers; 150 151 /** 152 * The <code>MimeMultipart</code> object containing this <code>MimeBodyPart</code>, 153 * if known. 154 * @since JavaMail 1.1 155 */ 156 private MimeMultipart parent; 157 158 private MIMEPart mimePart; 159 160 /** 161 * An empty MimeBodyPart object is created. 162 * This body part maybe filled in by a client constructing a multipart 163 * message. 164 */ 165 public MimeBodyPart() { 166 headers = new InternetHeaders(); 167 } 168 169 /** 170 * Constructs a MimeBodyPart by reading and parsing the data from 171 * the specified input stream. The parser consumes data till the end 172 * of the given input stream. The input stream must start at the 173 * beginning of a valid MIME body part and must terminate at the end 174 * of that body part. <p> 175 * 176 * Note that the "boundary" string that delimits body parts must 177 * <strong>not</strong> be included in the input stream. The intention 178 * is that the MimeMultipart parser will extract each body part's bytes 179 * from a multipart stream and feed them into this constructor, without 180 * the delimiter strings. 181 * 182 * @param is the body part Input Stream 183 */ 184 public MimeBodyPart(InputStream is) throws MessagingException { 185 if (!(is instanceof ByteArrayInputStream) && 186 !(is instanceof BufferedInputStream) && 187 !(is instanceof SharedInputStream)) 188 is = new BufferedInputStream(is); 189 190 headers = new InternetHeaders(is); 191 192 if (is instanceof SharedInputStream) { 193 SharedInputStream sis = (SharedInputStream) is; 194 contentStream = sis.newStream(sis.getPosition(), -1); 195 } else { 196 ByteOutputStream bos = null; 197 try { 198 bos = new ByteOutputStream(); 199 bos.write(is); 200 content = bos.getBytes(); 201 contentLength = bos.getCount(); 202 } catch (IOException ioex) { 203 throw new MessagingException("Error reading input stream", ioex); 204 } finally { 205 if (bos != null) 206 bos.close(); 207 } 208 } 209 210 } 211 212 /** 213 * Constructs a MimeBodyPart using the given header and 214 * content bytes. <p> 215 * 216 * Used by providers. 217 * 218 * @param headers The header of this part 219 * @param content bytes representing the body of this part. 220 */ 221 public MimeBodyPart(InternetHeaders headers, byte[] content, int len) { 222 this.headers = headers; 223 this.content = content; 224 this.contentLength = len; 225 } 226 227 public MimeBodyPart( 228 InternetHeaders headers, byte[] content, int start, int len) { 229 this.headers = headers; 230 this.content = content; 231 this.start = start; 232 this.contentLength = len; 233 } 234 235 public MimeBodyPart(MIMEPart part) { 236 mimePart = part; 237 headers = new InternetHeaders(); 238 List<? extends com.sun.xml.internal.org.jvnet.mimepull.Header> hdrs = mimePart.getAllHeaders(); 239 for (com.sun.xml.internal.org.jvnet.mimepull.Header hd : hdrs) { 240 headers.addHeader(hd.getName(), hd.getValue()); 241 } 242 } 243 /** 244 * Return the containing <code>MimeMultipart</code> object, 245 * or <code>null</code> if not known. 246 */ 247 public MimeMultipart getParent() { 248 return parent; 249 } 250 251 /** 252 * Set the parent of this <code>MimeBodyPart</code> to be the specified 253 * <code>MimeMultipart</code>. Normally called by <code>MimeMultipart</code>'s 254 * <code>addBodyPart</code> method. <code>parent</code> may be 255 * <code>null</code> if the <code>MimeBodyPart</code> is being removed 256 * from its containing <code>MimeMultipart</code>. 257 * @since JavaMail 1.1 258 */ 259 public void setParent(MimeMultipart parent) { 260 this.parent = parent; 261 } 262 263 /** 264 * Return the size of the content of this body part in bytes. 265 * Return -1 if the size cannot be determined. <p> 266 * 267 * Note that this number may not be an exact measure of the 268 * content size and may or may not account for any transfer 269 * encoding of the content. <p> 270 * 271 * This implementation returns the size of the <code>content</code> 272 * array (if not null), or, if <code>contentStream</code> is not 273 * null, and the <code>available</code> method returns a positive 274 * number, it returns that number as the size. Otherwise, it returns 275 * -1. 276 * 277 * @return size in bytes, or -1 if not known 278 */ 279 public int getSize() { 280 281 if (mimePart != null) { 282 try { 283 return mimePart.read().available(); 284 } catch (IOException ex) { 285 return -1; 286 } 287 } 288 if (content != null) 289 return contentLength; 290 if (contentStream != null) { 291 try { 292 int size = contentStream.available(); 293 // only believe the size if it's greate than zero, since zero 294 // is the default returned by the InputStream class itself 295 if (size > 0) 296 return size; 297 } catch (IOException ex) { 298 // ignore it 299 } 300 } 301 return -1; 302 } 303 304 /** 305 * Return the number of lines for the content of this MimeBodyPart. 306 * Return -1 if this number cannot be determined. <p> 307 * 308 * Note that this number may not be an exact measure of the 309 * content length and may or may not account for any transfer 310 * encoding of the content. <p> 311 * 312 * This implementation returns -1. 313 * 314 * @return number of lines, or -1 if not known 315 */ 316 public int getLineCount() { 317 return -1; 318 } 319 320 /** 321 * Returns the value of the RFC 822 "Content-Type" header field. 322 * This represents the content type of the content of this 323 * body part. This value must not be null. If this field is 324 * unavailable, "text/plain" should be returned. <p> 325 * 326 * This implementation uses <code>getHeader(name)</code> 327 * to obtain the requisite header field. 328 * 329 * @return Content-Type of this body part 330 */ 331 public String getContentType() { 332 if (mimePart != null) { 333 return mimePart.getContentType(); 334 } 335 String s = getHeader("Content-Type", null); 336 if (s == null) 337 s = "text/plain"; 338 339 return s; 340 } 341 342 /** 343 * Is this MimeBodyPart of the specified MIME type? This method 344 * compares <strong>only the <code>primaryType</code> and 345 * <code>subType</code></strong>. 346 * The parameters of the content types are ignored. <p> 347 * 348 * For example, this method will return <code>true</code> when 349 * comparing a MimeBodyPart of content type <strong>"text/plain"</strong> 350 * with <strong>"text/plain; charset=foobar"</strong>. <p> 351 * 352 * If the <code>subType</code> of <code>mimeType</code> is the 353 * special character '*', then the subtype is ignored during the 354 * comparison. 355 */ 356 public boolean isMimeType(String mimeType) { 357 boolean result; 358 // XXX - lots of room for optimization here! 359 try { 360 ContentType ct = new ContentType(getContentType()); 361 result = ct.match(mimeType); 362 } catch (ParseException ex) { 363 result = getContentType().equalsIgnoreCase(mimeType); 364 } 365 return result; 366 } 367 368 /** 369 * Returns the value of the "Content-Disposition" header field. 370 * This represents the disposition of this part. The disposition 371 * describes how the part should be presented to the user. <p> 372 * 373 * If the Content-Disposition field is unavailable, 374 * null is returned. <p> 375 * 376 * This implementation uses <code>getHeader(name)</code> 377 * to obtain the requisite header field. 378 * 379 * @see #headers 380 */ 381 public String getDisposition() throws MessagingException { 382 String s = getHeader("Content-Disposition", null); 383 384 if (s == null) 385 return null; 386 387 ContentDisposition cd = new ContentDisposition(s); 388 return cd.getDisposition(); 389 } 390 391 /** 392 * Set the "Content-Disposition" header field of this body part. 393 * If the disposition is null, any existing "Content-Disposition" 394 * header field is removed. 395 * 396 * @exception IllegalStateException if this body part is 397 * obtained from a READ_ONLY folder. 398 */ 399 public void setDisposition(String disposition) throws MessagingException { 400 if (disposition == null) 401 removeHeader("Content-Disposition"); 402 else { 403 String s = getHeader("Content-Disposition", null); 404 if (s != null) { 405 /* A Content-Disposition header already exists .. 406 * 407 * Override disposition, but attempt to retain 408 * existing disposition parameters 409 */ 410 ContentDisposition cd = new ContentDisposition(s); 411 cd.setDisposition(disposition); 412 disposition = cd.toString(); 413 } 414 setHeader("Content-Disposition", disposition); 415 } 416 } 417 418 /** 419 * Returns the content transfer encoding from the 420 * "Content-Transfer-Encoding" header 421 * field. Returns <code>null</code> if the header is unavailable 422 * or its value is absent. <p> 423 * 424 * This implementation uses <code>getHeader(name)</code> 425 * to obtain the requisite header field. 426 * 427 * @see #headers 428 */ 429 public String getEncoding() throws MessagingException { 430 String s = getHeader("Content-Transfer-Encoding", null); 431 432 if (s == null) 433 return null; 434 435 s = s.trim(); // get rid of trailing spaces 436 // quick check for known values to avoid unnecessary use 437 // of tokenizer. 438 if (s.equalsIgnoreCase("7bit") || s.equalsIgnoreCase("8bit") || 439 s.equalsIgnoreCase("quoted-printable") || 440 s.equalsIgnoreCase("base64")) 441 return s; 442 443 // Tokenize the header to obtain the encoding (skip comments) 444 HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); 445 446 HeaderTokenizer.Token tk; 447 int tkType; 448 449 for (;;) { 450 tk = h.next(); // get a token 451 tkType = tk.getType(); 452 if (tkType == HeaderTokenizer.Token.EOF) 453 break; // done 454 else if (tkType == HeaderTokenizer.Token.ATOM) 455 return tk.getValue(); 456 else // invalid token, skip it. 457 continue; 458 } 459 return s; 460 } 461 462 /** 463 * Returns the value of the "Content-ID" header field. Returns 464 * <code>null</code> if the field is unavailable or its value is 465 * absent. <p> 466 * 467 * This implementation uses <code>getHeader(name)</code> 468 * to obtain the requisite header field. 469 */ 470 public String getContentID() { 471 return getHeader("Content-ID", null); 472 } 473 474 /** 475 * Set the "Content-ID" header field of this body part. 476 * If the <code>cid</code> parameter is null, any existing 477 * "Content-ID" is removed. 478 * 479 * @exception IllegalStateException if this body part is 480 * obtained from a READ_ONLY folder. 481 * @since JavaMail 1.3 482 */ 483 public void setContentID(String cid) { 484 if (cid == null) 485 removeHeader("Content-ID"); 486 else 487 setHeader("Content-ID", cid); 488 } 489 490 /** 491 * Return the value of the "Content-MD5" header field. Returns 492 * <code>null</code> if this field is unavailable or its value 493 * is absent. <p> 494 * 495 * This implementation uses <code>getHeader(name)</code> 496 * to obtain the requisite header field. 497 */ 498 public String getContentMD5() { 499 return getHeader("Content-MD5", null); 500 } 501 502 /** 503 * Set the "Content-MD5" header field of this body part. 504 * 505 * @exception IllegalStateException if this body part is 506 * obtained from a READ_ONLY folder. 507 */ 508 public void setContentMD5(String md5) { 509 setHeader("Content-MD5", md5); 510 } 511 512 /** 513 * Get the languages specified in the Content-Language header 514 * of this MimeBodyPart. The Content-Language header is defined by 515 * RFC 1766. Returns <code>null</code> if this header is not 516 * available or its value is absent. <p> 517 * 518 * This implementation uses <code>getHeader(name)</code> 519 * to obtain the requisite header field. 520 */ 521 public String[] getContentLanguage() throws MessagingException { 522 String s = getHeader("Content-Language", null); 523 524 if (s == null) 525 return null; 526 527 // Tokenize the header to obtain the Language-tags (skip comments) 528 HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); 529 FinalArrayList v = new FinalArrayList(); 530 531 HeaderTokenizer.Token tk; 532 int tkType; 533 534 while (true) { 535 tk = h.next(); // get a language-tag 536 tkType = tk.getType(); 537 if (tkType == HeaderTokenizer.Token.EOF) 538 break; // done 539 else if (tkType == HeaderTokenizer.Token.ATOM) v.add(tk.getValue()); 540 else // invalid token, skip it. 541 continue; 542 } 543 544 if (v.size() == 0) 545 return null; 546 547 return (String[])v.toArray(new String[v.size()]); 548 } 549 550 /** 551 * Set the Content-Language header of this MimeBodyPart. The 552 * Content-Language header is defined by RFC 1766. 553 * 554 * @param languages array of language tags 555 */ 556 public void setContentLanguage(String[] languages) { 557 StringBuffer sb = new StringBuffer(languages[0]); 558 for (int i = 1; i < languages.length; i++) 559 sb.append(',').append(languages[i]); 560 setHeader("Content-Language", sb.toString()); 561 } 562 563 /** 564 * Returns the "Content-Description" header field of this body part. 565 * This typically associates some descriptive information with 566 * this part. Returns null if this field is unavailable or its 567 * value is absent. <p> 568 * 569 * If the Content-Description field is encoded as per RFC 2047, 570 * it is decoded and converted into Unicode. If the decoding or 571 * conversion fails, the raw data is returned as is. <p> 572 * 573 * This implementation uses <code>getHeader(name)</code> 574 * to obtain the requisite header field. 575 * 576 * @return content description 577 */ 578 public String getDescription() { 579 String rawvalue = getHeader("Content-Description", null); 580 581 if (rawvalue == null) 582 return null; 583 584 try { 585 return MimeUtility.decodeText(MimeUtility.unfold(rawvalue)); 586 } catch (UnsupportedEncodingException ex) { 587 return rawvalue; 588 } 589 } 590 591 /** 592 * Set the "Content-Description" header field for this body part. 593 * If the description parameter is <code>null</code>, then any 594 * existing "Content-Description" fields are removed. <p> 595 * 596 * If the description contains non US-ASCII characters, it will 597 * be encoded using the platform's default charset. If the 598 * description contains only US-ASCII characters, no encoding 599 * is done and it is used as is. <p> 600 * 601 * Note that if the charset encoding process fails, a 602 * MessagingException is thrown, and an UnsupportedEncodingException 603 * is included in the chain of nested exceptions within the 604 * MessagingException. 605 * 606 * @param description content description 607 * @exception IllegalStateException if this body part is 608 * obtained from a READ_ONLY folder. 609 * @exception MessagingException An 610 * UnsupportedEncodingException may be included 611 * in the exception chain if the charset 612 * conversion fails. 613 */ 614 public void setDescription(String description) throws MessagingException { 615 setDescription(description, null); 616 } 617 618 /** 619 * Set the "Content-Description" header field for this body part. 620 * If the description parameter is <code>null</code>, then any 621 * existing "Content-Description" fields are removed. <p> 622 * 623 * If the description contains non US-ASCII characters, it will 624 * be encoded using the specified charset. If the description 625 * contains only US-ASCII characters, no encoding is done and 626 * it is used as is. <p> 627 * 628 * Note that if the charset encoding process fails, a 629 * MessagingException is thrown, and an UnsupportedEncodingException 630 * is included in the chain of nested exceptions within the 631 * MessagingException. 632 * 633 * @param description Description 634 * @param charset Charset for encoding 635 * @exception IllegalStateException if this body part is 636 * obtained from a READ_ONLY folder. 637 * @exception MessagingException An 638 * UnsupportedEncodingException may be included 639 * in the exception chain if the charset 640 * conversion fails. 641 */ 642 public void setDescription(String description, String charset) 643 throws MessagingException { 644 if (description == null) { 645 removeHeader("Content-Description"); 646 return; 647 } 648 649 try { 650 setHeader("Content-Description", MimeUtility.fold(21, 651 MimeUtility.encodeText(description, charset, null))); 652 } catch (UnsupportedEncodingException uex) { 653 throw new MessagingException("Encoding error", uex); 654 } 655 } 656 657 /** 658 * Get the filename associated with this body part. <p> 659 * 660 * Returns the value of the "filename" parameter from the 661 * "Content-Disposition" header field of this body part. If its 662 * not available, returns the value of the "name" parameter from 663 * the "Content-Type" header field of this body part. 664 * Returns <code>null</code> if both are absent. 665 * 666 * @return filename 667 */ 668 public String getFileName() throws MessagingException { 669 String filename = null; 670 String s = getHeader("Content-Disposition", null); 671 672 if (s != null) { 673 // Parse the header .. 674 ContentDisposition cd = new ContentDisposition(s); 675 filename = cd.getParameter("filename"); 676 } 677 if (filename == null) { 678 // Still no filename ? Try the "name" ContentType parameter 679 s = getHeader("Content-Type", null); 680 if (s != null) { 681 try { 682 ContentType ct = new ContentType(s); 683 filename = ct.getParameter("name"); 684 } catch (ParseException pex) { } // ignore it 685 } 686 } 687 return filename; 688 } 689 690 /** 691 * Set the filename associated with this body part, if possible. <p> 692 * 693 * Sets the "filename" parameter of the "Content-Disposition" 694 * header field of this body part. 695 * 696 * @exception IllegalStateException if this body part is 697 * obtained from a READ_ONLY folder. 698 */ 699 public void setFileName(String filename) throws MessagingException { 700 // Set the Content-Disposition "filename" parameter 701 String s = getHeader("Content-Disposition", null); 702 ContentDisposition cd = 703 new ContentDisposition(s == null ? ATTACHMENT : s); 704 cd.setParameter("filename", filename); 705 setHeader("Content-Disposition", cd.toString()); 706 707 /* Also attempt to set the Content-Type "name" parameter, 708 * to satisfy ancient MUAs. 709 * XXX: This is not RFC compliant, and hence should really 710 * be conditional based on some property. Fix this once we 711 * figure out how to get at Properties from here ! 712 */ 713 s = getHeader("Content-Type", null); 714 if (s != null) { 715 try { 716 ContentType cType = new ContentType(s); 717 cType.setParameter("name", filename); 718 setHeader("Content-Type", cType.toString()); 719 } catch (ParseException pex) { } // ignore it 720 } 721 } 722 723 /** 724 * Return a decoded input stream for this body part's "content". <p> 725 * 726 * This implementation obtains the input stream from the DataHandler. 727 * That is, it invokes getDataHandler().getInputStream(); 728 * 729 * @return an InputStream 730 * @exception IOException this is typically thrown by the 731 * DataHandler. Refer to the documentation for 732 * javax.activation.DataHandler for more details. 733 * 734 * @see #getContentStream 735 * @see DataHandler#getInputStream 736 */ 737 public InputStream getInputStream() 738 throws IOException { 739 return getDataHandler().getInputStream(); 740 } 741 742 /** 743 * Produce the raw bytes of the content. This method is used 744 * when creating a DataHandler object for the content. Subclasses 745 * that can provide a separate input stream for just the MimeBodyPart 746 * content might want to override this method. <p> 747 * 748 * @see #content 749 */ 750 /*package*/ InputStream getContentStream() throws MessagingException { 751 if (mimePart != null) { 752 return mimePart.read(); 753 } 754 if (contentStream != null) 755 return ((SharedInputStream)contentStream).newStream(0, -1); 756 if (content != null) 757 return new ByteArrayInputStream(content,start,contentLength); 758 759 throw new MessagingException("No content"); 760 } 761 762 /** 763 * Return an InputStream to the raw data with any Content-Transfer-Encoding 764 * intact. This method is useful if the "Content-Transfer-Encoding" 765 * header is incorrect or corrupt, which would prevent the 766 * <code>getInputStream</code> method or <code>getContent</code> method 767 * from returning the correct data. In such a case the application may 768 * use this method and attempt to decode the raw data itself. <p> 769 * 770 * This implementation simply calls the <code>getContentStream</code> 771 * method. 772 * 773 * @see #getInputStream 774 * @see #getContentStream 775 * @since JavaMail 1.2 776 */ 777 public InputStream getRawInputStream() throws MessagingException { 778 return getContentStream(); 779 } 780 781 /** 782 * Return a DataHandler for this body part's content. <p> 783 * 784 * The implementation provided here works just like the 785 * the implementation in MimeMessage. 786 */ 787 public DataHandler getDataHandler() { 788 if (mimePart != null) { 789 //return an inputstream 790 return new DataHandler(new DataSource() { 791 792 public InputStream getInputStream() throws IOException { 793 return mimePart.read(); 794 } 795 796 public OutputStream getOutputStream() throws IOException { 797 throw new UnsupportedOperationException("getOutputStream cannot be supported : You have enabled LazyAttachments Option"); 798 } 799 800 public String getContentType() { 801 return mimePart.getContentType(); 802 } 803 804 public String getName() { 805 return "MIMEPart Wrapped DataSource"; 806 } 807 }); 808 } 809 if (dh == null) 810 dh = new DataHandler(new MimePartDataSource(this)); 811 return dh; 812 } 813 814 /** 815 * Return the content as a java object. The type of the object 816 * returned is of course dependent on the content itself. For 817 * example, the native format of a text/plain content is usually 818 * a String object. The native format for a "multipart" 819 * content is always a MimeMultipart subclass. For content types that are 820 * unknown to the DataHandler system, an input stream is returned 821 * as the content. <p> 822 * 823 * This implementation obtains the content from the DataHandler. 824 * That is, it invokes getDataHandler().getContent(); 825 * 826 * @return Object 827 * @exception IOException this is typically thrown by the 828 * DataHandler. Refer to the documentation for 829 * javax.activation.DataHandler for more details. 830 */ 831 public Object getContent() throws IOException { 832 return getDataHandler().getContent(); 833 } 834 835 /** 836 * This method provides the mechanism to set this body part's content. 837 * The given DataHandler object should wrap the actual content. 838 * 839 * @param dh The DataHandler for the content 840 * @exception IllegalStateException if this body part is 841 * obtained from a READ_ONLY folder. 842 */ 843 public void setDataHandler(DataHandler dh) { 844 if (mimePart != null) { 845 mimePart = null; 846 } 847 this.dh = dh; 848 this.content = null; 849 this.contentStream = null; 850 removeHeader("Content-Type"); 851 removeHeader("Content-Transfer-Encoding"); 852 } 853 854 /** 855 * A convenience method for setting this body part's content. <p> 856 * 857 * The content is wrapped in a DataHandler object. Note that a 858 * DataContentHandler class for the specified type should be 859 * available to the JavaMail implementation for this to work right. 860 * That is, to do <code>setContent(foobar, "application/x-foobar")</code>, 861 * a DataContentHandler for "application/x-foobar" should be installed. 862 * Refer to the Java Activation Framework for more information. 863 * 864 * @param o the content object 865 * @param type Mime type of the object 866 * @exception IllegalStateException if this body part is 867 * obtained from a READ_ONLY folder. 868 */ 869 public void setContent(Object o, String type) { 870 if (mimePart != null) { 871 mimePart = null; 872 } 873 if (o instanceof MimeMultipart) { 874 setContent((MimeMultipart)o); 875 } else { 876 setDataHandler(new DataHandler(o, type)); 877 } 878 } 879 880 /** 881 * Convenience method that sets the given String as this 882 * part's content, with a MIME type of "text/plain". If the 883 * string contains non US-ASCII characters, it will be encoded 884 * using the platform's default charset. The charset is also 885 * used to set the "charset" parameter. <p> 886 * 887 * Note that there may be a performance penalty if 888 * <code>text</code> is large, since this method may have 889 * to scan all the characters to determine what charset to 890 * use. <p> 891 * If the charset is already known, use the 892 * setText() version that takes the charset parameter. 893 * 894 * @see #setText(String text, String charset) 895 */ 896 public void setText(String text) { 897 setText(text, null); 898 } 899 900 /** 901 * Convenience method that sets the given String as this part's 902 * content, with a MIME type of "text/plain" and the specified 903 * charset. The given Unicode string will be charset-encoded 904 * using the specified charset. The charset is also used to set 905 * the "charset" parameter. 906 */ 907 public void setText(String text, String charset) { 908 if (charset == null) { 909 if (MimeUtility.checkAscii(text) != MimeUtility.ALL_ASCII) 910 charset = MimeUtility.getDefaultMIMECharset(); 911 else 912 charset = "us-ascii"; 913 } 914 setContent(text, "text/plain; charset=" + 915 MimeUtility.quote(charset, HeaderTokenizer.MIME)); 916 } 917 918 /** 919 * This method sets the body part's content to a MimeMultipart object. 920 * 921 * @param mp The multipart object that is the Message's content 922 * @exception IllegalStateException if this body part is 923 * obtained from a READ_ONLY folder. 924 */ 925 public void setContent(MimeMultipart mp) { 926 if (mimePart != null) { 927 mimePart = null; 928 } 929 setDataHandler(new DataHandler(mp, mp.getContentType().toString())); 930 mp.setParent(this); 931 } 932 933 /** 934 * Output the body part as an RFC 822 format stream. 935 * 936 * @exception MessagingException 937 * @exception IOException if an error occurs writing to the 938 * stream or if an error is generated 939 * by the javax.activation layer. 940 * @see DataHandler#writeTo 941 */ 942 public void writeTo(OutputStream os) 943 throws IOException, MessagingException { 944 945 // First, write out the header 946 List hdrLines = headers.getAllHeaderLines(); 947 int sz = hdrLines.size(); 948 for( int i=0; i<sz; i++ ) 949 OutputUtil.writeln((String)hdrLines.get(i),os); 950 951 // The CRLF separator between header and content 952 OutputUtil.writeln(os); 953 954 // Finally, the content. 955 // XXX: May need to account for ESMTP ? 956 if (contentStream != null) { 957 ((SharedInputStream)contentStream).writeTo(0,-1,os); 958 } else 959 if (content != null) { 960 os.write(content,start,contentLength); 961 } else 962 if (dh!=null) { 963 // this is the slowest route, so try it as the last resort 964 OutputStream wos = MimeUtility.encode(os, getEncoding()); 965 getDataHandler().writeTo(wos); 966 if(os!=wos) 967 wos.flush(); // Needed to complete encoding 968 } else if (mimePart != null) { 969 OutputStream wos = MimeUtility.encode(os, getEncoding()); 970 getDataHandler().writeTo(wos); 971 if(os!=wos) 972 wos.flush(); // Needed to complete encoding 973 }else { 974 throw new MessagingException("no content"); 975 } 976 } 977 978 /** 979 * Get all the headers for this header_name. Note that certain 980 * headers may be encoded as per RFC 2047 if they contain 981 * non US-ASCII characters and these should be decoded. 982 * 983 * @param name name of header 984 * @return array of headers 985 * @see MimeUtility 986 */ 987 public String[] getHeader(String name) { 988 return headers.getHeader(name); 989 } 990 991 /** 992 * Get all the headers for this header name, returned as a single 993 * String, with headers separated by the delimiter. If the 994 * delimiter is <code>null</code>, only the first header is 995 * returned. 996 * 997 * @param name the name of this header 998 * @param delimiter delimiter between fields in returned string 999 * @return the value fields for all headers with 1000 * this name 1001 */ 1002 public String getHeader(String name, String delimiter) { 1003 return headers.getHeader(name, delimiter); 1004 } 1005 1006 /** 1007 * Set the value for this header_name. Replaces all existing 1008 * header values with this new value. Note that RFC 822 headers 1009 * must contain only US-ASCII characters, so a header that 1010 * contains non US-ASCII characters must be encoded as per the 1011 * rules of RFC 2047. 1012 * 1013 * @param name header name 1014 * @param value header value 1015 * @see MimeUtility 1016 */ 1017 public void setHeader(String name, String value) { 1018 headers.setHeader(name, value); 1019 } 1020 1021 /** 1022 * Add this value to the existing values for this header_name. 1023 * Note that RFC 822 headers must contain only US-ASCII 1024 * characters, so a header that contains non US-ASCII characters 1025 * must be encoded as per the rules of RFC 2047. 1026 * 1027 * @param name header name 1028 * @param value header value 1029 * @see MimeUtility 1030 */ 1031 public void addHeader(String name, String value) { 1032 headers.addHeader(name, value); 1033 } 1034 1035 /** 1036 * Remove all headers with this name. 1037 */ 1038 public void removeHeader(String name) { 1039 headers.removeHeader(name); 1040 } 1041 1042 /** 1043 * Return all the headers from this Message as an Enumeration of 1044 * Header objects. 1045 */ 1046 public FinalArrayList getAllHeaders() { 1047 return headers.getAllHeaders(); 1048 } 1049 1050 1051 /** 1052 * Add a header line to this body part 1053 */ 1054 public void addHeaderLine(String line) { 1055 headers.addHeaderLine(line); 1056 } 1057 1058 /** 1059 * Examine the content of this body part and update the appropriate 1060 * MIME headers. Typical headers that get set here are 1061 * <code>Content-Type</code> and <code>Content-Transfer-Encoding</code>. 1062 * Headers might need to be updated in two cases: 1063 * 1064 * <br> 1065 * - A message being crafted by a mail application will certainly 1066 * need to activate this method at some point to fill up its internal 1067 * headers. 1068 * 1069 * <br> 1070 * - A message read in from a Store will have obtained 1071 * all its headers from the store, and so doesn't need this. 1072 * However, if this message is editable and if any edits have 1073 * been made to either the content or message structure, we might 1074 * need to resync our headers. 1075 * 1076 * <br> 1077 * In both cases this method is typically called by the 1078 * <code>Message.saveChanges</code> method. 1079 */ 1080 protected void updateHeaders() throws MessagingException { 1081 DataHandler dh = getDataHandler(); 1082 /* 1083 * Code flow indicates null is never returned from 1084 * getdataHandler() - findbugs 1085 */ 1086 //if (dh == null) // Huh ? 1087 // return; 1088 1089 try { 1090 String type = dh.getContentType(); 1091 boolean composite = false; 1092 boolean needCTHeader = getHeader("Content-Type") == null; 1093 1094 ContentType cType = new ContentType(type); 1095 if (cType.match("multipart/*")) { 1096 // If multipart, recurse 1097 composite = true; 1098 Object o = dh.getContent(); 1099 ((MimeMultipart) o).updateHeaders(); 1100 } else if (cType.match("message/rfc822")) { 1101 composite = true; 1102 } 1103 1104 // Content-Transfer-Encoding, but only if we don't 1105 // already have one 1106 if (!composite) { // not allowed on composite parts 1107 if (getHeader("Content-Transfer-Encoding") == null) 1108 setEncoding(MimeUtility.getEncoding(dh)); 1109 1110 if (needCTHeader && setDefaultTextCharset && 1111 cType.match("text/*") && 1112 cType.getParameter("charset") == null) { 1113 /* 1114 * Set a default charset for text parts. 1115 * We really should examine the data to determine 1116 * whether or not it's all ASCII, but that's too 1117 * expensive so we make an assumption: If we 1118 * chose 7bit encoding for this data, it's probably 1119 * ASCII. (MimeUtility.getEncoding will choose 1120 * 7bit only in this case, but someone might've 1121 * set the Content-Transfer-Encoding header manually.) 1122 */ 1123 String charset; 1124 String enc = getEncoding(); 1125 if (enc != null && enc.equalsIgnoreCase("7bit")) 1126 charset = "us-ascii"; 1127 else 1128 charset = MimeUtility.getDefaultMIMECharset(); 1129 cType.setParameter("charset", charset); 1130 type = cType.toString(); 1131 } 1132 } 1133 1134 // Now, let's update our own headers ... 1135 1136 // Content-type, but only if we don't already have one 1137 if (needCTHeader) { 1138 /* 1139 * Pull out "filename" from Content-Disposition, and 1140 * use that to set the "name" parameter. This is to 1141 * satisfy older MUAs (DtMail, Roam and probably 1142 * a bunch of others). 1143 */ 1144 String s = getHeader("Content-Disposition", null); 1145 if (s != null) { 1146 // Parse the header .. 1147 ContentDisposition cd = new ContentDisposition(s); 1148 String filename = cd.getParameter("filename"); 1149 if (filename != null) { 1150 cType.setParameter("name", filename); 1151 type = cType.toString(); 1152 } 1153 } 1154 1155 setHeader("Content-Type", type); 1156 } 1157 } catch (IOException ex) { 1158 throw new MessagingException("IOException updating headers", ex); 1159 } 1160 } 1161 1162 private void setEncoding(String encoding) { 1163 setHeader("Content-Transfer-Encoding", encoding); 1164 } 1165 }