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