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 }