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