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: 549494 $ $Date: 2007-06-21 10:28:09 -0400 (Thu, 21 Jun 2007) $
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 } catch (IOException e) {
088 throw new MessagingException(e.toString(),e);
089 }
090 content = baos.toByteArray();
091 }
092
093 public MimeBodyPart(InternetHeaders headers, byte[] content) throws MessagingException {
094 this.headers = headers;
095 this.content = content;
096 }
097
098 /**
099 * Return the content size of this message. This is obtained
100 * either from the size of the content field (if available) or
101 * from the contentStream, IFF the contentStream returns a positive
102 * size. Returns -1 if the size is not available.
103 *
104 * @return Size of the content in bytes.
105 * @exception MessagingException
106 */
107 public int getSize() throws MessagingException {
108 if (content != null) {
109 return content.length;
110 }
111 if (contentStream != null) {
112 try {
113 int size = contentStream.available();
114 if (size > 0) {
115 return size;
116 }
117 } catch (IOException e) {
118 }
119 }
120 return -1;
121 }
122
123 public int getLineCount() throws MessagingException {
124 return -1;
125 }
126
127 public String getContentType() throws MessagingException {
128 String value = getSingleHeader("Content-Type");
129 if (value == null) {
130 value = "text/plain";
131 }
132 return value;
133 }
134
135 /**
136 * Tests to see if this message has a mime-type match with the
137 * given type name.
138 *
139 * @param type The tested type name.
140 *
141 * @return If this is a type match on the primary and secondare portion of the types.
142 * @exception MessagingException
143 */
144 public boolean isMimeType(String type) throws MessagingException {
145 return new ContentType(getContentType()).match(type);
146 }
147
148 /**
149 * Retrieve the message "Content-Disposition" header field.
150 * This value represents how the part should be represented to
151 * the user.
152 *
153 * @return The string value of the Content-Disposition field.
154 * @exception MessagingException
155 */
156 public String getDisposition() throws MessagingException {
157 String disp = getSingleHeader("Content-Disposition");
158 if (disp != null) {
159 return new ContentDisposition(disp).getDisposition();
160 }
161 return null;
162 }
163
164 /**
165 * Set a new dispostion value for the "Content-Disposition" field.
166 * If the new value is null, the header is removed.
167 *
168 * @param disposition
169 * The new disposition value.
170 *
171 * @exception MessagingException
172 */
173 public void setDisposition(String disposition) throws MessagingException {
174 if (disposition == null) {
175 removeHeader("Content-Disposition");
176 }
177 else {
178 // the disposition has parameters, which we'll attempt to preserve in any existing header.
179 String currentHeader = getSingleHeader("Content-Disposition");
180 if (currentHeader != null) {
181 ContentDisposition content = new ContentDisposition(currentHeader);
182 content.setDisposition(disposition);
183 setHeader("Content-Disposition", content.toString());
184 }
185 else {
186 // set using the raw string.
187 setHeader("Content-Disposition", disposition);
188 }
189 }
190 }
191
192 /**
193 * Retrieves the current value of the "Content-Transfer-Encoding"
194 * header. Returns null if the header does not exist.
195 *
196 * @return The current header value or null.
197 * @exception MessagingException
198 */
199 public String getEncoding() throws MessagingException {
200 // this might require some parsing to sort out.
201 String encoding = getSingleHeader("Content-Transfer-Encoding");
202 if (encoding != null) {
203 // we need to parse this into ATOMs and other constituent parts. We want the first
204 // ATOM token on the string.
205 HeaderTokenizer tokenizer = new HeaderTokenizer(encoding, HeaderTokenizer.MIME);
206
207 Token token = tokenizer.next();
208 while (token.getType() != Token.EOF) {
209 // if this is an ATOM type, return it.
210 if (token.getType() == Token.ATOM) {
211 return token.getValue();
212 }
213 }
214 // not ATOMs found, just return the entire header value....somebody might be able to make sense of
215 // this.
216 return encoding;
217 }
218 // no header, nothing to return.
219 return null;
220 }
221
222
223 /**
224 * Retrieve the value of the "Content-ID" header. Returns null
225 * if the header does not exist.
226 *
227 * @return The current header value or null.
228 * @exception MessagingException
229 */
230 public String getContentID() throws MessagingException {
231 return getSingleHeader("Content-ID");
232 }
233
234 public void setContentID(String cid) throws MessagingException {
235 setOrRemoveHeader("Content-ID", cid);
236 }
237
238 public String getContentMD5() throws MessagingException {
239 return getSingleHeader("Content-MD5");
240 }
241
242 public void setContentMD5(String md5) throws MessagingException {
243 setHeader("Content-MD5", md5);
244 }
245
246 public String[] getContentLanguage() throws MessagingException {
247 return getHeader("Content-Language");
248 }
249
250 public void setContentLanguage(String[] languages) throws MessagingException {
251 if (languages == null) {
252 removeHeader("Content-Language");
253 } else if (languages.length == 1) {
254 setHeader("Content-Language", languages[0]);
255 } else {
256 StringBuffer buf = new StringBuffer(languages.length * 20);
257 buf.append(languages[0]);
258 for (int i = 1; i < languages.length; i++) {
259 buf.append(',').append(languages[i]);
260 }
261 setHeader("Content-Language", buf.toString());
262 }
263 }
264
265 public String getDescription() throws MessagingException {
266 String description = getSingleHeader("Content-Description");
267 if (description != null) {
268 try {
269 // this could be both folded and encoded. Return this to usable form.
270 return MimeUtility.decodeText(MimeUtility.unfold(description));
271 } catch (UnsupportedEncodingException e) {
272 // ignore
273 }
274 }
275 // return the raw version for any errors.
276 return description;
277 }
278
279 public void setDescription(String description) throws MessagingException {
280 setDescription(description, null);
281 }
282
283 public void setDescription(String description, String charset) throws MessagingException {
284 if (description == null) {
285 removeHeader("Content-Description");
286 }
287 else {
288 try {
289 setHeader("Content-Description", MimeUtility.fold(21, MimeUtility.encodeText(description, charset, null)));
290 } catch (UnsupportedEncodingException e) {
291 throw new MessagingException(e.getMessage(), e);
292 }
293 }
294 }
295
296 public String getFileName() throws MessagingException {
297 // see if there is a disposition. If there is, parse off the filename parameter.
298 String disposition = getSingleHeader("Content-Disposition");
299 String filename = null;
300
301 if (disposition != null) {
302 filename = new ContentDisposition(disposition).getParameter("filename");
303 }
304
305 // if there's no filename on the disposition, there might be a name parameter on a
306 // Content-Type header.
307 if (filename == null) {
308 String type = getSingleHeader("Content-Type");
309 if (type != null) {
310 try {
311 filename = new ContentType(type).getParameter("name");
312 } catch (ParseException e) {
313 }
314 }
315 }
316 // if we have a name, we might need to decode this if an additional property is set.
317 if (filename != null && SessionUtil.getBooleanProperty(MIME_DECODEFILENAME, false)) {
318 try {
319 filename = MimeUtility.decodeText(filename);
320 } catch (UnsupportedEncodingException e) {
321 throw new MessagingException("Unable to decode filename", e);
322 }
323 }
324
325 return filename;
326 }
327
328
329 public void setFileName(String name) throws MessagingException {
330 System.out.println("Setting file name to " + name);
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 // is this a multipart content?
532 if (content.match("multipart/*")) {
533 // the content is suppose to be a MimeMultipart. Ping it to update it's headers as well.
534 try {
535 MimeMultipart part = (MimeMultipart)handler.getContent();
536 part.updateHeaders();
537 } catch (ClassCastException e) {
538 throw new MessagingException("Message content is not MimeMultipart", e);
539 }
540 }
541 else if (!content.match("message/rfc822")) {
542 // simple part, we need to update the header type information
543 // if no encoding is set yet, figure this out from the data handler.
544 if (getSingleHeader("Content-Transfer-Encoding") == null) {
545 setHeader("Content-Transfer-Encoding", MimeUtility.getEncoding(handler));
546 }
547
548 // is a content type header set? Check the property to see if we need to set this.
549 if (getHeader("Content-Type") == null) {
550 if (SessionUtil.getBooleanProperty(MIME_SETDEFAULTTEXTCHARSET, true)) {
551 // is this a text type? Figure out the encoding and make sure it is set.
552 if (content.match("text/*")) {
553 // the charset should be specified as a parameter on the MIME type. If not there,
554 // try to figure one out.
555 if (content.getParameter("charset") == null) {
556
557 String encoding = getEncoding();
558 // if we're sending this as 7-bit ASCII, our character set need to be
559 // compatible.
560 if (encoding != null && encoding.equalsIgnoreCase("7bit")) {
561 content.setParameter("charset", "us-ascii");
562 }
563 else {
564 // get the global default.
565 content.setParameter("charset", MimeUtility.getDefaultMIMECharset());
566 }
567 }
568 }
569 }
570 }
571 }
572
573 // if we don't have a content type header, then create one.
574 if (getSingleHeader("Content-Type") == null) {
575 // get the disposition header, and if it is there, copy the filename parameter into the
576 // name parameter of the type.
577 String disp = getHeader("Content-Disposition", null);
578 if (disp != null) {
579 // parse up the string value of the disposition
580 ContentDisposition disposition = new ContentDisposition(disp);
581 // now check for a filename value
582 String filename = disposition.getParameter("filename");
583 // copy and rename the parameter, if it exists.
584 if (filename != null) {
585 content.setParameter("name", filename);
586 }
587 }
588 // set the header with the updated content type information.
589 setHeader("Content-Type", content.toString());
590 }
591
592 } catch (IOException e) {
593 throw new MessagingException("Error updating message headers", e);
594 }
595 }
596
597 private String getSingleHeader(String name) throws MessagingException {
598 String[] values = getHeader(name);
599 if (values == null || values.length == 0) {
600 return null;
601 } else {
602 return values[0];
603 }
604 }
605
606
607 /**
608 * Attach a file to this body part from a File object.
609 *
610 * @param file The source File object.
611 *
612 * @exception IOException
613 * @exception MessagingException
614 */
615 public void attachFile(File file) throws IOException, MessagingException {
616 FileDataSource dataSource = new FileDataSource(file);
617 setDataHandler(new DataHandler(dataSource));
618 setFileName(dataSource.getName());
619 }
620
621
622 /**
623 * Attach a file to this body part from a file name.
624 *
625 * @param file The source file name.
626 *
627 * @exception IOException
628 * @exception MessagingException
629 */
630 public void attachFile(String file) throws IOException, MessagingException {
631 // just create a File object and attach.
632 attachFile(new File(file));
633 }
634
635
636 /**
637 * Save the body part content to a given target file.
638 *
639 * @param file The File object used to store the information.
640 *
641 * @exception IOException
642 * @exception MessagingException
643 */
644 public void saveFile(File file) throws IOException, MessagingException {
645 OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
646 // we need to read the data in to write it out (sigh).
647 InputStream in = getInputStream();
648 try {
649 byte[] buffer = new byte[8192];
650 int length;
651 while ((length = in.read(buffer)) > 0) {
652 out.write(buffer, 0, length);
653 }
654 }
655 finally {
656 // make sure all of the streams are closed before we return
657 if (in != null) {
658 in.close();
659 }
660 if (out != null) {
661 out.close();
662 }
663 }
664 }
665
666
667 /**
668 * Save the body part content to a given target file.
669 *
670 * @param file The file name used to store the information.
671 *
672 * @exception IOException
673 * @exception MessagingException
674 */
675 public void saveFile(String file) throws IOException, MessagingException {
676 saveFile(new File(file));
677 }
678 }