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