001 /** 002 * 003 * Copyright 2003-2006 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.BufferedOutputStream; 021 import java.io.ByteArrayInputStream; 022 import java.io.ByteArrayOutputStream; 023 import java.io.FileOutputStream; 024 import java.io.File; 025 import java.io.IOException; 026 import java.io.InputStream; 027 import java.io.OutputStream; 028 import java.io.UnsupportedEncodingException; 029 import java.util.Enumeration; 030 031 import javax.activation.DataHandler; 032 import javax.activation.FileDataSource; 033 import javax.mail.BodyPart; 034 import javax.mail.MessagingException; 035 import javax.mail.Multipart; 036 import javax.mail.Part; 037 import javax.mail.internet.HeaderTokenizer.Token; 038 import javax.swing.text.AbstractDocument.Content; 039 040 import org.apache.geronimo.mail.util.ASCIIUtil; 041 import org.apache.geronimo.mail.util.SessionUtil; 042 043 /** 044 * @version $Rev: 421852 $ $Date: 2006-07-14 03:02:19 -0700 (Fri, 14 Jul 2006) $ 045 */ 046 public class MimeBodyPart extends BodyPart implements MimePart { 047 // constants for accessed properties 048 private static final String MIME_DECODEFILENAME = "mail.mime.decodefilename"; 049 private static final String MIME_ENCODEFILENAME = "mail.mime.encodefilename"; 050 private static final String MIME_SETDEFAULTTEXTCHARSET = "mail.mime.setdefaulttextcharset"; 051 private static final String MIME_SETCONTENTTYPEFILENAME = "mail.mime.setcontenttypefilename"; 052 053 054 /** 055 * The {@link DataHandler} for this Message's content. 056 */ 057 protected DataHandler dh; 058 /** 059 * This message's content (unless sourced from a SharedInputStream). 060 */ 061 protected byte content[]; 062 /** 063 * If the data for this message was supplied by a {@link SharedInputStream} 064 * then this is another such stream representing the content of this message; 065 * if this field is non-null, then {@link #content} will be null. 066 */ 067 protected InputStream contentStream; 068 /** 069 * This message's headers. 070 */ 071 protected InternetHeaders headers; 072 073 public MimeBodyPart() { 074 headers = new InternetHeaders(); 075 } 076 077 public MimeBodyPart(InputStream in) throws MessagingException { 078 headers = new InternetHeaders(in); 079 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 080 byte[] buffer = new byte[1024]; 081 int count; 082 try { 083 while((count = in.read(buffer, 0, 1024)) != -1) 084 baos.write(buffer, 0, count); 085 } catch (IOException e) { 086 throw new MessagingException(e.toString(),e); 087 } 088 content = baos.toByteArray(); 089 } 090 091 public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException { 092 this.headers = headers; 093 this.content = content; 094 } 095 096 /** 097 * Return the content size of this message. This is obtained 098 * either from the size of the content field (if available) or 099 * from the contentStream, IFF the contentStream returns a positive 100 * size. Returns -1 if the size is not available. 101 * 102 * @return Size of the content in bytes. 103 * @exception MessagingException 104 */ 105 public int getSize() throws MessagingException { 106 if (content != null) { 107 return content.length; 108 } 109 if (contentStream != null) { 110 try { 111 int size = contentStream.available(); 112 if (size > 0) { 113 return size; 114 } 115 } catch (IOException e) { 116 } 117 } 118 return -1; 119 } 120 121 public int getLineCount() throws MessagingException { 122 return -1; 123 } 124 125 public String getContentType() throws MessagingException { 126 String value = getSingleHeader("Content-Type"); 127 if (value == null) { 128 value = "text/plain"; 129 } 130 return value; 131 } 132 133 /** 134 * Tests to see if this message has a mime-type match with the 135 * given type name. 136 * 137 * @param type The tested type name. 138 * 139 * @return If this is a type match on the primary and secondare portion of the types. 140 * @exception MessagingException 141 */ 142 public boolean isMimeType(String type) throws MessagingException { 143 return new ContentType(getContentType()).match(type); 144 } 145 146 /** 147 * Retrieve the message "Content-Disposition" header field. 148 * This value represents how the part should be represented to 149 * the user. 150 * 151 * @return The string value of the Content-Disposition field. 152 * @exception MessagingException 153 */ 154 public String getDisposition() throws MessagingException { 155 String disp = getSingleHeader("Content-Disposition"); 156 if (disp != null) { 157 return new ContentDisposition(disp).getDisposition(); 158 } 159 return null; 160 } 161 162 /** 163 * Set a new dispostion value for the "Content-Disposition" field. 164 * If the new value is null, the header is removed. 165 * 166 * @param disposition 167 * The new disposition value. 168 * 169 * @exception MessagingException 170 */ 171 public void setDisposition(String disposition) throws MessagingException { 172 if (disposition == null) { 173 removeHeader("Content-Disposition"); 174 } 175 else { 176 // the disposition has parameters, which we'll attempt to preserve in any existing header. 177 String currentHeader = getSingleHeader("Content-Disposition"); 178 if (currentHeader != null) { 179 ContentDisposition content = new ContentDisposition(currentHeader); 180 content.setDisposition(disposition); 181 setHeader("Content-Disposition", content.toString()); 182 } 183 else { 184 // set using the raw string. 185 setHeader("Content-Disposition", disposition); 186 } 187 } 188 } 189 190 /** 191 * Retrieves the current value of the "Content-Transfer-Encoding" 192 * header. Returns null if the header does not exist. 193 * 194 * @return The current header value or null. 195 * @exception MessagingException 196 */ 197 public String getEncoding() throws MessagingException { 198 // this might require some parsing to sort out. 199 String encoding = getSingleHeader("Content-Transfer-Encoding"); 200 if (encoding != null) { 201 // we need to parse this into ATOMs and other constituent parts. We want the first 202 // ATOM token on the string. 203 HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME); 204 205 Token token = tokenizer.next(); 206 while (token.getType() != Token.EOF) { 207 // if this is an ATOM type, return it. 208 if (token.getType() == Token.ATOM) { 209 return token.getValue(); 210 } 211 } 212 // not ATOMs found, just return the entire header value....somebody might be able to make sense of 213 // this. 214 return encoding; 215 } 216 // no header, nothing to return. 217 return null; 218 } 219 220 221 /** 222 * Retrieve the value of the "Content-ID" header. Returns null 223 * if the header does not exist. 224 * 225 * @return The current header value or null. 226 * @exception MessagingException 227 */ 228 public String getContentID() throws MessagingException { 229 return getSingleHeader("Content-ID"); 230 } 231 232 public void setContentID(String cid) throws MessagingException { 233 setOrRemoveHeader("Content-ID", cid); 234 } 235 236 public String getContentMD5() throws MessagingException { 237 return getSingleHeader("Content-MD5"); 238 } 239 240 public void setContentMD5(String md5) throws MessagingException { 241 setHeader("Content-MD5", md5); 242 } 243 244 public String[] getContentLanguage() throws MessagingException { 245 return getHeader("Content-Language"); 246 } 247 248 public void setContentLanguage(String[] languages) throws MessagingException { 249 if (languages == null) { 250 removeHeader("Content-Language"); 251 } else if (languages.length == 1) { 252 setHeader("Content-Language", languages[0]); 253 } else { 254 StringBuffer buf = new StringBuffer(languages.length * 20); 255 buf.append(languages[0]); 256 for (int i = 1; i < languages.length; i++) { 257 buf.append(',').append(languages[i]); 258 } 259 setHeader("Content-Language", buf.toString()); 260 } 261 } 262 263 public String getDescription() throws MessagingException { 264 String description = getSingleHeader("Content-Description"); 265 if (description != null) { 266 try { 267 // this could be both folded and encoded. Return this to usable form. 268 return MimeUtility.decodeText(MimeUtility.unfold(description)); 269 } catch (UnsupportedEncodingException e) { 270 // ignore 271 } 272 } 273 // return the raw version for any errors. 274 return description; 275 } 276 277 public void setDescription(String description) throws MessagingException { 278 setDescription(description, null); 279 } 280 281 public void setDescription(String description, String charset) throws MessagingException { 282 if (description == null) { 283 removeHeader("Content-Description"); 284 } 285 else { 286 try { 287 setHeader("Content-Description", MimeUtility.fold(21, MimeUtility.encodeText(description, charset, null))); 288 } catch (UnsupportedEncodingException e) { 289 throw new MessagingException(e.getMessage(), e); 290 } 291 } 292 } 293 294 public String getFileName() throws MessagingException { 295 // see if there is a disposition. If there is, parse off the filename parameter. 296 String disposition = getSingleHeader("Content-Disposition"); 297 String filename = null; 298 299 if (disposition != null) { 300 filename = new ContentDisposition(disposition).getParameter("filename"); 301 } 302 303 // if there's no filename on the disposition, there might be a name parameter on a 304 // Content-Type header. 305 if (filename == null) { 306 String type = getSingleHeader("Content-Type"); 307 if (type != null) { 308 try { 309 filename = new ContentType(type).getParameter("name"); 310 } catch (ParseException e) { 311 } 312 } 313 } 314 // if we have a name, we might need to decode this if an additional property is set. 315 if (filename != null && SessionUtil.getBooleanProperty(MIME_DECODEFILENAME, false)) { 316 try { 317 filename = MimeUtility.decodeText(filename); 318 } catch (UnsupportedEncodingException e) { 319 throw new MessagingException("Unable to decode filename", e); 320 } 321 } 322 323 return filename; 324 } 325 326 327 public void setFileName(String name) throws MessagingException { 328 System.out.println("Setting file name to " + name); 329 // there's an optional session property that requests file name encoding...we need to process this before 330 // setting the value. 331 if (name != null && SessionUtil.getBooleanProperty(MIME_ENCODEFILENAME, false)) { 332 try { 333 name = MimeUtility.encodeText(name); 334 } catch (UnsupportedEncodingException e) { 335 throw new MessagingException("Unable to encode filename", e); 336 } 337 } 338 339 // get the disposition string. 340 String disposition = getDisposition(); 341 // if not there, then this is an attachment. 342 if (disposition == null) { 343 disposition = Part.ATTACHMENT; 344 } 345 346 // now create a disposition object and set the parameter. 347 ContentDisposition contentDisposition = new ContentDisposition(disposition); 348 contentDisposition.setParameter("filename", name); 349 350 // serialize this back out and reset. 351 setHeader("Content-Disposition", contentDisposition.toString()); 352 353 // The Sun implementation appears to update the Content-type name parameter too, based on 354 // another system property 355 if (SessionUtil.getBooleanProperty(MIME_SETCONTENTTYPEFILENAME, true)) { 356 ContentType type = new ContentType(getContentType()); 357 type.setParameter("name", name); 358 setHeader("Content-Type", type.toString()); 359 } 360 } 361 362 public InputStream getInputStream() throws MessagingException, IOException { 363 return getDataHandler().getInputStream(); 364 } 365 366 protected InputStream getContentStream() throws MessagingException { 367 if (contentStream != null) { 368 return contentStream; 369 } 370 371 if (content != null) { 372 return new ByteArrayInputStream(content); 373 } else { 374 throw new MessagingException("No content"); 375 } 376 } 377 378 public InputStream getRawInputStream() throws MessagingException { 379 return getContentStream(); 380 } 381 382 public synchronized DataHandler getDataHandler() throws MessagingException { 383 if (dh == null) { 384 dh = new DataHandler(new MimePartDataSource(this)); 385 } 386 return dh; 387 } 388 389 public Object getContent() throws MessagingException, IOException { 390 return getDataHandler().getContent(); 391 } 392 393 public void setDataHandler(DataHandler handler) throws MessagingException { 394 dh = handler; 395 // if we have a handler override, then we need to invalidate any content 396 // headers that define the types. This information will be derived from the 397 // data heander unless subsequently overridden. 398 removeHeader("Content-Type"); 399 removeHeader("Content-Transfer-Encoding"); 400 401 } 402 403 public void setContent(Object content, String type) throws MessagingException { 404 // Multipart content needs to be handled separately. 405 if (content instanceof Multipart) { 406 setContent((Multipart)content); 407 } 408 else { 409 setDataHandler(new DataHandler(content, type)); 410 } 411 412 } 413 414 public void setText(String text) throws MessagingException { 415 setText(text, null); 416 } 417 418 public void setText(String text, String charset) throws MessagingException { 419 // the default subtype is plain text. 420 setText(text, charset, "plain"); 421 } 422 423 424 public void setText(String text, String charset, String subtype) throws MessagingException { 425 // we need to sort out the character set if one is not provided. 426 if (charset == null) { 427 // if we have non us-ascii characters here, we need to adjust this. 428 if (!ASCIIUtil.isAscii(text)) { 429 charset = MimeUtility.getDefaultMIMECharset(); 430 } 431 else { 432 charset = "us-ascii"; 433 } 434 } 435 setContent(text, "text/plain; charset=" + MimeUtility.quote(charset, HeaderTokenizer.MIME)); 436 } 437 438 public void setContent(Multipart part) throws MessagingException { 439 setDataHandler(new DataHandler(part, part.getContentType())); 440 part.setParent(this); 441 } 442 443 public void writeTo(OutputStream out) throws IOException, MessagingException { 444 headers.writeTo(out, null); 445 // add the separater between the headers and the data portion. 446 out.write('\r'); 447 out.write('\n'); 448 // we need to process this using the transfer encoding type 449 OutputStream encodingStream = MimeUtility.encode(out, getEncoding()); 450 getDataHandler().writeTo(encodingStream); 451 encodingStream.flush(); 452 } 453 454 public String[] getHeader(String name) throws MessagingException { 455 return headers.getHeader(name); 456 } 457 458 public String getHeader(String name, String delimiter) throws MessagingException { 459 return headers.getHeader(name, delimiter); 460 } 461 462 public void setHeader(String name, String value) throws MessagingException { 463 headers.setHeader(name, value); 464 } 465 466 /** 467 * Conditionally set or remove a named header. If the new value 468 * is null, the header is removed. 469 * 470 * @param name The header name. 471 * @param value The new header value. A null value causes the header to be 472 * removed. 473 * 474 * @exception MessagingException 475 */ 476 private void setOrRemoveHeader(String name, String value) throws MessagingException { 477 if (value == null) { 478 headers.removeHeader(name); 479 } 480 else { 481 headers.setHeader(name, value); 482 } 483 } 484 485 public void addHeader(String name, String value) throws MessagingException { 486 headers.addHeader(name, value); 487 } 488 489 public void removeHeader(String name) throws MessagingException { 490 headers.removeHeader(name); 491 } 492 493 public Enumeration getAllHeaders() throws MessagingException { 494 return headers.getAllHeaders(); 495 } 496 497 public Enumeration getMatchingHeaders(String[] name) throws MessagingException { 498 return headers.getMatchingHeaders(name); 499 } 500 501 public Enumeration getNonMatchingHeaders(String[] name) throws MessagingException { 502 return headers.getNonMatchingHeaders(name); 503 } 504 505 public void addHeaderLine(String line) throws MessagingException { 506 headers.addHeaderLine(line); 507 } 508 509 public Enumeration getAllHeaderLines() throws MessagingException { 510 return headers.getAllHeaderLines(); 511 } 512 513 public Enumeration getMatchingHeaderLines(String[] names) throws MessagingException { 514 return headers.getMatchingHeaderLines(names); 515 } 516 517 public Enumeration getNonMatchingHeaderLines(String[] names) throws MessagingException { 518 return headers.getNonMatchingHeaderLines(names); 519 } 520 521 protected void updateHeaders() throws MessagingException { 522 DataHandler handler = getDataHandler(); 523 524 try { 525 // figure out the content type. If not set, we'll need to figure this out. 526 String type = dh.getContentType(); 527 // parse this content type out so we can do matches/compares. 528 ContentType content = new ContentType(type); 529 // is this a multipart content? 530 if (content.match("multipart/*")) { 531 // the content is suppose to be a MimeMultipart. Ping it to update it's headers as well. 532 try { 533 MimeMultipart part = (MimeMultipart)handler.getContent(); 534 part.updateHeaders(); 535 } catch (ClassCastException e) { 536 throw new MessagingException("Message content is not MimeMultipart", e); 537 } 538 } 539 else if (!content.match("message/rfc822")) { 540 // simple part, we need to update the header type information 541 // if no encoding is set yet, figure this out from the data handler. 542 if (getSingleHeader("Content-Transfer-Encoding") == null) { 543 setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler)); 544 } 545 546 // is a content type header set? Check the property to see if we need to set this. 547 if (getHeader("Content-Type") == null) { 548 if (SessionUtil.getBooleanProperty(MIME_SETDEFAULTTEXTCHARSET, true)) { 549 // is this a text type? Figure out the encoding and make sure it is set. 550 if (content.match("text/*")) { 551 // the charset should be specified as a parameter on the MIME type. If not there, 552 // try to figure one out. 553 if (content.getParameter("charset") == null) { 554 555 String encoding = getEncoding(); 556 // if we're sending this as 7-bit ASCII, our character set need to be 557 // compatible. 558 if (encoding != null && encoding.equalsIgnoreCase("7bit")) { 559 content.setParameter("charset", "us-ascii"); 560 } 561 else { 562 // get the global default. 563 content.setParameter("charset", MimeUtility.getDefaultMIMECharset()); 564 } 565 } 566 } 567 } 568 } 569 } 570 571 // if we don't have a content type header, then create one. 572 if (getSingleHeader("Content-Type") == null) { 573 // get the disposition header, and if it is there, copy the filename parameter into the 574 // name parameter of the type. 575 String disp = getHeader("Content-Disposition", null); 576 if (disp != null) { 577 // parse up the string value of the disposition 578 ContentDisposition disposition = new ContentDisposition(disp); 579 // now check for a filename value 580 String filename = disposition.getParameter("filename"); 581 // copy and rename the parameter, if it exists. 582 if (filename != null) { 583 content.setParameter("name", filename); 584 } 585 } 586 // set the header with the updated content type information. 587 setHeader("Content-Type", content.toString()); 588 } 589 590 } catch (IOException e) { 591 throw new MessagingException("Error updating message headers", e); 592 } 593 } 594 595 private String getSingleHeader(String name) throws MessagingException { 596 String[] values = getHeader(name); 597 if (values == null || values.length == 0) { 598 return null; 599 } else { 600 return values[0]; 601 } 602 } 603 604 605 /** 606 * Attach a file to this body part from a File object. 607 * 608 * @param file The source File object. 609 * 610 * @exception IOException 611 * @exception MessagingException 612 */ 613 public void attachFile(File file) throws IOException, MessagingException { 614 FileDataSource dataSource = new FileDataSource(file); 615 setDataHandler(new DataHandler(dataSource)); 616 setFileName(dataSource.getName()); 617 } 618 619 620 /** 621 * Attach a file to this body part from a file name. 622 * 623 * @param file The source file name. 624 * 625 * @exception IOException 626 * @exception MessagingException 627 */ 628 public void attachFile(String file) throws IOException, MessagingException { 629 // just create a File object and attach. 630 attachFile(new File(file)); 631 } 632 633 634 /** 635 * Save the body part content to a given target file. 636 * 637 * @param file The File object used to store the information. 638 * 639 * @exception IOException 640 * @exception MessagingException 641 */ 642 public void saveFile(File file) throws IOException, MessagingException { 643 OutputStream out = new BufferedOutputStream(new FileOutputStream(file)); 644 // we need to read the data in to write it out (sigh). 645 InputStream in = getInputStream(); 646 try { 647 byte[] buffer = new byte[8192]; 648 int length; 649 while ((length = in.read(buffer)) > 0) { 650 out.write(buffer, 0, length); 651 } 652 } 653 finally { 654 // make sure all of the streams are closed before we return 655 if (in != null) { 656 in.close(); 657 } 658 if (out != null) { 659 out.close(); 660 } 661 } 662 } 663 664 665 /** 666 * Save the body part content to a given target file. 667 * 668 * @param file The file name used to store the information. 669 * 670 * @exception IOException 671 * @exception MessagingException 672 */ 673 public void saveFile(String file) throws IOException, MessagingException { 674 saveFile(new File(file)); 675 } 676 }